Skip to main content

Lesson 1.4: Tables

Learning Objectives

By the end of this lesson, you will:

  • Create and manipulate arrays (sequential tables)
  • Create and use dictionaries (key-value tables)
  • Iterate over tables with different loop patterns
  • Understand table references vs copies
  • Use the table library for common operations

Syntax Note: Coming from JavaScript or After Effects?

JavaScript / After Effects Comparison
ConceptLuauJavaScript
Array{1, 2, 3}[1, 2, 3]
Object/Dict{a = 1, b = 2}{a: 1, b: 2}
First index1 (one-based)0 (zero-based)
Length#arrarr.length
Add to endtable.insert(arr, v)arr.push(v)
Remove lasttable.remove(arr)arr.pop()
Key checktbl.key ~= nil"key" in obj

Critical difference: Luau arrays start at index 1, not 0!


What Is a Table?

A table is Luau's only data structure. It's incredibly flexible — the same syntax creates:

  • Arrays: Ordered lists of values (like JS arrays)
  • Dictionaries: Key-value pairs (like JS objects)
  • Mixed tables: Both at once
  • Everything else: Sets, matrices, objects, etc.
-- Array (sequential numeric keys)
local enemies = {"Slime", "Goblin", "Mage"}

-- Dictionary (string keys)
local player = { name = "Hero", health = 100 }

-- Mixed (both!)
local mixed = {
"first", -- index 1
"second", -- index 2
name = "mixed", -- key "name"
}

Arrays (Sequential Tables)

Creating Arrays

-- Create with values
local fruits = {"Apple", "Banana", "Cherry"}

-- Create empty and add later
local scores = {}
table.insert(scores, 100)
table.insert(scores, 95)
table.insert(scores, 87)

Index Starts at 1!

This is the #1 confusion for JavaScript developers:

local items = {"Sword", "Shield", "Potion"}

