ComponentsPanel
Accordion
A stack of headings, each showing or hiding its own panel of content.
Import
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "@booleanpress/ui/accordion"Usage
An accordion groups related sections under headings, so a long page of settings or answers can be scanned. For one region on its own, use a collapsible.
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@booleanpress/ui/accordion"
export function DeliveryFaq() {
return (
<Accordion type="single" collapsible>
<AccordionItem value="log">
<AccordionTrigger>What does the delivery log record?</AccordionTrigger>
<AccordionContent>Every email the site sends, kept for 30 days.</AccordionContent>
</AccordionItem>
</Accordion>
)
}type="single" opens one panel at a time; add collapsible so the open one can be closed again. type="multiple" lets any number stay open. It is uncontrolled with defaultValue, or controlled with value and onValueChange (a string for single, an array for multiple). disabled on the Accordion disables every panel; on an AccordionItem it disables that one.
The chevron turns when a panel opens. indicator on AccordionTrigger replaces it with your own icon; the trigger is the Tailwind group accordion-trigger, so an icon can follow the state with group-data-[state=open]/accordion-trigger:. The trigger takes any content, such as an icon and a badge beside the title. The content's height animates from Radix's measured --radix-accordion-content-height.
Examples
Basic
One panel open at a time; collapsible lets the open one close.
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@booleanpress/ui/accordion"
export default function AccordionBasic() {
return (
<Accordion type="single" collapsible className="w-full max-w-md">
<AccordionItem value="log">
<AccordionTrigger>What does the delivery log record?</AccordionTrigger>
<AccordionContent>
Every email the site sends: the recipient, the subject, the status and the mail server's reply. Entries are
kept for 30 days.
</AccordionContent>
</AccordionItem>
<AccordionItem value="resend">
<AccordionTrigger>Can I resend a failed email?</AccordionTrigger>
<AccordionContent>
Yes. Open the entry in the log and choose Resend. The new attempt gets its own entry.
</AccordionContent>
</AccordionItem>
<AccordionItem value="providers">
<AccordionTrigger>Which mail providers can I connect?</AccordionTrigger>
<AccordionContent>
Any SMTP server, and Amazon SES, Mailgun, Postmark and SendGrid through their APIs.
</AccordionContent>
</AccordionItem>
</Accordion>
)
}Multiple
type="multiple" keeps several panels open, here with the first open from the start.
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@booleanpress/ui/accordion"
export default function AccordionMultiple() {
return (
<Accordion type="multiple" defaultValue={["sending"]} className="w-full max-w-md">
<AccordionItem value="sending">
<AccordionTrigger>Sending</AccordionTrigger>
<AccordionContent>
Mail goes out through the primary connection. When it fails, the backup connection takes over.
</AccordionContent>
</AccordionItem>
<AccordionItem value="tracking">
<AccordionTrigger>Tracking</AccordionTrigger>
<AccordionContent>
Opens and clicks are counted per message. Tracking pixels are off for transactional email.
</AccordionContent>
</AccordionItem>
<AccordionItem value="retention">
<AccordionTrigger>Retention</AccordionTrigger>
<AccordionContent>
Log entries older than 30 days are removed every night at 02:00.
</AccordionContent>
</AccordionItem>
</Accordion>
)
}Controlled
value and onValueChange keep the open panel outside, so buttons can open and close panels.
import { useState } from "react"
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@booleanpress/ui/accordion"
import { Button } from "@booleanpress/ui/button"
const PANELS = [
{ value: "smtp", title: "SMTP connection", body: "Host smtp.example.com, port 587, STARTTLS, signed in as mailer@example.com." },
{ value: "sender", title: "Sender", body: "Mail is sent from Acme Support <support@example.com>, replies go to the help desk." },
{ value: "alerts", title: "Failure alerts", body: "After three failed sends in ten minutes, an alert goes to ops@example.com." },
]
export default function AccordionControlled() {
const [open, setOpen] = useState("smtp")
return (
<div className="flex w-full max-w-md flex-col gap-4">
<div className="flex flex-wrap justify-center gap-2">
{PANELS.map((panel, index) => (
<Button
key={panel.value}
variant={open === panel.value ? "default" : "secondary"}
onClick={() => setOpen(panel.value)}
>
Panel {index + 1}
</Button>
))}
<Button variant="destructive" onClick={() => setOpen("")}>
Close all
</Button>
</div>
<Accordion type="single" collapsible value={open} onValueChange={setOpen}>
{PANELS.map((panel) => (
<AccordionItem key={panel.value} value={panel.value}>
<AccordionTrigger>{panel.title}</AccordionTrigger>
<AccordionContent>{panel.body}</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
)
}Custom indicator
indicator replaces the chevron with a plus that becomes a minus when the panel opens.
import { MinusIcon, PlusIcon } from "lucide-react"
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@booleanpress/ui/accordion"
const QUESTIONS = [
{ value: "keys", title: "Where do I find my API key?", body: "Under Settings, then API keys. A key is shown once, when you create it." },
{ value: "rotate", title: "How do I rotate a key?", body: "Create a new key, move your sites to it, then revoke the old one." },
{ value: "scopes", title: "What can a key do?", body: "Each key has scopes: send, read logs, or manage connections." },
]
// The trigger is the Tailwind group `accordion-trigger`: each icon shows in one state.
const indicator = (
<>
<PlusIcon className="size-3.5 group-data-[state=open]/accordion-trigger:hidden" />
<MinusIcon className="hidden size-3.5 group-data-[state=open]/accordion-trigger:block" />
</>
)
export default function AccordionCustomIndicator() {
return (
<Accordion type="single" collapsible className="w-full max-w-md">
{QUESTIONS.map((question) => (
<AccordionItem key={question.value} value={question.value}>
<AccordionTrigger indicator={indicator}>{question.title}</AccordionTrigger>
<AccordionContent>{question.body}</AccordionContent>
</AccordionItem>
))}
</Accordion>
)
}With content template
An icon and a badge in each header, inside a bordered box.
import { CreditCardIcon, LockIcon, MailIcon } from "lucide-react"
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@booleanpress/ui/accordion"
import { Badge } from "@booleanpress/ui/badge"
const SECTIONS = [
{ value: "mail", icon: MailIcon, title: "Mail delivery", badge: { label: "Healthy", variant: "success" }, body: "98.6% of 12,480 messages delivered in the last 7 days." },
{ value: "security", icon: LockIcon, title: "Security", badge: { label: "2 warnings", variant: "warning" }, body: "Two API keys have not been used for 90 days. Revoke them if they are no longer needed." },
{ value: "billing", icon: CreditCardIcon, title: "Billing", badge: { label: "Pro", variant: "secondary" }, body: "The Pro plan renews on 1 November 2026." },
] as const
export default function AccordionTemplate() {
return (
<Accordion type="single" collapsible className="w-full max-w-md rounded-md border [&>*:last-child]:border-b-0">
{SECTIONS.map(({ value, icon: Icon, title, badge, body }) => (
<AccordionItem key={value} value={value}>
<AccordionTrigger>
<span className="flex flex-1 items-center gap-2">
<Icon className="text-muted-foreground" />
{title}
<Badge variant={badge.variant} className="ms-auto">
{badge.label}
</Badge>
</span>
</AccordionTrigger>
<AccordionContent>{body}</AccordionContent>
</AccordionItem>
))}
</Accordion>
)
}Disabled
disabled on the accordion disables every panel; on one item, only that one.
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@booleanpress/ui/accordion"
export default function AccordionDisabled() {
return (
<div className="flex w-full max-w-md flex-col gap-8">
<Accordion type="single" collapsible disabled>
<AccordionItem value="reset">
<AccordionTrigger>How do I reset my password?</AccordionTrigger>
<AccordionContent>Choose Forgot password on the sign-in page.</AccordionContent>
</AccordionItem>
<AccordionItem value="team">
<AccordionTrigger>Can I invite my team?</AccordionTrigger>
<AccordionContent>Yes, from Settings, then Members.</AccordionContent>
</AccordionItem>
</Accordion>
<Accordion type="single" collapsible>
<AccordionItem value="limit">
<AccordionTrigger>What happens when I reach the sending limit?</AccordionTrigger>
<AccordionContent>Mail waits in the queue and goes out when the limit resets at midnight.</AccordionContent>
</AccordionItem>
<AccordionItem value="app" disabled>
<AccordionTrigger>Is there a mobile app?</AccordionTrigger>
<AccordionContent>Not yet.</AccordionContent>
</AccordionItem>
</Accordion>
</div>
)
}Accessibility
- Semantics
- Each header is an
h3holding abuttonwitharia-expandedandaria-controls; each panel is aregionnamed by its trigger (aria-labelledby). A disabled trigger hasdisabledandaria-disabled; in asingleaccordion that is notcollapsible, the open trigger isaria-disabledtoo, since it cannot close. - Labels
- Each trigger is named by its text, and names its panel. Keep the text the same when the panel opens: the state is announced from
aria-expanded. - Focus
- Every trigger is a tab stop, in order; the panels' own controls follow their trigger in the tab order. Focus is a 1px
--ringoutline drawn just inside the header. - Known limits
- Closed panels are removed from the page, so find-in-page and screen readers do not see their content.
- The heading level is fixed at
h3. On a page whose outline needs another level, place the accordion where anh3belongs. - The indicator is decoration (
aria-hidden); the state is inaria-expanded.
Keyboard
| Key | Behaviour |
|---|---|
| EnterorSpace | Opens or closes the panel of the focused trigger. |
| Tab | Moves to the next trigger, or into the open panel's controls. |
| โ | Moves to the next trigger, wrapping at the end. |
| โ | Moves to the previous trigger, wrapping at the start. |
| HomeorEnd | Moves to the first or last trigger. |
API
Accordion
Renders Radix Accordion.Root and passes it every other prop.
| Prop | Type | Default | Description |
|---|---|---|---|
typerequired | "single" | "multiple" | single opens one panel at a time; multiple lets several stay open. Required. | |
asChild | boolean | ||
collapsible | boolean | false | Whether an accordion item can be collapsed after it has been opened. |
defaultValue | string | string[] | The value of the item whose content is expanded when the accordion is initially rendered. Use
defaultValue if you do not need to control the state of an accordion.
The value of the items whose contents are expanded when the accordion is initially rendered. Use
defaultValue if you do not need to control the state of an accordion. | |
dir | "ltr" | "rtl" | The language read direction. | |
disabled | boolean | Whether or not an accordion is disabled from user interaction. | |
onValueChange | ((value: string) => void) | ((value: string[]) => void) | The callback that fires when the state of the accordion changes. | |
orientation | "horizontal" | "vertical" | vertical | The layout in which the Accordion operates. |
value | string | string[] | The controlled stateful value of the accordion item whose content is expanded. The controlled stateful value of the accordion items whose contents are expanded. |
AccordionItem
Renders Radix Accordion.Item and passes it every other prop.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | ||
disabled | boolean | Whether or not an accordion is disabled from user interaction. | |
value | string | string[] | The controlled stateful value of the accordion item whose content is expanded. The controlled stateful value of the accordion items whose contents are expanded. |
AccordionTrigger
Renders Radix Accordion.Trigger and passes it every other prop.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | Render the child element instead, with the trigger's behaviour and classes merged onto it. | |
indicator | ReactNode | Replaces the chevron, which turns when the panel opens. The trigger is the Tailwind group accordion-trigger. |
AccordionContent
Renders Radix Accordion.Content and passes it every other prop.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | ||
forceMount | true | Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. |
Every part takes className, merged with its defaults by cn(), and ref, which reaches the element it renders.
Data attributes: data-slot="accordion" (Accordion), data-slot="accordion-item" (AccordionItem), data-slot="accordion-header" (AccordionTrigger), data-slot="accordion-content" (AccordionContent).
Theming
Headers and panels sit on --card. A closed header is --muted-foreground, an open or hovered one --foreground; the rule between panels is --border; focus is --ring. A disabled panel is drawn at 60% opacity.
The tokens its classes read. Change their values in your theme, and it follows.
| Token | Used for |
|---|---|
--card | background |
--foreground | text |
--muted-foreground | text |
--ring | outline |