Skip to content

Containers

“Perrier” edited this page May 10, 2026 · 1 revision

Containers

Components that wrap other components for collapsible sections or state-driven content.

Accordion

Vertical stack of collapsible sections. Headers are always visible; clicking a header toggles the body underneath. Bodies are attached for the entire lifetime of the accordion (state inside a body persists across collapses).

Two variants:

  • Accordion — multi-open. Any combination of sections may be open.
  • AccordionSingle — single-open. Opening one section closes the others.
// Section factories
AccordionSection(String title, UIComponent body)
AccordionSection(String title, UIComponent body, boolean defaultOpen)

// Multi-open
Accordion(AccordionSection... sections)
Accordion(Style headerStyle, AccordionSection... sections)
Accordion(State<Set<Integer>> openSet, AccordionSection... sections)
Accordion(State<Set<Integer>> openSet, Style headerStyle, AccordionSection... sections)

// Single-open
AccordionSingle(AccordionSection... sections)
AccordionSingle(Style headerStyle, AccordionSection... sections)
AccordionSingle(State<Integer> openIndex, AccordionSection... sections)
AccordionSingle(State<Integer> openIndex, Style headerStyle, AccordionSection... sections)

Multi-open example

Accordion(
    AccordionSection("General", Column(
        Text("Name: ..."),
        TextField(name)
    ), /* defaultOpen */ true),

    AccordionSection("Audio", Column(
        Text("Master volume"),
        SliderInt(volume, 0, 100)
    )),

    AccordionSection("Advanced", Text("..."))
);

State-driven (multi-open)

Pass your own State<Set<Integer>> to control which sections are open programmatically:

State<Set<Integer>> open = State.of(Set.of(0));   // section 0 starts open

Accordion(open,
    AccordionSection("First", /* body */),
    AccordionSection("Second", /* body */),
    AccordionSection("Third", /* body */));

open.set(Set.of(0, 2));   // open first and third

State-driven (single-open)

AccordionSingle(State<Integer> openIndex, ...) exposes the active section as a single integer (-1 = nothing open). The library bridges the underlying Set<Integer> to your Integer state in both directions, with the equals-check on State.set preventing the round-trip from looping.

State<Integer> active = State.of(0);

AccordionSingle(active,
    AccordionSection("Profile", /* body */),
    AccordionSection("Settings", /* body */),
    AccordionSection("Help", /* body */));

active.set(2);   // jump to "Help"

Dynamic

Container whose single child is derived from a State<T>. The builder runs every time the state changes; the resulting component replaces the previous child. Use this for tabs, paginated views, conditional sections, list rendering — anywhere the shape of the tree depends on state.

<T> Dynamic(State<T> state, Function<T, UIComponent> builder)
<T> Dynamic(State<T> state, Supplier<UIComponent> builder)
State<Integer> tab = State.of(0);

Column(
    Row(
        Button("Profile",  Style.onClick((x, y, b) -> tab.set(0)).build()),
        Button("Settings", Style.onClick((x, y, b) -> tab.set(1)).build()),
        Button("About",    Style.onClick((x, y, b) -> tab.set(2)).build())),

    Dynamic(tab, t -> switch (t) {
        case 0 -> renderProfile();
        case 1 -> renderSettings();
        default -> renderAbout();
    })
);

Dynamic only swaps when the State<T> actually changes. If you derive a value (e.g. state.map(...) then Dynamic on the derived state), the equals-short-circuit on State.set keeps things efficient — equal mapped values won't trigger a rebuild.

Tabs are not yet a first-class component. The pattern above (Row of buttons + Dynamic on a State<Integer>) is the recommended substitute until a dedicated Tabs component lands.

Clone this wiki locally