print(items[1]) -- "Sword" (FIRST element!)
print(items[2]) -- "Shield"
print(items[3]) -- "Potion"
print(items[0]) -- nil (doesn't exist!)
Arrays Start at 1
-- WRONG mental model (JavaScript)
-- items[0] = "Sword" ❌

-- CORRECT mental model (Luau)
-- items[1] = "Sword" ✓

Every time you write a loop, remember: for i = 1, #arr not for i = 0, #arr - 1

JavaScript Comparison
// JavaScript (0-indexed)
const items = ["Sword", "Shield", "Potion"];
console.log(items[0]); // "Sword"

for (let i = 0; i < items.length; i++) {
console.log(items[i]);
}
-- Luau (1-indexed)
local items = {"Sword", "Shield", "Potion"}
print(items[1]) -- "Sword"

for i = 1, #items do
print(items[i])
end

Output (loop):

Sword
Shield
Potion

Array Length

local arr = {10, 20, 30, 40, 50}
print(#arr) -- 5
JavaScript Comparison
// JavaScript
arr.length // Property

// Luau
#arr -- Operator (like #string for string length)

Modifying Arrays

local items = {"Sword", "Shield"}

-- Add to end
table.insert(items, "Potion")
-- items = {"Sword", "Shield", "Potion"}

-- Add at position 2
table.insert(items, 2, "Armor")
-- items = {"Sword", "Armor", "Shield", "Potion"}

-- Remove last element
local last = table.remove(items)
-- last = "Potion", items = {"Sword", "Armor", "Shield"}

-- Remove at position 1
local first = table.remove(items, 1)
-- first = "Sword", items = {"Armor", "Shield"}

-- Modify directly
items[1] = "Heavy Armor"
-- items = {"Heavy Armor", "Shield"}
JavaScript Comparison
OperationJavaScriptLuau
Add to endarr.push(x)table.insert(arr, x)
Add at indexarr.splice(i, 0, x)table.insert(arr, i, x)
Remove lastarr.pop()table.remove(arr)
Remove at indexarr.splice(i, 1)table.remove(arr, i)
Find indexarr.indexOf(x)table.find(arr, x)

Dictionaries (Key-Value Tables)

Creating Dictionaries

-- Object-like syntax
local player = {
name = "Hero",
health = 100,
level = 5,
}

-- Access with dot notation
print(player.name) -- "Hero"
print(player.health) -- 100

-- Access with bracket notation (same result)
print(player["name"]) -- "Hero"
print(player["health"]) -- 100

When to Use Bracket Notation

Use brackets when:

  • Key is a variable
  • Key has spaces or special characters
  • Key is computed at runtime
local stats = { hp = 100, mp = 50 }

-- Key from variable
local statName = "hp"
print(stats[statName]) -- 100

-- Key with space (must use brackets)
local labels = {
["health points"] = 100,
["mana points"] = 50,
}
print(labels["health points"]) -- 100
JavaScript Comparison

This works exactly like JavaScript objects:

// JavaScript
const player = { name: "Hero", health: 100 };
console.log(player.name); // dot notation
console.log(player["name"]); // bracket notation
console.log(player[varName]); // variable key
-- Luau
local player = { name = "Hero", health = 100 }
print(player.name) -- dot notation
print(player["name"]) -- bracket notation
print(player[varName]) -- variable key

Only difference: Luau uses = not : in definitions.

Adding and Modifying

local inventory = {}

-- Add new keys
inventory.gold = 500
inventory.gems = 10
inventory["magic crystals"] = 3

-- Modify existing
inventory.gold = inventory.gold + 100

-- Remove key (set to nil)
inventory.gems = nil

Iterating Over Tables

Arrays: Use ipairs or Numeric Loop

local items = {"Sword", "Shield", "Potion"}

-- Method 1: ipairs (index and value)
for i, item in ipairs(items) do
print(`{i}: {item}`)
end

-- Method 2: numeric loop (most control)
for i = 1, #items do
print(`{i}: {items[i]}`)
end

Output:

1: Sword
2: Shield
3: Potion
JavaScript Comparison
// JavaScript forEach
items.forEach((item, index) => {
console.log(`${index}: ${item}`); // 0: Sword, 1: Shield...
});

// JavaScript for...of
for (const item of items) {
console.log(item);
}

// JavaScript traditional for
for (let i = 0; i < items.length; i++) {
console.log(items[i]);
}
-- Luau ipairs
for i, item in ipairs(items) do
print(`{i}: {item}`) -- 1: Sword, 2: Shield...
end

-- Luau numeric
for i = 1, #items do
print(items[i])
end

Dictionaries: Use pairs

local stats = { health = 100, mana = 50, stamina = 75 }

-- Method 1: pairs (key and value)
for key, value in pairs(stats) do
print(`{key}: {value}`)
end

Output (order may vary):

health: 100
mana: 50
stamina: 75
Dictionary Order is Not Guaranteed

Unlike arrays, dictionaries don't maintain insertion order. The loop may iterate in any order.

JavaScript Comparison
// JavaScript Object.entries
for (const [key, value] of Object.entries(stats)) {
console.log(`${key}: ${value}`);
}

// JavaScript for...in (keys only)
for (const key in stats) {
console.log(`${key}: ${stats[key]}`);
}
-- Luau pairs
for key, value in pairs(stats) do
print(`{key}: {value}`)
end

Table References vs Copies

Critical concept: When you assign a table to a variable, you're copying the reference, not the data!

local original = {1, 2, 3}
local reference = original -- NOT a copy!

reference[1] = 999

print(original[1]) -- 999 (also changed!)
print(reference[1]) -- 999

Creating True Copies

-- Shallow copy (one level deep)
local original = {1, 2, 3}
local copy = table.clone(original)

copy[1] = 999
print(original[1]) -- 1 (unchanged!)
print(copy[1]) -- 999

-- For nested tables, you need deep copy
local function deepCopy(t)
if type(t) ~= "table" then return t end
local copy = {}
for k, v in pairs(t) do
copy[k] = deepCopy(v)
end
return copy
end
JavaScript Comparison

Same behavior in JavaScript:

// JavaScript - reference, not copy
const original = [1, 2, 3];
const reference = original;
reference[0] = 999;
console.log(original[0]); // 999

// JavaScript - shallow copy
const copy = [...original];
// or: const copy = Object.assign({}, original);
// or: const copy = JSON.parse(JSON.stringify(original));
-- Luau - reference, not copy
local original = {1, 2, 3}
local reference = original
reference[1] = 999
print(original[1]) -- 999

-- Luau - shallow copy
local copy = table.clone(original)

Common Table Operations

OperationSyntaxDescription
Length#tblNumber of sequential elements
Insert at endtable.insert(tbl, value)Add to end
Insert at positiontable.insert(tbl, idx, value)Insert at index
Remove from endtable.remove(tbl)Remove and return last
Remove at positiontable.remove(tbl, idx)Remove and return at index
Find valuetable.find(tbl, value)Returns index or nil
Clear alltable.clear(tbl)Remove all elements
Shallow copytable.clone(tbl)Copy one level
Sorttable.sort(tbl)Sort in place
Concatenatetable.concat(tbl, sep)Join strings

Exercises

Exercise 1: Array Basics ⭐

Premise

Arrays are the most common way to store ordered data in Rive scripts. They power lists of points, particles, and UI elements.

Goal

By the end of this exercise, you will build an array and compute a sum from it.

Use Case

You are storing three keyframe values and need their total. This is a small version of summing a list of positions or values for a chart.

Example scenarios:

  • Summing values for a bar chart
  • Computing a total from a list of inputs

Setup

In Rive Editor:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise1_ArrayBasics
  2. Attach and run:

    • Attach to any shape and press Play

Starter Code

--!strict

export type Exercise1 = {}

function init(self: Exercise1): boolean
-- TODO 1: Create an array with values 10, 20, 30
local values = {}
-- TODO 2: Sum the three values into total
local total = 0

print(`ANSWER: total={total},len={#values}`)
return true
end

function draw(self: Exercise1, renderer: Renderer)
end

return function(): Node<Exercise1>
return {
init = init,
draw = draw,
}
end

Assignment

Complete these tasks:

  1. Create the array with three values
  2. Compute the sum of all three values

Expected Output

Your console output should display:

  • total= — the sum of all values in the array
  • len= — the number of elements in the array

Verify Your Answer

Verify Your Answer

Checklist

  • Array length is 3
  • Total equals 60
  • Output matches the expected line

Exercise 2: Dictionary Lookup ⭐

Premise

Dictionaries let you store named values. This is how you model structured state in Rive scripts.

Goal

By the end of this exercise, you will read values from a dictionary and compute a derived flag.

Use Case

You are tracking a character profile with name, level, and unlocked status. You need a flag that determines if the character is ready for a higher tier.

Example scenarios:

  • Profile data for a UI card
  • Feature unlock flags

Setup

In Rive Editor:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise2_DictionaryLookup
  2. Attach and run:

    • Attach to any shape and press Play

Starter Code

--!strict

export type Exercise2 = {}

function init(self: Exercise2): boolean
local profile = {
name = "Astra",
level = 12,
unlocked = true,
}

-- TODO: ready should be true when level >= 10 and unlocked
local ready = false

print(`ANSWER: {profile.name},{ready}`)
return true
end

function draw(self: Exercise2, renderer: Renderer)
end

return function(): Node<Exercise2>
return {
init = init,
draw = draw,
}
end

Assignment

Complete these tasks:

  1. Use dictionary fields to compute ready
  2. Print the profile name and flag

Expected Output

Your console output should display:

  • The profile name from the dictionary
  • The ready boolean (true when level threshold is met AND unlocked is true)

Verify Your Answer

Verify Your Answer

Checklist

  • ready uses both level and unlocked
  • Output matches the expected line

Exercise 3: Insert and Remove ⭐

Premise

The table library provides standard operations for arrays. You will use table.insert and table.remove constantly.

Goal

By the end of this exercise, you will insert and remove items from an array.

Use Case

You are managing a list of active items in a UI. Items are added when they appear and removed when they disappear.

Example scenarios:

  • Active notifications list
  • Particle pools

Setup

In Rive Editor:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise3_InsertRemove
  2. Attach and run:

    • Attach to any shape and press Play

Starter Code

--!strict

export type Exercise3 = {}

function init(self: Exercise3): boolean
local items = {1, 2, 3}

-- TODO 1: Insert 4 at the end
-- TODO 2: Remove the last item and store it in removed
local removed = 0

print(`ANSWER: len={#items},removed={removed}`)
return true
end

function draw(self: Exercise3, renderer: Renderer)
end

return function(): Node<Exercise3>
return {
init = init,
draw = draw,
}
end

Assignment

Complete these tasks:

  1. Use table.insert to add 4
  2. Use table.remove to remove the last item

Expected Output

Your console output should display:

  • len= — the final array length after insert and remove operations
  • removed= — the value that was removed from the array

Verify Your Answer

Verify Your Answer

Checklist

  • Inserted value is 4
  • Removed value is 4
  • Output matches the expected line

Exercise 4: Copy vs Reference ⭐⭐

Premise

Tables are references in Luau. If you assign one table to another variable, both variables point to the same data.

Goal

By the end of this exercise, you will create a real copy so edits do not affect the original.

Use Case

You need to keep an original set of values while you experiment with temporary changes. If you only copy the reference, you corrupt the original data.

Example scenarios:

  • Editing a temporary style without affecting the base style
  • Trying a layout change before committing it

Setup

In Rive Editor:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise4_CopyReference
  2. Attach and run:

    • Attach to any shape and press Play

Starter Code

--!strict

export type Exercise4 = {}

function init(self: Exercise4): boolean
local original = { x = 1, y = 2 }
-- TODO: Create a copy table and copy fields from original
local copy = {}

-- Modify the copy
-- TODO: Set copy.x to 9

print(`ANSWER: original={original.x},copy={copy.x}`)
return true
end

function draw(self: Exercise4, renderer: Renderer)
end

return function(): Node<Exercise4>
return {
init = init,
draw = draw,
}
end

Assignment

Complete these tasks:

  1. Copy fields from original into a new table
  2. Change copy.x to 9

Expected Output

Your console output should display:

  • original= — the x value from the original table (should remain unchanged)
  • copy= — the x value from the copy (should reflect your modification)

The original must not be affected by changes to the copy.


Verify Your Answer

Verify Your Answer

Checklist

  • original remains unchanged
  • copy is a separate table
  • Output matches the expected line

Exercise 5: Nested Tables ⭐⭐

Premise

Nested tables are how you model structured data like stats, styles, and layouts. You will use this constantly in Rive scripts.

Goal

By the end of this exercise, you will read nested values and compute a total.

Use Case

You are reading a player stats table and need a combined total to drive a UI meter.

Example scenarios:

  • Summing HP and MP for a resource meter
  • Combining size values for layout math

Setup

In Rive Editor:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise5_NestedTables
  2. Attach and run:

    • Attach to any shape and press Play

Starter Code

--!strict

export type Exercise5 = {}

function init(self: Exercise5): boolean
local player = {
name = "Rin",
stats = {
hp = 80,
mp = 20,
}
}

-- TODO: total = hp + mp
local total = 0

print(`ANSWER: {player.name},{total}`)
return true
end

function draw(self: Exercise5, renderer: Renderer)
end

return function(): Node<Exercise5>
return {
init = init,
draw = draw,
}
end

Assignment

Complete these tasks:

  1. Read hp and mp from the nested table
  2. Compute total

Expected Output

Your console output should display:

  • The player name from the root level
  • The total computed from the nested stats table (hp + mp)

Verify Your Answer

Verify Your Answer

Checklist

  • Nested access uses player.stats
  • Total equals 100
  • Output matches the expected line

Exercise 6: Sorting Tables ⭐⭐

Premise

The table library provides tools like sorting that are essential for ordered UI or priority-based logic.

Goal

By the end of this exercise, you will sort an array and read min/max values.

Use Case

You have a list of scores and need to display the highest and lowest values for a leaderboard.

Example scenarios:

  • Sorting scores
  • Ordering UI elements

Setup

In Rive Editor:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise6_Sorting
  2. Attach and run:

    • Attach to any shape and press Play

Starter Code

--!strict

export type Exercise6 = {}

function init(self: Exercise6): boolean
local scores = {5, 2, 9, 4}

-- TODO: Sort scores ascending
-- TODO: min should be first, max should be last
local minScore = 0
local maxScore = 0

print(`ANSWER: min={minScore},max={maxScore}`)
return true
end

function draw(self: Exercise6, renderer: Renderer)
end

return function(): Node<Exercise6>
return {
init = init,
draw = draw,
}
end

Assignment

Complete these tasks:

  1. Sort the scores array
  2. Read the min and max values

Expected Output

Your console output should display:

  • min= — the smallest value in the sorted array (first element after sorting)
  • max= — the largest value in the sorted array (last element after sorting)

Verify Your Answer

Verify Your Answer

Checklist

  • table.sort is used
  • Min and max are correct
  • Output matches the expected line

Knowledge Check

Q:What index does the first element of a Luau array have?
Q:How do you get the number of elements in an array?
Q:What function adds an element to the end of an array?
Q:What happens when you assign a table to a new variable?
Q:Which function creates a shallow copy of a table?

Common Mistakes

Top Mistakes
  1. Using index 0 — Arrays start at 1 in Luau!
  2. Forgetting # is only for sequential arrays — It counts until the first nil
  3. Thinking = copies tables — It only copies the reference
  4. Modifying while iterating — Can cause skipped elements
  5. Expecting dictionary order — Dictionaries don't guarantee order

Quick Reference

-- Arrays
local arr = {1, 2, 3}
arr[1] -- First element
#arr -- Length
table.insert(arr, v) -- Add to end
table.insert(arr, i, v) -- Add at index
table.remove(arr) -- Remove last
table.remove(arr, i) -- Remove at index
table.find(arr, v) -- Find index

-- Dictionaries
local dict = { a = 1, b = 2 }
dict.a -- Get by key
dict["a"] -- Same thing
dict.c = 3 -- Add/modify
dict.a = nil -- Remove

-- Iteration
for i, v in ipairs(arr) do ... end -- Array
for k, v in pairs(dict) do ... end -- Dictionary
for k, v in pairs(tbl) do ... end

-- Utilities
table.sort(arr) -- Sort ascending
table.sort(arr, fn) -- Sort with comparator
table.clone(tbl) -- Shallow copy
table.clear(tbl) -- Remove all
table.concat(arr, sep) -- Join strings

Module 1 Complete!

You've finished the Fundamentals module. You now know:

  • Variables and types
  • Control flow and loops
  • Functions and closures
  • Tables (arrays and dictionaries)

Next, we'll explore Luau's powerful type system.

→ Module 2: Type System

Next Steps