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
tablelibrary for common operations
Syntax Note: Coming from JavaScript or After Effects?
| Concept | Luau | JavaScript |
|---|---|---|
| Array | {1, 2, 3} | [1, 2, 3] |
| Object/Dict | {a = 1, b = 2} | {a: 1, b: 2} |
| First index | 1 (one-based) | 0 (zero-based) |
| Length | #arr | arr.length |
| Add to end | table.insert(arr, v) | arr.push(v) |
| Remove last | table.remove(arr) | arr.pop() |
| Key check | tbl.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!)
-- 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 (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
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"}
| Operation | JavaScript | Luau |
|---|---|---|
| Add to end | arr.push(x) | table.insert(arr, x) |
| Add at index | arr.splice(i, 0, x) | table.insert(arr, i, x) |
| Remove last | arr.pop() | table.remove(arr) |
| Remove at index | arr.splice(i, 1) | table.remove(arr, i) |
| Find index | arr.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
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 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
Unlike arrays, dictionaries don't maintain insertion order. The loop may iterate in any order.
// 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
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
| Operation | Syntax | Description |
|---|---|---|
| Length | #tbl | Number of sequential elements |
| Insert at end | table.insert(tbl, value) | Add to end |
| Insert at position | table.insert(tbl, idx, value) | Insert at index |
| Remove from end | table.remove(tbl) | Remove and return last |
| Remove at position | table.remove(tbl, idx) | Remove and return at index |
| Find value | table.find(tbl, value) | Returns index or nil |
| Clear all | table.clear(tbl) | Remove all elements |
| Shallow copy | table.clone(tbl) | Copy one level |
| Sort | table.sort(tbl) | Sort in place |
| Concatenate | table.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.
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:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise1_ArrayBasics
- Assets panel →
-
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:
- Create the array with three values
- Compute the sum of all three values
Expected Output
Your console output should display:
total=— the sum of all values in the arraylen=— the number of elements in the array
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.
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:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise2_DictionaryLookup
- Assets panel →
-
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:
- Use dictionary fields to compute
ready - Print the profile name and flag
Expected Output
Your console output should display:
- The profile name from the dictionary
- The
readyboolean (true when level threshold is met AND unlocked is true)
Verify Your Answer
Checklist
-
readyuses 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.
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:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise3_InsertRemove
- Assets panel →
-
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:
- Use
table.insertto add 4 - Use
table.removeto remove the last item
Expected Output
Your console output should display:
len=— the final array length after insert and remove operationsremoved=— the value that was removed from the array
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.
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:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise4_CopyReference
- Assets panel →
-
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:
- Copy fields from original into a new table
- 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
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.
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:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise5_NestedTables
- Assets panel →
-
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:
- Read hp and mp from the nested table
- 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
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.
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:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise6_Sorting
- Assets panel →
-
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:
- Sort the scores array
- 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
Checklist
- table.sort is used
- Min and max are correct
- Output matches the expected line
Knowledge Check
Common Mistakes
- Using index 0 — Arrays start at 1 in Luau!
- Forgetting
#is only for sequential arrays — It counts until the first nil - Thinking
=copies tables — It only copies the reference - Modifying while iterating — Can cause skipped elements
- 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.
Next Steps
- Continue to Iteration
- Need a refresher? Review Quick Reference