Layout Script Protocol
Learning Objectives
- Understand Layout Script lifecycle functions
- Use
measure()to propose sizes for Hug layouts - Use
resize()to react to container size changes - Position children programmatically within layouts
AE/JS Syntax Comparison
| Concept | CSS/JavaScript | Luau (Rive Layout Scripts) |
|---|---|---|
| Container query | @container (min-width) | resize(self, size) callback |
| Preferred size | width: fit-content | measure() returns Vector |
| Flexbox gap | gap: 10px | Manual positioning in resize() |
| Grid layout | display: grid | Custom logic in resize() |
Layout Scripts let you implement custom layout algorithms. Unlike CSS where you pick from predefined models (flex, grid), in Rive you write the exact positioning logic yourself.
Overview
Layout Scripts extend Node Scripts to give you programmatic control over Layout components. They enable custom layout behaviors like masonry grids, carousels, and dynamic spacing logic.
When to use Layout Scripts:
- Custom responsive layouts (masonry, carousel)
- Dynamic spacing/positioning logic
- Programmatic child arrangement
- Data-driven layout systems
The Layout Script Template
--!strict
export type MyLayout = {
spacing: Input<number>,
columns: Input<number>,
}
function init(self: MyLayout, context: Context): boolean
-- Initialize layout state
return true
end
function advance(self: MyLayout, seconds: number): boolean
-- Per-frame updates (for animated layouts)
return true
end
function update(self: MyLayout)
-- Called when inputs (spacing, columns) change
end
function draw(self: MyLayout, renderer: Renderer)
-- Custom rendering (optional)
end
-- Propose ideal size (only for "Hug" fit type)
function measure(self: MyLayout): Vector
return Vector.xy(200, 150)
end
-- REQUIRED: React to container size changes
function resize(self: MyLayout, size: Vector)
-- Position children based on available size
local width = size.x
local height = size.y
-- ... layout logic here
end
return function(): Layout<MyLayout>
return {
init = init,
advance = advance,
update = update,
draw = draw,
measure = measure,
resize = resize,
spacing = 10,
columns = 3,
}
end
Type Signature Reference
Layout Protocol Functions
| Function | Signature | Required | Description |
|---|---|---|---|
init | (self: T, context: Context): boolean | Yes | Initialize layout state |
resize | (self: T, size: Vector) | Yes | React to container size changes |
measure | (self: T): Vector | No | Propose ideal size (Hug fit only) |
update | (self: T) | No | Respond to input changes |
advance | (self: T, seconds: number): boolean | No | Per-frame animation updates |
draw | (self: T, renderer: Renderer) | No | Custom rendering |
Factory Return Type
return function(): Layout<YourLayoutType>
return {
init = init,
resize = resize, -- Required
measure = measure, -- Optional
update = update, -- Optional
-- ... your inputs with defaults
}
end
Lifecycle Functions
resize(self, size: Vector) — REQUIRED
Called whenever the layout container changes size. This is where you position children.
function resize(self: MyLayout, size: Vector)
print(`Container resized to: {size.x} x {size.y}`)
-- Position children here
end
When it's called:
- On initial layout load
- When the container resizes
- When parent layout changes
measure(self): Vector — Optional
Proposes an ideal size for layouts with Hug fit type. Return a Vector with your preferred width and height.
function measure(self: MyLayout): Vector
-- Calculate ideal size based on content
local totalWidth = self.columns * (self.itemWidth + self.spacing)
local totalHeight = self.rows * (self.itemHeight + self.spacing)
return Vector.xy(totalWidth, totalHeight)
end
Important: measure() is only effective when the Layout's Fit type is set to Hug. For Fill or Fixed layouts, the return value is ignored.
Other Lifecycle Functions
Layout Scripts inherit all Node Script lifecycle functions:
| Function | Purpose |
|---|---|
init(self, context) | One-time initialization |
advance(self, seconds) | Per-frame updates (for animations) |
update(self) | React to Input changes |
draw(self, renderer) | Custom rendering |
Fit Types and measure()
The effect of measure() depends on the Layout's Fit setting:
| Fit Type | Behavior | measure() Effect |
|---|---|---|
| Hug | Container sizes to fit content | Your measure() return is used |
| Fill | Container fills available space | measure() ignored |
| Fixed | Container has explicit size | measure() ignored |
-- measure() only matters for Hug layouts
function measure(self: MyLayout): Vector
if self.fitType == "hug" then
return Vector.xy(self.contentWidth, self.contentHeight)
end
-- For Fill/Fixed, this return is ignored anyway
return Vector.xy(0, 0)
end
Practice Exercises
Exercise 1: Simple Horizontal Layout ⭐
Premise
Layout Scripts give you precise control over positioning. Understanding resize() is fundamental—it's where all the layout magic happens.
By the end of this exercise, you will implement a simple horizontal layout that spaces items evenly.
Use Case
You're building a toolbar with evenly-spaced buttons. The toolbar width may change, and buttons should redistribute automatically.
Setup
In Rive Editor:
-
Create the Layout:
- Add a Layout component to the Artboard
- Set Fit to "Fill" (width) and "Fixed" (height: 60)
-
Create the script:
- Assets panel →
+→ Script → Layout Script - Name it
Exercise1_HorizontalLayout
- Assets panel →
-
Attach and run:
- Add the script as child of the Layout
- Press Play
Starter Code
--!strict
export type Exercise1 = {
itemCount: Input<number>,
}
function resize(self: Exercise1, size: Vector)
local count = self.itemCount
local width = size.x
-- TODO: Calculate spacing between items
-- Formula: spacing = width / (count + 1)
local spacing = 0
print(`ANSWER: count={count},width={width},spacing={spacing}`)
end
return function(): Layout<Exercise1>
return {
resize = resize,
itemCount = 4,
}
end
Assignment
- Calculate
spacingusing the formula:width / (itemCount + 1) - Run with default itemCount of 4 and a container width of 500
Expected Output
Your console output should display the item count, container width, and calculated spacing between items.
Verify Your Answer
Checklist
-
spacingis calculated correctly - Formula divides width by (count + 1)
- Output matches expected line
Exercise 2: Grid Layout Calculation ⭐⭐
Premise
Grid layouts require calculating both row and column positions. The resize() callback gives you the container dimensions to work with.
By the end of this exercise, you will calculate row and column counts for a responsive grid.
Use Case
You're building a responsive image gallery. As the container resizes, the grid should adjust column count to fit available space.
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Layout Script - Name it
Exercise2_GridLayout
- Assets panel →
-
Attach and run:
- Add to a Layout component
- Press Play
Starter Code
--!strict
export type Exercise2 = {
itemSize: Input<number>,
itemCount: Input<number>,
}
function resize(self: Exercise2, size: Vector)
local itemSize = self.itemSize
local itemCount = self.itemCount
local width = size.x
-- TODO 1: Calculate columns that fit in width
-- Use: math.floor(width / itemSize)
local columns = 0
-- TODO 2: Calculate rows needed for all items
-- Use: math.ceil(itemCount / columns)
local rows = 0
print(`ANSWER: cols={columns},rows={rows}`)
end
return function(): Layout<Exercise2>
return {
resize = resize,
itemSize = 100,
itemCount = 10,
}
end
Assignment
- Calculate
columnsusingmath.floor(width / itemSize) - Calculate
rowsusingmath.ceil(itemCount / columns) - Container width is 450, itemSize is 100, itemCount is 10
Expected Output
Your console output should display the calculated column and row counts for the grid.
Verify Your Answer
Checklist
- Columns calculated with math.floor
- Rows calculated with math.ceil
- Output matches expected line
Exercise 3: Measure for Hug Layout ⭐⭐
Premise
When a Layout uses "Hug" fit type, your measure() function proposes the ideal size. The container then sizes to fit.
By the end of this exercise, you will implement measure() to propose a size based on content.
Use Case
You're building a dynamic tag cloud where the container should grow to fit all tags. The layout "hugs" its content.
Setup
In Rive Editor:
-
Create the Layout:
- Add a Layout component
- Set Fit to "Hug" for both width and height
-
Create the script:
- Assets panel →
+→ Script → Layout Script - Name it
Exercise3_HugLayout
- Assets panel →
-
Attach and run
Starter Code
--!strict
export type Exercise3 = {
tagWidth: Input<number>,
tagHeight: Input<number>,
tagCount: Input<number>,
columns: Input<number>,
}
function measure(self: Exercise3): Vector
local tagW = self.tagWidth
local tagH = self.tagHeight
local count = self.tagCount
local cols = self.columns
-- TODO 1: Calculate total width needed
-- Width = columns * tagWidth
local totalWidth = 0
-- TODO 2: Calculate rows needed
local rows = math.ceil(count / cols)
-- TODO 3: Calculate total height needed
-- Height = rows * tagHeight
local totalHeight = 0
print(`ANSWER: w={totalWidth},h={totalHeight}`)
return Vector.xy(totalWidth, totalHeight)
end
function resize(self: Exercise3, size: Vector)
-- Layout positioning would go here
end
return function(): Layout<Exercise3>
return {
measure = measure,
resize = resize,
tagWidth = 80,
tagHeight = 30,
tagCount = 12,
columns = 4,
}
end
Assignment
- Calculate
totalWidthascolumns * tagWidth - Calculate
totalHeightasrows * tagHeight - With tagWidth=80, tagHeight=30, tagCount=12, columns=4
Expected Output
Your console output should display the proposed width and height for the hug layout.
Verify Your Answer
Checklist
- totalWidth = columns × tagWidth
- rows = ceil(tagCount / columns)
- totalHeight = rows × tagHeight
- Output matches expected line
Knowledge Check
Common Mistakes
-
Forgetting resize() is required
-- WRONG: No resize function
return function(): Layout<MyLayout>
return { measure = measure } -- Error!
end
-- CORRECT: Always include resize
return function(): Layout<MyLayout>
return { resize = resize, measure = measure }
end -
Expecting measure() to work with Fill layouts
-- measure() is ignored for Fill fit type
-- The container size is determined by parent, not your measure() -
Not using the size parameter in resize()
-- WRONG: Ignoring container size
function resize(self: MyLayout, size: Vector)
positionAt(100, 100) -- Hardcoded, doesn't adapt!
end
-- CORRECT: Use size to calculate positions
function resize(self: MyLayout, size: Vector)
local centerX = size.x / 2
local centerY = size.y / 2
positionAt(centerX, centerY)
end
Self-Assessment Checklist
- I understand when resize() is called
- I know the difference between measure() and resize()
- I can implement a simple grid layout in resize()
- I understand which Fit type uses measure()
- I can calculate positions based on container size
Next Steps
- Continue to Converter Script Protocol
- Need a refresher? Review Quick Reference