Skip to content

Accordion / expander

In review
Live · web

Single-open: opening one closes the rest

Per-session overrides. Leave untouched to inherit the registry defaults for this model + provider.

temperature 0.7 · top_p 0.95 · max_tokens 4096

Multi-open: each section toggles independently

$ bun run build
✓ 142 modules · 2.4s

Header accessory + uncontrolled: always-visible delete, each self-manages

Open me and delete me independently. The trash button never toggles the row, and no host holds my open state (defaultExpanded seeds it, I keep it).

Zebra (experimental): opt-in alternating row tone; default stays plain

Single-open: sampling · multi-open: 1 section expanded. The island is live.

// Web: React, consuming @dizyx/nockerl-tokens. (Web is the laggard platform. Spec + live demo,
// not yet exported from @dizyx/nockerl-react; promotion is tracked.)
import { useState } from 'react';
export function SessionAdvanced() {
// Single-open: opening one closes the rest (Voice `expandedID` semantics).
const [open, setOpen] = useState<string | null>('sampling');
return (
<Accordion mode="single" value={open} onValueChange={setOpen}>
<AccordionItem
id="sampling"
title="Sampling"
hint="Temperature, top-p, penalties"
icon={<TuneIcon />}
pill={{ text: 'MODIFIED', tone: 'accent' }}
>
<SamplingSliders />
</AccordionItem>
<AccordionItem id="workspace" title="Workspace" hint="Locked by your administrator" disabled>
<WorkspaceFields />
</AccordionItem>
</Accordion>
);
}
PropTypeDefaultDescription
mode'single' | 'multiple''single'Disclosure policy. single closes siblings on open (Voice); multiple lets sections stack open (Android).
valuestring | string[] | nullControlled open id(s). A string | null in single mode, a string[] in multiple mode.
onValueChange(value) => voidFired on toggle with the next open id(s). Pair with value for a controlled accordion.
AccordionItem.id *stringStable identity used by value to track which item is open.
AccordionItem.title *stringHeader label: the disclosure's accessible name (label.large role).
AccordionItem.hintstringOptional supporting line under the title (body.small role).
AccordionItem.iconReactNodeOptional leading glyph rendered in a fixed slot, so titles align with or without it.
AccordionItem.pill{ text: string; tone: 'accent' | 'success' | 'warning' }Optional trailing header pill. accent for 'modified', warm tones for status only.
AccordionItem.headerAccessoryReactNodeAlways-visible header controls pinned to the trailing edge, left of the chevron. Rendered outside the toggle button so a delete IconButton / count Badge fires independently and never toggles the row. Reserves trailing space via --nk-acc-accessory-reserve (default 64px). Matches the headerAccessory seat from FormSection / TodoWidget.
AccordionItem.expandedbooleanControlled open state. When set, the item is controlled. Pair with onExpandedChange.
AccordionItem.onExpandedChange(next: boolean) => voidFired with the requested next open state on toggle (controlled).
AccordionItem.defaultExpandedbooleanfalseInitial open state for uncontrolled usage: the item self-manages when neither expanded nor onExpandedChange is given.
AccordionItem.disabledbooleanfalseInert and clearly seen (never faded to invisible); header is not focusable.
Header accessory + controlled binding, cross-platform

A header trailing-accessory slot renders always-visible controls (a delete button, a count badge) in the header, collapsed or expanded, pinned left of the chevron and outside the toggle target so interactive controls fire independently and never toggle the row (a real <button> cannot nest inside the row’s toggle <button>, so the accessory is a sibling). Expansion is controlled (expanded + onExpandedChange) or uncontrolled (defaultExpanded, self-managed). One shared contract: Compose (a sibling in the header Row) and Swift (a sibling in the header HStack) mirror the same shape; group single/multi-open coordination stays host-owned as before.

Experimental: row zebra, not ratified

An opt-in zebra wash tones alternating rows (even rows a whisper of neutral on-card @ 4%, odd rows plain) on the accordion container, theme-following, default off, resting surface only (beneath hairlines / selection / hover). Experimental: the design lead is evaluating; not canon. Mirror lives on all three platforms via the same container opt-in.

Cross-platform drift (tracked for reconciliation)

The intended spec is unified; these are the real divergences between the shipped platforms:

  • Disclosure indicator (resolved): a rotating chevron wins (interpolatable, reduced-motion-safe). Compose (today swaps KeyboardArrowDown↔KeyboardArrowUp) and Swift (swaps chevron.right↔chevron.down) conform to rotation, not icon-swap.
  • Chevron position (resolved): trailing wins (leading is content’s seat, trailing is the affordance’s). Swift (today leading) conforms.
  • Open policy (resolved): a mode param, default single, multiple opt-in. Voice (single-open expandedID) and Android (multi-open stacked sections) both adopt the shared mode.
  • Reveal motion: web animates grid-rows height + opacity; Compose expandVertically + fadeIn; Swift a .spring layout change. Same intent, three engines.
  • Packaging: neither native app ships a reusable Accordion; both inline the header+body pattern. Web’s @dizyx/nockerl-react Accordion/AccordionItem is the intended canonical surface.