Overview
The accordion widget creates collapsible content sections with expand/collapse controls. Each item has a title header and associated content panel. Supports single or multiple expanded sections.
Basic Usage
import { ui } from "@rezi-ui/core";
import { defineWidget } from "@rezi-ui/core";
const MyAccordion = defineWidget((ctx) => {
const [expanded, setExpanded] = ctx.useState<readonly string[]>(["item-1"]);
return ui.accordion({
id: "faq-accordion",
expanded,
onChange: setExpanded,
items: [
{
key: "item-1",
title: "What is Rezi?",
content: ui.text("Rezi is a runtime-agnostic TUI framework for TypeScript."),
},
{
key: "item-2",
title: "How do I install it?",
content: ui.text("Run: npm install @rezi-ui/core @rezi-ui/node"),
},
{
key: "item-3",
title: "Does it support TypeScript?",
content: ui.text("Yes, Rezi is written in TypeScript with full type definitions."),
},
],
});
});
Props
Unique identifier for focus routing.
items
readonly AccordionItem[]
required
Array of accordion items with key, title, and content.
expanded
readonly string[]
required
Array of expanded item keys (controlled state).
onChange
(expanded: readonly string[]) => void
required
Callback when expansion state changes.
When false, only one item can be expanded at a time (single-expand mode).
Design System Props
dsVariant
WidgetVariant
default:"soft"
Design system visual variant: "solid", "soft", "outline", "ghost".
dsTone
WidgetTone
default:"default"
Design system color tone: "default", "primary", "success", "warning", "danger".
Design system size preset: "xs", "sm", "md", "lg", "xl".
Keyboard Navigation
Accordion supports full keyboard control:
| Key | Action |
|---|
Up | Navigate to previous item header |
Down | Navigate to next item header |
Enter | Toggle focused item |
Space | Toggle focused item |
Home | Jump to first item |
End | Jump to last item |
Tab | Move focus into expanded content |
Single-Expand Mode
Only one section can be open at a time:
import { defineWidget } from "@rezi-ui/core";
const SingleAccordion = defineWidget((ctx) => {
const [expanded, setExpanded] = ctx.useState<readonly string[]>(["section-1"]);
return ui.accordion({
id: "single-accordion",
expanded,
onChange: setExpanded,
allowMultiple: false,
items: [
{
key: "section-1",
title: "Account Settings",
content: AccountSettingsForm(),
},
{
key: "section-2",
title: "Privacy Settings",
content: PrivacySettingsForm(),
},
{
key: "section-3",
title: "Notification Settings",
content: NotificationSettingsForm(),
},
],
});
});
Multi-Expand Mode
Multiple sections can be open simultaneously:
import { defineWidget } from "@rezi-ui/core";
const MultiAccordion = defineWidget((ctx) => {
const [expanded, setExpanded] = ctx.useState<readonly string[]>([
"filters",
"sorting",
]);
return ui.accordion({
id: "multi-accordion",
expanded,
onChange: setExpanded,
allowMultiple: true,
items: [
{
key: "filters",
title: "Filters",
content: FilterControls(),
},
{
key: "sorting",
title: "Sorting",
content: SortControls(),
},
{
key: "grouping",
title: "Grouping",
content: GroupControls(),
},
],
});
});
Rich Content
Accordion items can contain any VNode content:
ui.accordion({
id: "rich-accordion",
expanded: state.expanded,
onChange: setExpanded,
items: [
{
key: "profile",
title: "Profile Information",
content: ui.column({ gap: 1 }, [
ui.field({
label: "Name",
children: ui.input("name", state.name),
}),
ui.field({
label: "Email",
children: ui.input("email", state.email),
}),
ui.actions([
ui.button("save-profile", "Save", { intent: "primary" }),
]),
]),
},
{
key: "stats",
title: "Statistics",
content: ui.column({ gap: 1 }, [
ui.text("Performance Metrics", { variant: "heading" }),
ui.progress(0.75, { label: "CPU Usage" }),
ui.progress(0.42, { label: "Memory Usage" }),
ui.sparkline([10, 20, 15, 30, 25, 18, 22]),
]),
},
],
});
Design System Integration
// Primary solid variant
ui.accordion({
id: "primary-accordion",
expanded: state.expanded,
onChange: setExpanded,
dsVariant: "solid",
dsTone: "primary",
items: [
{ key: "item-1", title: "Section 1", content: Content1() },
{ key: "item-2", title: "Section 2", content: Content2() },
],
});
// Large size
ui.accordion({
id: "large-accordion",
expanded: state.expanded,
onChange: setExpanded,
dsSize: "lg",
items: [
{ key: "item-1", title: "Large Section 1", content: Content1() },
{ key: "item-2", title: "Large Section 2", content: Content2() },
],
});
Expand/Collapse All
import { defineWidget } from "@rezi-ui/core";
const AccordionWithControls = defineWidget((ctx) => {
const items = [
{ key: "item-1", title: "Section 1", content: ui.text("Content 1") },
{ key: "item-2", title: "Section 2", content: ui.text("Content 2") },
{ key: "item-3", title: "Section 3", content: ui.text("Content 3") },
];
const [expanded, setExpanded] = ctx.useState<readonly string[]>([]);
const expandAll = () => {
setExpanded(items.map((item) => item.key));
};
const collapseAll = () => {
setExpanded([]);
};
return ui.column({ gap: 1 }, [
ui.row({ gap: 1 }, [
ui.button({
id: "expand-all",
label: "Expand All",
onPress: expandAll,
dsSize: "sm",
}),
ui.button({
id: "collapse-all",
label: "Collapse All",
onPress: collapseAll,
dsSize: "sm",
}),
]),
ui.accordion({
id: "controlled-accordion",
expanded,
onChange: setExpanded,
items,
}),
]);
});
FAQ Example
import { defineWidget } from "@rezi-ui/core";
const FAQ = defineWidget((ctx) => {
const [expanded, setExpanded] = ctx.useState<readonly string[]>([]);
return ui.panel("Frequently Asked Questions", [
ui.accordion({
id: "faq",
expanded,
onChange: setExpanded,
allowMultiple: true,
items: [
{
key: "q1",
title: "How do I get started?",
content: ui.column({ gap: 1 }, [
ui.text("Install Rezi with npm:"),
ui.text("npm install @rezi-ui/core @rezi-ui/node", {
variant: "code",
}),
ui.text("Then create your first app!"),
]),
},
{
key: "q2",
title: "What platforms are supported?",
content: ui.text(
"Rezi runs on Node.js 18+ and Bun 1.3+ on Linux, macOS, and Windows (x64/arm64)."
),
},
{
key: "q3",
title: "Can I use TypeScript?",
content: ui.text(
"Yes! Rezi is written in TypeScript with full type definitions."
),
},
],
}),
]);
});
Settings Panel Example
import { defineWidget } from "@rezi-ui/core";
const SettingsPanel = defineWidget((ctx) => {
const [expanded, setExpanded] = ctx.useState<readonly string[]>(["general"]);
const [settings, setSettings] = ctx.useState({
name: "Alice",
theme: "dark",
notifications: true,
});
return ui.panel("Settings", [
ui.accordion({
id: "settings-accordion",
expanded,
onChange: setExpanded,
allowMultiple: false,
items: [
{
key: "general",
title: "General",
content: ui.form([
ui.field({
label: "Name",
children: ui.input("settings-name", settings.name, {
onInput: (value) => setSettings({ ...settings, name: value }),
}),
}),
]),
},
{
key: "appearance",
title: "Appearance",
content: ui.form([
ui.field({
label: "Theme",
children: ui.select({
id: "settings-theme",
value: settings.theme,
options: [
{ value: "light", label: "Light" },
{ value: "dark", label: "Dark" },
{ value: "auto", label: "Auto" },
],
onChange: (value) => setSettings({ ...settings, theme: value }),
}),
}),
]),
},
{
key: "notifications",
title: "Notifications",
content: ui.form([
ui.checkbox({
id: "settings-notifications",
checked: settings.notifications,
label: "Enable notifications",
onChange: (checked) =>
setSettings({ ...settings, notifications: checked }),
}),
]),
},
],
}),
]);
});
Best Practices
- Use single-expand for wizards - Sequential forms work better with
allowMultiple: false
- Provide meaningful titles - Headers should clearly describe content
- Keep content concise - Long sections are hard to navigate in TUI
- Preserve state - Store expanded state in parent to persist across renders
- Use for hierarchical data - Good for settings, FAQs, and documentation
Accessibility
- Headers have
role="button" and aria-expanded attributes
- Content panels have
role="region" when expanded
- Focus moves between headers with Up/Down arrows
- Enter/Space keys toggle expansion
- Tab key moves focus into expanded content
- Tabs - For switching between views
- Tree - For hierarchical file/folder navigation
- Panel - For grouped content sections
Location in Source
- Implementation:
packages/core/src/widgets/accordion.ts
- Types:
packages/core/src/widgets/types.ts:1544-1565
- Factory:
packages/core/src/widgets/ui.ts:1501