ComponentsHorizontalAccordion
HorizontalAccordion
Keep every section in reach while giving one room to shine. A focused workspace for project overviews, activity, and everyday actions.
On this page
Install component source from the public registry, or use the local workspace package. Follow the setup guide first. The npm package is not published yet.
View registry files- Motion for React
(motion)Animations, transitions, and reduced-motion handling.
Shared foundation: React, clsx for conditional classes, tailwind-merge for resolving utility-class conflicts. Styles use Tailwind CSS and Dethink CSS variables.
Based on this component’s source and registry composition. Libraries used only in showcase examples are excluded.
Import the component into your page or component file.
import { HorizontalAccordion } from "@dethink/components";Try the examples, then open the code to use them in your app. Focus a blade and use Arrow, Home, and End keys to navigate; Enter or Space activates.
Project command centre
Four sections, one workspace. Follow the project, filter activity, preview documents, or assign a review lead. Your choices survive switching panels. defaultValue and collapsible={false} keep the workspace open.
Website refresh
On trackGood work.
Coming together.
One final review before the next chapter goes live.
Next milestone
Design sign-off · 26 Sep
Show sourceexamples/horizontal-accordion/basic.tsx
"use client";
/* eslint-disable jsx-a11y/no-redundant-roles -- Safari needs explicit list roles when list markers are removed. */
import { useId, useState } from "react";
import {
Activity,
ArrowUpRight,
Bell,
Check,
FileText,
LayoutDashboard,
Users,
} from "lucide-react";
import { Button, HorizontalAccordion } from "@dethink/components";
const activity = [
{
title: "Checkout flow approved",
person: "Maya Chen",
time: "12 min ago",
type: "Design",
},
{
title: "Preview build is ready",
person: "Alex Morgan",
time: "38 min ago",
type: "Engineering",
},
{
title: "Navigation review complete",
person: "Sam Rivera",
time: "1 hour ago",
type: "Design",
},
];
const files = [
{
title: "Launch brief",
meta: "Updated today · 3 min read",
body: "A calmer checkout, clearer navigation, and a faster path from discovery to purchase. The release is ready for the final accessibility review.",
},
{
title: "Review checklist",
meta: "Updated yesterday · 2 min read",
body: "Verify keyboard navigation, review payment errors, check the mobile layout, and confirm that every order confirmation includes a receipt.",
},
];
const team = [
{ name: "Maya Chen", initials: "MC", role: "Product design" },
{ name: "Alex Morgan", initials: "AM", role: "Engineering" },
{ name: "Sam Rivera", initials: "SR", role: "Quality assurance" },
];
const panelClass = "flex h-full flex-col gap-5 p-6 sm:p-8";
function OverviewPanel() {
const [following, setFollowing] = useState(false);
return (
<div className={panelClass}>
<div className="flex flex-wrap items-center justify-between gap-2">
<p className="text-muted-foreground text-xs font-medium tracking-widest uppercase">
Website refresh
</p>
<span className="bg-primary/10 text-primary inline-flex items-center gap-1 rounded-full px-2 py-1 text-xs font-medium">
<Check aria-hidden="true" className="size-3" /> On track
</span>
</div>
<div>
<h3 className="text-foreground text-2xl font-semibold tracking-tight sm:text-3xl">
Good work.
<br />
Coming together.
</h3>
<p className="text-muted-foreground mt-2 max-w-sm text-sm leading-6">
One final review before the next chapter goes live.
</p>
</div>
<div>
<div className="flex flex-wrap items-baseline gap-2">
<span className="text-foreground text-5xl font-semibold tracking-tighter tabular-nums">
24
<span className="text-muted-foreground text-xl font-normal">
{" "}
/ 32
</span>
</span>
<span className="text-muted-foreground text-xs">tasks complete</span>
</div>
<progress
aria-label="Launch progress"
className="[&::-webkit-progress-bar]:bg-muted [&::-webkit-progress-value]:bg-primary [&::-moz-progress-bar]:bg-primary mt-3 h-1.5 w-full overflow-hidden rounded-full"
max={32}
value={24}
/>
</div>
<div className="border-border mt-auto flex flex-wrap items-center justify-between gap-3 border-t pt-4">
<div>
<p className="text-muted-foreground text-xs">Next milestone</p>
<p className="text-foreground mt-1 text-sm font-medium">
Design sign-off · 26 Sep
</p>
</div>
<Button
aria-pressed={following}
leftIcon={<Bell aria-hidden="true" />}
onClick={() => setFollowing(!following)}
size="sm"
variant={following ? "soft" : "outline"}
>
{following ? "Following" : "Follow project"}
</Button>
</div>
</div>
);
}
function ActivityPanel() {
const [filter, setFilter] = useState("All updates");
const id = useId();
const entries = activity.filter(
(entry) => filter === "All updates" || entry.type === filter,
);
return (
<div className={panelClass}>
<div>
<p className="text-primary text-xs font-medium tracking-widest uppercase">
The latest
</p>
<h3 className="text-foreground mt-2 text-2xl font-semibold tracking-tight">
Small steps. Real progress.
</h3>
</div>
<div className="flex flex-wrap items-center justify-between gap-2">
<label className="text-muted-foreground text-sm" htmlFor={id}>
Show activity
</label>
<select
className="border-border bg-background text-foreground focus-visible:outline-ring min-h-9 rounded-md border px-3 text-sm focus-visible:outline-2 focus-visible:outline-offset-2"
id={id}
onChange={(event) => setFilter(event.target.value)}
value={filter}
>
<option>All updates</option>
<option>Design</option>
<option>Engineering</option>
</select>
</div>
<ul className="divide-border divide-y" role="list">
{entries.map((entry) => (
<li className="flex items-start gap-3 py-3" key={entry.title}>
<span className="bg-primary/10 text-primary mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-full">
<Check aria-hidden="true" className="size-3.5" />
</span>
<div>
<p className="text-foreground text-sm font-medium">
{entry.title}
</p>
<p className="text-muted-foreground mt-1 text-xs">
{entry.person} · {entry.time}
</p>
</div>
</li>
))}
</ul>
<p className="text-muted-foreground mt-auto text-xs" role="status">
{entries.length} {entries.length === 1 ? "update" : "updates"} ·{" "}
{filter}
</p>
</div>
);
}
function FilesPanel() {
const [selected, setSelected] = useState<string>();
return (
<div className={panelClass}>
<div>
<p className="text-primary text-xs font-medium tracking-widest uppercase">
Shared knowledge
</p>
<h3 className="text-foreground mt-2 text-2xl font-semibold tracking-tight">
Everything in its place.
</h3>
</div>
<ul className="space-y-2" role="list">
{files.map((file) => (
<li key={file.title}>
<button
aria-expanded={selected === file.title}
className="border-border bg-background text-foreground hover:bg-muted focus-visible:outline-ring flex w-full items-center gap-3 rounded-lg border p-3 text-start focus-visible:outline-2 focus-visible:outline-offset-2"
onClick={() =>
setSelected(selected === file.title ? undefined : file.title)
}
type="button"
>
<FileText
aria-hidden="true"
className="text-primary size-5 shrink-0"
/>
<span className="min-w-0 flex-1">
<span className="block text-sm font-medium">{file.title}</span>
<span className="text-muted-foreground mt-1 block text-xs">
{file.meta}
</span>
</span>
<ArrowUpRight aria-hidden="true" className="size-4 shrink-0" />
</button>
{selected === file.title ? (
<p className="border-border text-muted-foreground mt-2 border-s-2 ps-3 text-sm leading-6">
{file.body}
</p>
) : null}
</li>
))}
</ul>
<p className="text-muted-foreground mt-auto text-xs">
Select a document to read its summary.
</p>
</div>
);
}
function TeamPanel() {
const [owner, setOwner] = useState("Maya Chen");
return (
<div className={panelClass}>
<div>
<p className="text-primary text-xs font-medium tracking-widest uppercase">
Better together
</p>
<h3 className="text-foreground mt-2 text-2xl font-semibold tracking-tight">
A small, focused team.
</h3>
<p className="text-muted-foreground mt-2 text-sm">
Choose who leads the next review.
</p>
</div>
<ul className="divide-border divide-y" role="list">
{team.map((person) => (
<li
className="flex flex-wrap items-center gap-3 py-3"
key={person.name}
>
<span
aria-hidden="true"
className="bg-muted text-muted-foreground flex size-9 shrink-0 items-center justify-center rounded-full text-xs font-semibold"
>
{person.initials}
</span>
<div className="min-w-0 flex-1">
<p className="text-foreground text-sm font-medium">
{person.name}
</p>
<p className="text-muted-foreground text-xs">{person.role}</p>
</div>
<Button
aria-label={`Assign ${person.name} as review lead`}
aria-pressed={owner === person.name}
onClick={() => setOwner(person.name)}
size="sm"
variant={owner === person.name ? "soft" : "ghost"}
>
{owner === person.name ? "Lead" : "Assign"}
</Button>
</li>
))}
</ul>
<p className="text-muted-foreground mt-auto text-xs" role="status">
{owner} leads the next review.
</p>
</div>
);
}
const sections = [
{
value: "overview",
label: "Overview",
summary: "75% complete",
badge: "75%",
icon: LayoutDashboard,
panel: OverviewPanel,
},
{
value: "activity",
label: "Activity",
summary: "3 recent updates",
badge: "03",
icon: Activity,
panel: ActivityPanel,
},
{
value: "files",
label: "Files",
summary: "2 documents",
badge: "02",
icon: FileText,
panel: FilesPanel,
},
{
value: "team",
label: "Team",
summary: "3 members",
badge: "03",
icon: Users,
panel: TeamPanel,
},
];
export function HorizontalAccordionBasic() {
return (
<HorizontalAccordion
aria-label="Website refresh project"
bladeWidth={56}
className="border-border bg-background rounded-xl border shadow-sm"
collapsible={false}
compactBreakpoint={600}
defaultValue="overview"
height={440}
>
{sections.map(
({ value, label, summary, badge, icon: Icon, panel: Panel }) => (
<HorizontalAccordion.Item key={value} value={value}>
<HorizontalAccordion.Blade
aria-label={`${label}, ${summary}`}
className="bg-muted/50 data-[layout=compact]:px-1"
>
<HorizontalAccordion.BladeIcon>
<Icon aria-hidden="true" className="size-4" />
</HorizontalAccordion.BladeIcon>
<HorizontalAccordion.BladeLabel className="data-[layout=compact]:text-xs">
{label}
</HorizontalAccordion.BladeLabel>
<span
aria-hidden="true"
className="order-3 rounded-full border border-current/20 px-1.5 py-0.5 text-[0.625rem] font-medium tabular-nums"
>
{badge}
</span>
</HorizontalAccordion.Blade>
<HorizontalAccordion.Panel className="overflow-y-auto">
<Panel />
</HorizontalAccordion.Panel>
</HorizontalAccordion.Item>
),
)}
</HorizontalAccordion>
);
}Controlled checklist
Move between delivery phases with external controls. Completed steps stay checked, and collapsing the checklist leaves a compact tray with a clear next step.
Launch checklist
Make every detail count.
1 of 3 steps complete
Show sourceexamples/horizontal-accordion/controlled.tsx
"use client";
/* eslint-disable jsx-a11y/no-redundant-roles -- Safari needs explicit list roles when list markers are removed. */
import { useState } from "react";
import { Check, Hammer, ListChecks, Rocket } from "lucide-react";
import {
Button,
HorizontalAccordion,
type HorizontalAccordionValue,
} from "@dethink/components";
const phases = [
{
value: "plan",
label: "Plan",
icon: ListChecks,
title: "Give the work a clear shape.",
tasks: [
"Agree the launch scope",
"Assign review owners",
"Set the release date",
],
},
{
value: "build",
label: "Build",
icon: Hammer,
title: "Make every detail count.",
tasks: [
"Complete the checkout flow",
"Review keyboard navigation",
"Verify mobile layouts",
],
},
{
value: "ship",
label: "Ship",
icon: Rocket,
title: "Ready for the next chapter.",
tasks: [
"Approve the release candidate",
"Publish the release notes",
"Check the first orders",
],
},
];
export function HorizontalAccordionControlled() {
const [value, setValue] = useState<HorizontalAccordionValue>("build");
const [done, setDone] = useState<string[]>([
"Agree the launch scope",
"Complete the checkout flow",
]);
return (
<div className="space-y-4">
<div className="flex flex-wrap items-center gap-2">
{phases.map((phase) => (
<Button
aria-pressed={value === phase.value}
key={phase.value}
onClick={() => setValue(phase.value)}
size="sm"
variant={value === phase.value ? "soft" : "outline"}
>
{phase.label}
</Button>
))}
<Button onClick={() => setValue(undefined)} size="sm" variant="ghost">
Collapse all
</Button>
<span className="text-muted-foreground ms-auto text-xs tabular-nums">
{done.length} / 9 complete
</span>
</div>
{value === undefined ? (
<p className="text-muted-foreground text-sm" role="status">
Choose a phase to continue your launch checklist.
</p>
) : null}
<HorizontalAccordion
aria-label="Delivery phases"
className="border-border bg-background rounded-xl border shadow-sm"
compactBreakpoint={560}
height={value === undefined ? 96 : 340}
onValueChange={setValue}
value={value}
>
{phases.map(
({ value: phaseValue, label, icon: Icon, title, tasks }) => (
<HorizontalAccordion.Item key={phaseValue} value={phaseValue}>
<HorizontalAccordion.Blade>
<HorizontalAccordion.BladeIcon>
<Icon aria-hidden="true" className="size-4" />
</HorizontalAccordion.BladeIcon>
<HorizontalAccordion.BladeLabel>
{label}
</HorizontalAccordion.BladeLabel>
</HorizontalAccordion.Blade>
<HorizontalAccordion.Panel className="overflow-y-auto">
<div className="flex h-full flex-col gap-5 p-6">
<div>
<p className="text-primary text-xs font-medium tracking-widest uppercase">
Launch checklist
</p>
<h3 className="text-foreground mt-2 text-2xl font-semibold tracking-tight">
{title}
</h3>
</div>
<ul className="space-y-3" role="list">
{tasks.map((task) => (
<li key={task}>
<label className="text-foreground flex min-h-8 cursor-pointer items-center gap-3 text-sm">
<input
checked={done.includes(task)}
className="accent-primary size-4 shrink-0"
onChange={(event) =>
setDone((current) =>
event.target.checked
? [...current, task]
: current.filter((item) => item !== task),
)
}
type="checkbox"
/>
<span
className={
done.includes(task)
? "text-muted-foreground line-through"
: ""
}
>
{task}
</span>
</label>
</li>
))}
</ul>
<p className="text-muted-foreground mt-auto flex items-center gap-1.5 text-xs">
<Check aria-hidden="true" className="size-3.5" />
{tasks.filter((task) => done.includes(task)).length} of 3
steps complete
</p>
</div>
</HorizontalAccordion.Panel>
</HorizontalAccordion.Item>
),
)}
</HorizontalAccordion>
</div>
);
}Blade content
Blades compose freely: rotated or vertical labels, icon-only blades with an aria-label, and icon-plus-label mixes with iconPosition.
Rotated label with icon
The default label orientation rotates text to read bottom to top, with a supporting icon at the start of the blade. See the API.
Show sourceexamples/horizontal-accordion/blade-content.tsx
"use client";
import { HorizontalAccordion, Link } from "@dethink/components";
function TypeIcon() {
return (
<svg
aria-hidden="true"
fill="none"
height="18"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
viewBox="0 0 24 24"
width="18"
>
<path d="M5 7V5h14v2M12 5v14m-3 0h6" />
</svg>
);
}
function CompassIcon() {
return (
<svg
aria-hidden="true"
fill="none"
height="18"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
viewBox="0 0 24 24"
width="18"
>
<circle cx="12" cy="12" r="9" />
<path d="m14.8 9.2-1.9 4.7-3.7 1.9 1.9-4.7 3.7-1.9Z" />
</svg>
);
}
function SparklesIcon() {
return (
<svg
aria-hidden="true"
fill="none"
height="18"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
viewBox="0 0 24 24"
width="18"
>
<path d="M12 4l1.6 4.4L18 10l-4.4 1.6L12 16l-1.6-4.4L6 10l4.4-1.6L12 4Z" />
<path d="M19 15l.8 2.2L22 18l-2.2.8L19 21l-.8-2.2L16 18l2.2-.8L19 15Z" />
</svg>
);
}
function Panel({
title,
body,
cta,
}: {
title: string;
body: string;
cta: string;
}) {
return (
<div className="bg-background flex h-full flex-col justify-center gap-3 p-8">
<h3 className="text-foreground text-lg font-semibold">{title}</h3>
<p className="text-muted-foreground max-w-md text-sm leading-6">
{body}{" "}
<Link href="#props-heading" variant="muted" underline="always">
See the API
</Link>
.
</p>
<div>
<Link href="#props-heading" underline="always">
{cta}
</Link>
</div>
</div>
);
}
export function HorizontalAccordionBladeContent() {
return (
<HorizontalAccordion
aria-label="Blade content mixes"
className="border-border rounded-lg border"
compactBreakpoint={480}
collapsible={false}
defaultValue="rotated"
height={320}
>
<HorizontalAccordion.Item value="rotated">
<HorizontalAccordion.Blade>
<HorizontalAccordion.BladeIcon>
<TypeIcon />
</HorizontalAccordion.BladeIcon>
<HorizontalAccordion.BladeLabel>
Rotated
</HorizontalAccordion.BladeLabel>
</HorizontalAccordion.Blade>
<HorizontalAccordion.Panel>
<Panel
body="The default label orientation rotates text to read bottom to top, with a supporting icon at the start of the blade."
cta="View label props"
title="Rotated label with icon"
/>
</HorizontalAccordion.Panel>
</HorizontalAccordion.Item>
<HorizontalAccordion.Item value="vertical">
<HorizontalAccordion.Blade>
<HorizontalAccordion.BladeLabel
direction="top-to-bottom"
orientation="vertical"
>
Vertical
</HorizontalAccordion.BladeLabel>
</HorizontalAccordion.Blade>
<HorizontalAccordion.Panel>
<Panel
body="True vertical writing mode reading top to bottom keeps glyphs upright, which suits short wayfinding labels."
cta="View orientation props"
title="Vertical writing mode"
/>
</HorizontalAccordion.Panel>
</HorizontalAccordion.Item>
<HorizontalAccordion.Item value="icon-only">
<HorizontalAccordion.Blade aria-label="Highlights">
<HorizontalAccordion.BladeIcon>
<SparklesIcon />
</HorizontalAccordion.BladeIcon>
</HorizontalAccordion.Blade>
<HorizontalAccordion.Panel>
<Panel
body="Icon-only blades stay ultra compact and take an aria-label so assistive technology still announces a meaningful name."
cta="View blade props"
title="Icon-only blade"
/>
</HorizontalAccordion.Panel>
</HorizontalAccordion.Item>
<HorizontalAccordion.Item value="mixed">
<HorizontalAccordion.Blade iconPosition="end">
<HorizontalAccordion.BladeIcon>
<CompassIcon />
</HorizontalAccordion.BladeIcon>
<HorizontalAccordion.BladeLabel>
Icon end
</HorizontalAccordion.BladeLabel>
</HorizontalAccordion.Blade>
<HorizontalAccordion.Panel>
<Panel
body="Icon plus label with the icon anchored to the end of the blade axis via iconPosition."
cta="View icon position props"
title="Icon anchored to the end"
/>
</HorizontalAccordion.Panel>
</HorizontalAccordion.Item>
</HorizontalAccordion>
);
}Compact layout
A working inbox in a narrow container. Archive and restore messages; blade counts stay in sync. Below compactBreakpoint the panel moves above a touch-friendly tray, regardless of viewport size.
Website refresh
Inbox
Final screens are ready
Maya Chen
A note on the launch brief
Alex Morgan
Last week's review notes
Sam Rivera
Archive a message, then restore it from Archive.
Show sourceexamples/horizontal-accordion/compact.tsx
"use client";
/* eslint-disable jsx-a11y/no-redundant-roles -- Safari needs explicit list roles when list markers are removed. */
import { useState } from "react";
import { Archive, Check, Inbox, Sun } from "lucide-react";
import { Button, HorizontalAccordion } from "@dethink/components";
const messages = [
{ id: 1, title: "Final screens are ready", sender: "Maya Chen", today: true },
{
id: 2,
title: "A note on the launch brief",
sender: "Alex Morgan",
today: true,
},
{
id: 3,
title: "Last week's review notes",
sender: "Sam Rivera",
today: false,
},
];
const folders = [
{ value: "inbox", label: "Inbox", icon: Inbox },
{ value: "today", label: "Today", icon: Sun },
{ value: "archive", label: "Archive", icon: Archive },
];
export function HorizontalAccordionCompact() {
const [archived, setArchived] = useState<number[]>([]);
const [notice, setNotice] = useState("");
const inFolder = (folder: string) =>
messages.filter((message) =>
folder === "archive"
? archived.includes(message.id)
: !archived.includes(message.id) &&
(folder !== "today" || message.today),
);
return (
<div className="mx-auto max-w-sm">
<HorizontalAccordion
aria-label="Project inbox"
className="border-border bg-background outline-border rounded-xl border shadow-md outline-1"
collapsible={false}
defaultValue="inbox"
height={380}
>
{folders.map(({ value, label, icon: Icon }) => (
<HorizontalAccordion.Item key={value} value={value}>
<HorizontalAccordion.Blade
aria-label={`${label}, ${inFolder(value).length} ${inFolder(value).length === 1 ? "message" : "messages"}`}
>
<HorizontalAccordion.BladeIcon>
<Icon aria-hidden="true" className="size-4" />
</HorizontalAccordion.BladeIcon>
<HorizontalAccordion.BladeLabel className="gap-1.5">
{label}
<span aria-hidden="true" className="text-xs tabular-nums">
{inFolder(value).length}
</span>
</HorizontalAccordion.BladeLabel>
</HorizontalAccordion.Blade>
<HorizontalAccordion.Panel className="overflow-y-auto">
<div className="flex h-full flex-col p-5">
<p className="text-primary text-xs font-medium tracking-widest uppercase">
Website refresh
</p>
<h3 className="text-foreground mt-2 text-2xl font-semibold tracking-tight">
{label}
</h3>
<ul className="divide-border mt-4 divide-y" role="list">
{inFolder(value).map((message) => (
<li
className="flex flex-wrap items-center justify-between gap-2 py-3"
key={message.id}
>
<div>
<p className="text-foreground text-sm font-medium">
{message.title}
</p>
<p className="text-muted-foreground mt-1 text-xs">
{message.sender}
</p>
</div>
<Button
aria-label={`${value === "archive" ? "Restore" : "Archive"}: ${message.title}`}
onClick={(event) => {
// The clicked row will disappear. Keep focus in this
// inbox instead of letting it fall back to the page.
event.currentTarget
.closest('[data-slot="horizontal-accordion"]')
?.querySelector<HTMLButtonElement>(
'[data-slot="horizontal-accordion-blade"][data-state="active"]',
)
?.focus();
setArchived((current) =>
value === "archive"
? current.filter((id) => id !== message.id)
: [...current, message.id],
);
setNotice(
`${message.title} ${value === "archive" ? "restored to inbox" : "archived"}.`,
);
}}
size="sm"
variant="ghost"
>
{value === "archive" ? "Restore" : "Archive"}
</Button>
</li>
))}
</ul>
{inFolder(value).length === 0 ? (
<div className="text-muted-foreground my-auto py-8 text-center">
<Check aria-hidden="true" className="mx-auto mb-3 size-6" />
<p className="text-sm">
{value === "archive"
? "No archived messages yet."
: "You're all caught up."}
</p>
</div>
) : null}
</div>
</HorizontalAccordion.Panel>
</HorizontalAccordion.Item>
))}
</HorizontalAccordion>
<p
className="text-muted-foreground mt-3 min-h-8 text-xs leading-5"
role="status"
>
{notice || "Archive a message, then restore it from Archive."}
</p>
</div>
);
}Use a horizontal accordion for a small set of related sections when people benefit from seeing every destination at once.
- Start persistent workspaces with defaultValue and collapsible=false. Keep collapse optional for supplementary information and give the collapsed state an intentional height or instruction.
- Keep blade labels short. Compose counts or status with ordinary elements and include their meaning in the blade's accessible name. Avoid nested buttons or links inside a blade.
- Choose compactBreakpoint for the space your content needs, especially with four or more blades. For long content, give Panel overflow-y-auto and check keyboard access to everything inside.
- Arrow keys move focus; Enter or Space opens the focused section. Home and End reach the first and last enabled blades. Inactive panels preserve state by default and are inert during exit.
- Use Tabs for frequent comparisons with short labels, or Accordion for long reading sections. Test translated labels, RTL, reduced motion, zoom, and your target screen readers.
HorizontalAccordion renders a div and accepts all native div attributes. Blades render real buttons with aria-expanded and aria-controls; panels are labelled regions hidden when inactive. Every part accepts className and exposes data-slot, data-state, and data-layout attributes for styling.
| Prop | What it does | Default |
|---|---|---|
valuestring | undefined | Controlled active item value. undefined means no active item. Pair with onValueChange. | Not set |
defaultValuestring | Initial active item for uncontrolled usage. Omit to start fully collapsed. | Not set |
onValueChange(value: string | undefined) => void | Fires with the next value on activation, or undefined when the active item collapses. | Not set |
collapsibleboolean | Whether activating the active blade collapses it. With false the accordion keeps one section open once activated. | true |
activationMode"manual" | "automatic" | Manual activates blades with click, Enter, or Space. Automatic also activates on keyboard focus. | "manual" |
bladeWidthnumber | Collapsed blade width in pixels, exposed as --horizontal-accordion-blade-width. | 72 |
heightnumber | Band height in pixels, exposed as --horizontal-accordion-height. Compact layout treats it as a minimum. | 420 |
compactBreakpointnumber | Container width in pixels at or below which the compact layout activates. Measured with a ResizeObserver, not the viewport. | 640 |
animation{ duration?: number; easing?: string; content?: boolean } | Tunes the expansion and Motion choreography globally. content false disables panel content animation. | { duration: 260, easing: "cubic-bezier(0.2, 0, 0, 1)", content: true } |
unmountInactivePanelsboolean | Unmounts inactive panel children after their exit animation instead of keeping them mounted and hidden. | false |
| Prop | What it does | Default |
|---|---|---|
Item valuestring | Required stable identity for each item. Duplicate values throw in development. | Not set |
Blade iconPosition"start" | "end" | "top" | "bottom" | Position of BladeIcon along the blade axis. top and bottom are aliases for start and end. | "start" |
BladeLabel orientation"rotated" | "vertical" | Rotated turns horizontal text; vertical uses a true vertical writing mode. | "rotated" |
BladeLabel direction"bottom-to-top" | "top-to-bottom" | Reading direction of the blade label text. | "bottom-to-top" |