Skip to content
zabloo

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.

collapse-accordion.viewIR v1
Viewport

Viewport: 960 × 360

An Options panel with two collapsible sections: Audio, open, showing Master volume 80 and Music 45, and Video, closed, showing only its header.
STATE
  • 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>
  );
}
Two sections under one accordion. Press Run and open Video, then watch what moves: Audio shuts, everything below it travels up, and the whole panel re-measures — live, from a document that never states either section's height.

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.

PropTypeDefaultDescription
openbooleantrueInitial open state.
childrenReactNodeabsentFirst 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.

PropTypeDefaultDescription
openbooleantrueInitial state. Not bindable — the runtime state belongs to the SDK.
childrenZNode[][]children[0] = header; children[1..] = collapsible content.
open is where it starts, not where it is

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 a Collapse joins the focusable set the way a Button does.
  • children[1..] is the content. It enters and leaves the layout with display:none semantics — the same single hiding mechanism as visible — so closing genuinely removes it and the siblings below move up.

Behavior

States

The Collapse itself carries none of its own; its header carries hover, pressed and focused. That pressed comes 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 carry disabled: declared on the Collapse it 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, a Collapse animates 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 Button with an onClick; the other direction is the host channel, with SetOpen(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

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.

AudioVideo

AudioMaster volumeVideoResolution

Composition

The header is an ordinary node, so what it looks like is a layout question.

  • A Button as the header — use it when the game needs to hear that the section was opened, since the toggle itself sends nothing.
  • A Row with 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>