Accordion
Stacked sections that expand and collapse — single or multiple open, five visual variants.
Named colors like surface, label and stroke that resolve differently in light and dark mode — components never reference raw hex values.
"use client";
import { Accordion, Box } from "astralis-ui";
export function AccordionDemo() {
return (
<Box w="full" maxW="md">
<Accordion type="single" defaultValue="tokens" collapsible>
<Accordion.Item value="tokens">
<Accordion.Trigger>What are semantic tokens?</Accordion.Trigger>
<Accordion.Content>
Named colors like surface, label and stroke that resolve differently
in light and dark mode — components never reference raw hex values.
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="build">
<Accordion.Trigger>Do I need a build step?</Accordion.Trigger>
<Accordion.Content>
No. The library ships precompiled CSS — import one stylesheet and
every component works.
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="theming">
<Accordion.Trigger>How does brand theming work?</Accordion.Trigger>
<Accordion.Content>
Pass a brand color to AstralisProvider and the full shade scale is
generated at runtime.
</Accordion.Content>
</Accordion.Item>
</Accordion>
</Box>
);
}Import#
import { Accordion } from "astralis-ui";
// Accordion.Item, Accordion.Trigger, Accordion.ContentAnatomy#
<Accordion type="single" collapsible>
<Accordion.Item value="unique-id">
<Accordion.Trigger>Heading</Accordion.Trigger>
<Accordion.Content>Body</Accordion.Content>
</Accordion.Item>
</Accordion>Variants#
enclosed
Content of the first section.
outline
Content of the first section.
separated
Content of the first section.
subtle
Content of the first section.
plain
Content of the first section.
"use client";
import { Accordion, Text, VStack } from "astralis-ui";
const variants = ["enclosed", "outline", "separated", "subtle", "plain"] as const;
export function AccordionVariants() {
return (
<VStack gap="6" alignItems="stretch" w="full" maxW="md">
{variants.map((variant) => (
<VStack key={variant} gap="2" alignItems="stretch">
<Text as="span" size="xs" weight="medium" color="muted">
{variant}
</Text>
<Accordion variant={variant} type="single" defaultValue="one" collapsible>
<Accordion.Item value="one">
<Accordion.Trigger>First section</Accordion.Trigger>
<Accordion.Content>Content of the first section.</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="two">
<Accordion.Trigger>Second section</Accordion.Trigger>
<Accordion.Content>Content of the second section.</Accordion.Content>
</Accordion.Item>
</Accordion>
</VStack>
))}
</VStack>
);
}Multiple open#
type="multiple" lets any number of items stay open; value/defaultValue
become arrays.
Several items can be open at once.
defaultValue takes an array in multiple mode.
"use client";
import { Accordion, Box } from "astralis-ui";
export function AccordionMultiple() {
return (
<Box w="full" maxW="md">
<Accordion type="multiple" defaultValue={["a", "b"]} variant="separated">
<Accordion.Item value="a">
<Accordion.Trigger>Open by default</Accordion.Trigger>
<Accordion.Content>Several items can be open at once.</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="b">
<Accordion.Trigger>Also open by default</Accordion.Trigger>
<Accordion.Content>defaultValue takes an array in multiple mode.</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="c">
<Accordion.Trigger>Closed until clicked</Accordion.Trigger>
<Accordion.Content>Opening this one closes nothing else.</Accordion.Content>
</Accordion.Item>
</Accordion>
</Box>
);
}Controlled#
Pass value + onValueChange to own the state — useful for "expand from
elsewhere" flows like deep links or wizards.
Ships worldwide within 3–5 days.
"use client";
import { useState } from "react";
import { Accordion, Button, ButtonGroup, VStack } from "astralis-ui";
export function AccordionControlled() {
const [open, setOpen] = useState<string | string[]>("shipping");
return (
<VStack gap="4" alignItems="stretch" w="full" maxW="md">
<ButtonGroup size="sm" variant="outline" colorScheme="gray">
<Button onClick={() => setOpen("shipping")}>Open shipping</Button>
<Button onClick={() => setOpen("returns")}>Open returns</Button>
<Button onClick={() => setOpen("")}>Close all</Button>
</ButtonGroup>
<Accordion type="single" collapsible value={open} onValueChange={setOpen}>
<Accordion.Item value="shipping">
<Accordion.Trigger>Shipping</Accordion.Trigger>
<Accordion.Content>Ships worldwide within 3–5 days.</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="returns">
<Accordion.Trigger>Returns</Accordion.Trigger>
<Accordion.Content>Free returns within 30 days.</Accordion.Content>
</Accordion.Item>
</Accordion>
</VStack>
);
}Custom indicator#
Swap the chevron, move it to the leading edge, recolor with colorScheme,
or disable individual items.
"use client";
import { Accordion, Icon, Box } from "astralis-ui";
import { Plus } from "lucide-react";
export function AccordionCustom() {
return (
<Box w="full" maxW="md">
<Accordion
variant="outline"
colorScheme="purple"
indicator={<Icon as={Plus} size="xs" />}
indicatorPosition="start"
type="single"
collapsible
>
<Accordion.Item value="one">
<Accordion.Trigger>Custom indicator, leading position</Accordion.Trigger>
<Accordion.Content>
The indicator rotates as the item opens; colorScheme drives the
focus ring and accents.
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="two" disabled>
<Accordion.Trigger>A disabled item</Accordion.Trigger>
<Accordion.Content>Unreachable.</Accordion.Content>
</Accordion.Item>
</Accordion>
</Box>
);
}Props#
| Prop | Type | Default | Description |
|---|---|---|---|
type | "single" | "multiple" | "single" | One open item at a time, or any number. |
variant | "enclosed" | "outline" | "separated" | "subtle" | "plain" | "enclosed" | Container styling family. |
size | "sm" | "md" | "lg" | "md" | Trigger and content density. |
colorScheme | "gray" | "brand" | … (all 11 schemes) | "gray" | Accent hue for focus rings and highlights. |
value · defaultValue | string | string[] | — | Controlled / initial open item(s) — an array in multiple mode. |
onValueChange | (value: string | string[]) => void | — | Fires with the next open value(s). |
collapsible | boolean | false | In single mode, allows closing the open item. |
disabled | boolean | false | Disables every item. |
keepMounted | boolean | false | Render content in the DOM before first open (SEO / heavy content). |
indicator · indicatorPosition · hideIndicator | ReactNode · "start" | "end" · boolean | chevron · "end" · false | Customize or hide the rotating open indicator. |
headingLevel | 1 – 6 | 3 | Heading level wrapping each trigger, for the document outline. |
Accordion.Item#
Accordion.Trigger and Accordion.Content take only className (Trigger
can also override indicator/hideIndicator per item).
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Required — identifies the item in value/defaultValue. |
disabled | boolean | false | Disables just this item. |
Keyboard#
| Key | Action |
|---|---|
Enter / Space | Toggle the focused trigger |
↓ / ↑ | Next / previous trigger (wraps) |
Home / End | First / last trigger |
Accessibility#
- Triggers are real buttons wrapped in headings (
headingLevel), witharia-expandedandaria-controls. - Content regions use
role="region"+aria-labelledby.