Accordion
A stack of items that open one at a time, or several at once
Accordion
A stack of items that open one at a time, or several at once.
Installation
import { Accordion, AccordionItem, AccordionHeader, AccordionPanel } from '@geckoui/geckoui';Basic Usage
<Accordion defaultValue="shipping">
<AccordionItem value="shipping">
<AccordionHeader>Shipping</AccordionHeader>
<AccordionPanel>Ships in two to three working days.</AccordionPanel>
</AccordionItem>
<AccordionItem value="returns">
<AccordionHeader>Returns</AccordionHeader>
<AccordionPanel>Thirty days, unopened.</AccordionPanel>
</AccordionItem>
</Accordion>The header and the panel sit inside their item, so nothing needs pairing up by hand.
Props API
Accordion
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | string[] | - | What is open. An array when multiple is set. Leave it out to let the component hold the state |
defaultValue | string | string[] | - | What starts open when uncontrolled |
onChange | (value: string | string[]) => void | - | Called with what is open now. An empty string when the last item closes |
multiple | boolean | false | Let several items be open at once |
collapsible | boolean | true | Whether the open item can be closed, leaving nothing open |
variant | keyof AccordionVariantMap | 'plain' | plain, separated or contained |
size | keyof AccordionSizeMap | 'md' | sm, md or lg |
keepMounted | boolean | true | Keep closed panels in the DOM |
| ...rest | HTMLAttributes<HTMLDivElement> | - | Spread onto the container |
AccordionItem
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | required | What value and onChange report |
disabled | boolean | false | Cannot be opened, and skipped by the keyboard |
children | ReactNode | - | An AccordionHeader and an AccordionPanel |
| ...rest | HTMLAttributes<HTMLDivElement> | - | Spread onto the item |
AccordionHeader
| Prop | Type | Default | Description |
|---|---|---|---|
hideIcon | boolean | false | Drop the chevron |
icon | ReactNode | - | Replace the chevron with your own |
children | ReactNode | - | What the header shows |
| ...rest | HTMLAttributes<HTMLButtonElement> | - | Spread onto the button |
AccordionPanel
| Prop | Type | Default | Description |
|---|---|---|---|
keepMounted | boolean | - | Overrides keepMounted on Accordion for this panel |
children | ReactNode | - | The panel contents |
| ...rest | HTMLAttributes<HTMLDivElement> | - | Spread onto the panel |
Examples
Variants
plain is the default: dividers and nothing else, sitting flush with whatever contains it.
Reach for the others when the accordion needs a frame of its own.
plain
separated
contained
<Accordion defaultValue="shipping" variant="plain">
<AccordionItem value="shipping">
<AccordionHeader>Shipping</AccordionHeader>
<AccordionPanel>Ships in two to three working days.</AccordionPanel>
</AccordionItem>
</Accordion>Swap variant for "separated" (each item its own card) or "contained" (one box with
dividers).
Sizes
sm
md
lg
<Accordion defaultValue="shipping" size="lg">
<AccordionItem value="shipping">
<AccordionHeader>Shipping</AccordionHeader>
<AccordionPanel>Ships in two to three working days.</AccordionPanel>
</AccordionItem>
</Accordion>Several at once
<Accordion multiple defaultValue={['shipping', 'returns']}>
<AccordionItem value="shipping">
<AccordionHeader>Shipping</AccordionHeader>
<AccordionPanel>Ships in two to three working days.</AccordionPanel>
</AccordionItem>
<AccordionItem value="returns">
<AccordionHeader>Returns</AccordionHeader>
<AccordionPanel>Thirty days, unopened.</AccordionPanel>
</AccordionItem>
</Accordion>value, defaultValue and onChange all deal in arrays once multiple is set.
Always one open
By default, clicking the open item shuts it and leaves nothing open. Pass
collapsible={false} to keep one open at all times.
<Accordion defaultValue="shipping" collapsible={false}>
<AccordionItem value="shipping">
<AccordionHeader>Shipping</AccordionHeader>
<AccordionPanel>Ships in two to three working days.</AccordionPanel>
</AccordionItem>
</Accordion>Anything in a header
Whatever sits inside AccordionHeader is what it shows, so icons and badges need no props
of their own.
<AccordionItem value="shipping">
<AccordionHeader>
<TruckIcon /> Shipping <Badge color="info">free</Badge>
</AccordionHeader>
<AccordionPanel>Ships in two to three working days.</AccordionPanel>
</AccordionItem>Use hideIcon to drop the chevron, or icon to replace it:
<AccordionHeader icon={<PlusIcon />}>Shipping</AccordionHeader>
<AccordionHeader hideIcon>Shipping</AccordionHeader>Forms inside panels
Closed panels stay in the DOM, so a half filled form survives being shut. Type below, close the panel, and open it again.
Turn that off with keepMounted={false} when the panels are expensive to render. Closing
then happens without the animation, because there is nothing left to animate:
// lighter DOM, no closing animation
<Accordion defaultValue="shipping" keepMounted={false}>
<AccordionItem value="shipping">
<AccordionHeader>Shipping</AccordionHeader>
<AccordionPanel>Ships in two to three working days.</AccordionPanel>
</AccordionItem>
</Accordion>
// or one panel at a time
<Accordion defaultValue="shipping">
<AccordionItem value="shipping">
<AccordionHeader>Shipping</AccordionHeader>
<AccordionPanel keepMounted={false}>Ships in two to three working days.</AccordionPanel>
</AccordionItem>
</Accordion>Controlled
Open: "shipping"
import { useState } from 'react';
import type { AccordionValue } from '@geckoui/geckoui';
function Faq() {
const [open, setOpen] = useState<AccordionValue>('shipping');
return (
<>
<button onClick={() => setOpen('')}>Close all</button>
<Accordion value={open} onChange={setOpen}>
<AccordionItem value="shipping">
<AccordionHeader>Shipping</AccordionHeader>
<AccordionPanel>Ships in two to three working days.</AccordionPanel>
</AccordionItem>
</Accordion>
</>
);
}onChange reports an empty string when the last open item closes, so setOpen('') closes
everything.
Keyboard
| Key | Does |
|---|---|
| ↑ ↓ | Move focus between headers, wrapping at the ends |
| Home End | Move focus to the first or last header |
| Enter Space | Open or close the focused item |
| Tab | Move through the headers and whatever is inside an open panel |
Disabled items are skipped. The arrow keys only steer while a header has focus, so they behave normally inside a panel — a text field there keeps them.
Accessibility
- Each header is a
<button>witharia-expandedandaria-controlspointing at its panel - Each panel is a
regionlabelled by its header - A disabled item's header is a disabled button, and the arrow keys pass over it
- The open and close animation is dropped under
prefers-reduced-motion
Styling with CSS
.GeckoUIAccordion [data-variant] [data-size]
.GeckoUIAccordion__item [data-state="open|closed"] [data-disabled]
.GeckoUIAccordion__header [data-state] [data-disabled]
.GeckoUIAccordion__header__content
.GeckoUIAccordion__header__icon
.GeckoUIAccordion__panel [data-state]
.GeckoUIAccordion__panel__contentRetheming is a handful of variables:
.GeckoUIAccordion {
--gecko-accordion-radius: 1rem;
--gecko-accordion-gap: 0.75rem;
--gecko-accordion-padding-x: 1.25rem;
--gecko-accordion-padding-y: 1rem;
--gecko-accordion-font-size: 0.875rem;
--gecko-accordion-duration: 200ms;
}How the animation works
The panel is a CSS grid whose single row goes from 0fr to 1fr:
.GeckoUIAccordion__panel {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows var(--gecko-accordion-duration) ease;
}
.GeckoUIAccordion__panel[data-state="open"] {
grid-template-rows: 1fr;
}That animates to the content's real height with nothing measured in JavaScript. It is also why closed panels stay mounted by default — there is nothing to collapse otherwise.
Adding your own variants
AccordionVariantMap and AccordionSizeMap are open, so a new variant is a type
declaration and a CSS block:
declare module '@geckoui/geckoui' {
interface AccordionVariantMap {
ghost: unknown;
}
}.GeckoUIAccordion[data-variant="ghost"] .GeckoUIAccordion__header[data-state="open"] {
background: var(--color-surface-secondary);
}