Collapse
A section that opens and shuts. Its content genuinely leaves the layout when it closes, so everything below moves up to fill the space.
primitivesince v1not focusable
A Collapse is a section the player can open and shut. Its first child is the
header — tapping it toggles the section — and everything after it is the content,
which genuinely leaves the layout when the section closes, so the siblings
below move up to fill the gap. Picture a long settings page where only the
section you are working in takes room. It is also the node that proved this
format could never ship baked rectangles: a screen with one of these on it
re-measures itself while the player is looking at it.
Viewport: 960 × 360

- hover — off
- pressed — off
- focused — off
- selected — off
- disabled — off
Reported from the last painted frame
Not running — press Run to draw this on the GPU.
import { Accordion, Button, Collapse, Column, Row, Text } from "@zabloo/react";
/** A header is `children[0]`, and tapping it toggles — whatever it is. */
function Header({ label }: { label: string }) {
return (
<Button
variant="secondary"
layout={{ padding: "{space.3}", justify: "start", align: "center" }}
>
<Text style={{ color: "{color.text}", fontSize: "{text.sm}" }}>{label}</Text>
</Button>
);
}
function Line({ name, value }: { name: string; value: string }) {
return (
<Row layout={{ justify: "space-between", align: "center" }}>
<Text style={{ color: "{color.muted}", fontSize: "{text.sm}" }}>{name}</Text>
<Text style={{ color: "{color.text}", fontSize: "{text.sm}" }}>{value}</Text>
</Row>
);
}
export default function CollapseAccordion() {
return (
<Column
// `align: "stretch"` on the ROOT is what hands the panel the view's own
// width; centring it instead would pin the panel to its content and no
// change of viewport could reach it (ZAB-153).
layout={{ grow: 1, justify: "center", align: "stretch", padding: "{space.6}" }}
style={{ background: "{color.bg}" }}
>
<Column
id="panel"
layout={{ padding: "{space.5}", gap: "{space.4}", align: "stretch" }}
style={{
background: "{color.surface}",
radius: "{radius.lg}",
borderWidth: "{border.hairline}",
borderColor: "{color.line}",
}}
>
<Text style={{ color: "{color.text}", fontSize: "{text.lg}" }}>Options</Text>
<Accordion layout={{ gap: "{space.2}", align: "stretch" }}>
<Collapse
id="audio"
open
transition={{ duration: "{motion.fast}" }}
layout={{ gap: "{space.2}", align: "stretch" }}
>
<Header label="Audio" />
{/* Everything from here down is content: it leaves the layout when
the section closes, and the siblings below move up. */}
<Column
layout={{ padding: "{space.3}", gap: "{space.2}", align: "stretch" }}
style={{ background: "{color.slot}", radius: "{radius.md}" }}
>
<Line name="Master volume" value="80" />
<Line name="Music" value="45" />
</Column>
</Collapse>
<Collapse
id="video"
open={false}
transition={{ duration: "{motion.fast}" }}
layout={{ gap: "{space.2}", align: "stretch" }}
>
<Header label="Video" />
<Column
layout={{ padding: "{space.3}", gap: "{space.2}", align: "stretch" }}
style={{ background: "{color.slot}", radius: "{radius.md}" }}
>
<Line name="Resolution" value="1920 × 1080" />
<Line name="Quality" value="High" />
</Column>
</Collapse>
</Accordion>
</Column>
</Column>
);
}{
"v": 1,
"tokens": {
"color.bg": "#0b0d13",
"color.surface": "#0e1016",
"color.raised": "#14141a",
"color.line": "#ffffff1f",
"color.line-strong": "#ffffff24",
"color.text": "#ffffff",
"color.muted": "#a1a1aa",
"color.faint": "#8a8a93",
"color.on-brand": "#ffffff",
"color.brand": "#8b5cf6",
"color.brand-strong": "#7c3aed",
"color.brand-hover": "#8b5cf6",
"color.brand-pressed": "#6d28d9",
"color.brand-soft": "#8b5cf61f",
"color.gold": "#fcd34d",
"color.slot": "#ffffff08",
"color.danger": "#f87171",
"radius.sm": 6,
"radius.md": 10,
"radius.lg": 14,
"radius.pill": 999,
"border.hairline": 1,
"border.focus": 2,
"space.1": 4,
"space.2": 8,
"space.3": 12,
"space.4": 16,
"space.5": 20,
"space.6": 24,
"text.xs": 11,
"text.sm": 13,
"text.md": 15,
"text.lg": 17,
"motion.fast": 120,
"motion.loop": 900
},
"views": {
"collapse-accordion": {
"type": "Container",
"layout": {
"direction": "column",
"grow": 1,
"justify": "center",
"align": "stretch",
"padding": "{space.6}"
},
"style": {
"background": "{color.bg}"
},
"children": [
{
"type": "Container",
"id": "panel",
"layout": {
"direction": "column",
"padding": "{space.5}",
"gap": "{space.4}",
"align": "stretch"
},
"style": {
"background": "{color.surface}",
"radius": "{radius.lg}",
"borderWidth": "{border.hairline}",
"borderColor": "{color.line}"
},
"children": [
{
"type": "Text",
"style": {
"color": "{color.text}",
"fontSize": "{text.lg}"
},
"text": "Options"
},
{
"type": "Container",
"layout": {
"direction": "column",
"gap": "{space.2}",
"align": "stretch"
},
"group": "exclusive-open",
"children": [
{
"type": "Collapse",
"id": "audio",
"layout": {
"gap": "{space.2}",
"align": "stretch"
},
"transition": {
"duration": "{motion.fast}"
},
"open": true,
"children": [
{
"type": "Button",
"layout": {
"padding": "{space.3}",
"justify": "start",
"align": "center"
},
"style": {
"background": "#00000000",
"borderWidth": "{border.hairline}",
"borderColor": "{color.line-strong}",
"radius": "{radius.md}",
"color": "{color.text}"
},
"states": {
"hover": {
"style": {
"background": "{color.slot}"
}
},
"pressed": {
"style": {
"background": "{color.brand-soft}"
}
},
"focused": {
"style": {
"borderWidth": "{border.focus}",
"borderColor": "{color.brand}"
}
},
"disabled": {
"style": {
"opacity": 0.45
}
}
},
"transition": {
"duration": "{motion.fast}"
},
"children": [
{
"type": "Text",
"style": {
"color": "{color.text}",
"fontSize": "{text.sm}"
},
"text": "Audio"
}
]
},
{
"type": "Container",
"layout": {
"direction": "column",
"padding": "{space.3}",
"gap": "{space.2}",
"align": "stretch"
},
"style": {
"background": "{color.slot}",
"radius": "{radius.md}"
},
"children": [
{
"type": "Container",
"layout": {
"direction": "row",
"justify": "space-between",
"align": "center"
},
"children": [
{
"type": "Text",
"style": {
"color": "{color.muted}",
"fontSize": "{text.sm}"
},
"text": "Master volume"
},
{
"type": "Text",
"style": {
"color": "{color.text}",
"fontSize": "{text.sm}"
},
"text": "80"
}
]
},
{
"type": "Container",
"layout": {
"direction": "row",
"justify": "space-between",
"align": "center"
},
"children": [
{
"type": "Text",
"style": {
"color": "{color.muted}",
"fontSize": "{text.sm}"
},
"text": "Music"
},
{
"type": "Text",
"style": {
"color": "{color.text}",
"fontSize": "{text.sm}"
},
"text": "45"
}
]
}
]
}
]
},
{
"type": "Collapse",
"id": "video",
"layout": {
"gap": "{space.2}",
"align": "stretch"
},
"transition": {
"duration": "{motion.fast}"
},
"open": false,
"children": [
{
"type": "Button",
"layout": {
"padding": "{space.3}",
"justify": "start",
"align": "center"
},
"style": {
"background": "#00000000",
"borderWidth": "{border.hairline}",
"borderColor": "{color.line-strong}",
"radius": "{radius.md}",
"color": "{color.text}"
},
"states": {
"hover": {
"style": {
"background": "{color.slot}"
}
},
"pressed": {
"style": {
"background": "{color.brand-soft}"
}
},
"focused": {
"style": {
"borderWidth": "{border.focus}",
"borderColor": "{color.brand}"
}
},
"disabled": {
"style": {
"opacity": 0.45
}
}
},
"transition": {
"duration": "{motion.fast}"
},
"children": [
{
"type": "Text",
"style": {
"color": "{color.text}",
"fontSize": "{text.sm}"
},
"text": "Video"
}
]
},
{
"type": "Container",
"layout": {
"direction": "column",
"padding": "{space.3}",
"gap": "{space.2}",
"align": "stretch"
},
"style": {
"background": "{color.slot}",
"radius": "{radius.md}"
},
"children": [
{
"type": "Container",
"layout": {
"direction": "row",
"justify": "space-between",
"align": "center"
},
"children": [
{
"type": "Text",
"style": {
"color": "{color.muted}",
"fontSize": "{text.sm}"
},
"text": "Resolution"
},
{
"type": "Text",
"style": {
"color": "{color.text}",
"fontSize": "{text.sm}"
},
"text": "1920 × 1080"
}
]
},
{
"type": "Container",
"layout": {
"direction": "row",
"justify": "space-between",
"align": "center"
},
"children": [
{
"type": "Text",
"style": {
"color": "{color.muted}",
"fontSize": "{text.sm}"
},
"text": "Quality"
},
{
"type": "Text",
"style": {
"color": "{color.text}",
"fontSize": "{text.sm}"
},
"text": "High"
}
]
}
]
}
]
}
]
}
]
}
]
}
}
}The two tables below are almost the same, which on this node is the interesting part: what the format carries is only where the section starts. Whether it is open right now is runtime state the SDK holds, and it is nowhere in what ships.
Authoring props
What you write in @zabloo/react.
| Prop | Type | Default | Description |
|---|---|---|---|
| open | boolean | true | Initial open state. |
| children | ReactNode | absent | First child = header; the rest = content. |
On top of these, every component takes the node base props — id, visible,
disabled, layout, style, states, transition, autofocus, clip —
plus variant, which the theme resolves away at export time and which never
appears in the IR.
IR props
What ships to the game, after the authoring layer is gone.
| Prop | Type | Default | Description |
|---|---|---|---|
| open | boolean | true | Initial state. Not bindable — the runtime state belongs to the SDK. |
| children | ZNode[] | [] | children[0] = header; children[1..] = collapsible content. |
Afterwards it is runtime state held by the SDK — like a Button’s
pressed — and it survives neither serialization nor a reload. It is deliberately not
bindable: a bound open would make the game the owner of something the player is holding, and
the two would fight. A game that needs to drive it says so out of band, with SetOpen(id, open).
Slots
children[0]is the header. Always in layout, and tapping it toggles the region. It is focusable whatever it is: the header of aCollapsejoins the focusable set the way aButtondoes.children[1..]is the content. It enters and leaves the layout withdisplay:nonesemantics — the same single hiding mechanism asvisible— so closing genuinely removes it and the siblings below move up.
Behavior
- States
The
Collapseitself carries none of its own; its header carrieshover,pressedandfocused. Thatpressedcomes only from the keyboard or the pad — a header is not one of the types the pointer presses, so a tap toggles the section without ever lighting the down-state. Both carrydisabled: declared on theCollapseit reaches the header, and a disabled header no longer opens or closes the section.- Focusable
The node itself is not; its header is, whatever kind of node it happens to be. That is the one place in the format where a child joins the focusable set because of its position.
- Motion
With a
transition, aCollapseanimates its own height between closed and its content’s natural height, and clips while it does — so the content is cut by the box closing over it, without the author having to ask for a clip.- Actions
The player taps the header → the section opens or shuts and the layout below it re-flows → the game hears nothing. Opening is not a named action. If the game does need to know, make the header a
Buttonwith anonClick; the other direction is the host channel, withSetOpen(id, open).
In an accordion
Inside a Container with group: "exclusive-open",
opening one Collapse closes its siblings. The behavior is the parent’s: the
Collapse declares nothing about it, and an SDK that ignores the group leaves
independent collapses. That split is the same one every group behavior follows —
the child stays a child, and the rule lives one level up.
Degradation
On an older SDK every section is open and stays open. Nothing is hidden — but nothing can be put away either.
As a Container: everything shows, permanently open, and the header is just another child. Nothing is lost — what goes is the ability to close it, which is the least harmful way for a disclosure control to fail.
Composition
The header is an ordinary node, so what it looks like is a layout question.
- A
Buttonas the header — use it when the game needs to hear that the section was opened, since the toggle itself sends nothing. - A
Rowwith a label and a chevron — use it for the ordinary case, where the header is only a handle. - An
<Accordion>around several — use it when the sections are long enough that two open at once would bury the rest. - A
transition— use it when the section is short; on a long one the height animation is a slow way to say the same thing.
// A section that starts closed. The header is children[0], whatever it is.
<Collapse open={false}>
<Button onClick="audio-toggled"><Text>Audio</Text></Button>
<Slider value={{ bind: "settings.volume" }} />
</Collapse>
// An accordion: the "only one open" rule is the parent's, not the children's.
<Accordion layout={{ gap: 8 }}>
<Collapse open={false}>…</Collapse>
<Collapse open={false}>…</Collapse>
</Accordion>
// With a transition it animates its own height, and clips while it does.
<Collapse open transition={{ duration: 160 }}>
<Text>Advanced</Text>
<Column layout={{ gap: 8 }}>…</Column>
</Collapse>