Collapsible sections of content, on their own or grouped as an accordion. Three group styles (line, card, separated), chevron or plus indicators, single or multiple expansion, controlled state, and a height animation that respects reduced motion. Collapsed content stays findable with the browser's find-in-page.
Yes. Every component is built on React Aria and follows WAI-ARIA patterns.
Yes. It uses the same tokens and installs with the shadcn CLI.
Change the CSS variables. Every component reads from them.
import { Disclosure, DisclosureGroup, DisclosurePanel, DisclosureTrigger,} from "@/components/ui/disclosure";const faqs = [ { id: "a11y", q: "Is it accessible?", a: "Yes. Every component is built on React Aria and follows WAI-ARIA patterns.", }, { id: "shadcn", q: "Does it work with shadcn/ui?", a: "Yes. It uses the same tokens and installs with the shadcn CLI.", }, { id: "themes", q: "Can I change the theme?", a: "Change the CSS variables. Every component reads from them.", },];export default function DisclosureDemo() { return ( <DisclosureGroup className="max-w-md" defaultExpandedKeys={["a11y"]}> {faqs.map((f) => ( <Disclosure key={f.id} id={f.id}> <DisclosureTrigger>{f.q}</DisclosureTrigger> <DisclosurePanel>{f.a}</DisclosurePanel> </Disclosure> ))} </DisclosureGroup> );}
variant="card" wraps the whole group in one bordered card with dividers, and tints the open item's trigger. Icons in the trigger are sized to 16px and muted.
Invite people to the workspace and choose what they can access.
Create keys for CI and server-side scripts. Keys inherit the permissions of the member who created them.
Change your plan, update the card on file and download invoices.
import { CreditCardIcon, KeyRoundIcon, UsersIcon } from "lucide-react";import { Disclosure, DisclosureGroup, DisclosurePanel, DisclosureTrigger,} from "@/components/ui/disclosure";export default function DisclosureCard() { return ( <DisclosureGroup variant="card" className="max-w-md" defaultExpandedKeys={["members"]} > <Disclosure id="members"> <DisclosureTrigger> <UsersIcon /> Members </DisclosureTrigger> <DisclosurePanel> Invite people to the workspace and choose what they can access. </DisclosurePanel> </Disclosure> <Disclosure id="keys"> <DisclosureTrigger> <KeyRoundIcon /> API keys </DisclosureTrigger> <DisclosurePanel> Create keys for CI and server-side scripts. Keys inherit the permissions of the member who created them. </DisclosurePanel> </Disclosure> <Disclosure id="billing"> <DisclosureTrigger> <CreditCardIcon /> Billing </DisclosureTrigger> <DisclosurePanel> Change your plan, update the card on file and download invoices. </DisclosurePanel> </Disclosure> </DisclosureGroup> );}
variant="separated" gives each item its own card with a gap between them. The open item gets a slightly stronger shadow. Suits FAQs and marketing pages.
Unlimited projects, 1 TB of bandwidth, preview deployments for every branch and email support.
Yes. Cancel from Billing settings and you keep Pro features until the end of the current period.
We refund annual plans in full within 14 days of purchase. Contact support and we'll handle it within one business day.
import { Disclosure, DisclosureGroup, DisclosurePanel, DisclosureTrigger,} from "@/components/ui/disclosure";const faqs = [ { id: "included", q: "What's included in the Pro plan?", a: "Unlimited projects, 1 TB of bandwidth, preview deployments for every branch and email support.", }, { id: "cancel", q: "Can I cancel anytime?", a: "Yes. Cancel from Billing settings and you keep Pro features until the end of the current period.", }, { id: "refunds", q: "Do you offer refunds?", a: "We refund annual plans in full within 14 days of purchase. Contact support and we'll handle it within one business day.", },];export default function DisclosureSeparated() { return ( <DisclosureGroup variant="separated" className="max-w-md"> {faqs.map((f) => ( <Disclosure key={f.id} id={f.id}> <DisclosureTrigger indicator="plus">{f.q}</DisclosureTrigger> <DisclosurePanel>{f.a}</DisclosurePanel> </Disclosure> ))} </DisclosureGroup> );}
allowsMultipleExpanded lets users open several items at once. Use it when items are independent and users may compare them, like product details or filters.
Over-ear wireless headphones with active noise cancelling and 40 hours of battery life.
Recycled aluminium frame, protein-leather cushions. Wipe with a dry cloth.
The trigger's children are laid out in a row with gap-2, so icons, monospace labels and Badges line up. Keep interactive elements out of the trigger; it's already a button.
3 instances · p95 142 ms · last deploy 12 minutes ago
1 of 4 instances failing health checks since 09:42 UTC
Primary in us-east-1 · 2 read replicas · 38% storage used
isDisabled on a Disclosure stops it from toggling and dims its trigger. isDisabled on DisclosureGroup disables every item. Explain why in the trigger text or nearby.
Workspace name, URL and default language.
SAML and OIDC configuration.
import { LockIcon } from "lucide-react";import { Disclosure, DisclosureGroup, DisclosurePanel, DisclosureTrigger,} from "@/components/ui/disclosure";export default function DisclosureDisabled() { return ( <DisclosureGroup variant="card" className="max-w-md"> <Disclosure id="general"> <DisclosureTrigger>General</DisclosureTrigger> <DisclosurePanel> Workspace name, URL and default language. </DisclosurePanel> </Disclosure> <Disclosure id="sso" isDisabled> <DisclosureTrigger> <LockIcon /> Single sign-on (Enterprise) </DisclosureTrigger> <DisclosurePanel>SAML and OIDC configuration.</DisclosurePanel> </Disclosure> </DisclosureGroup> );}
Pass expandedKeys and onExpandedChange to DisclosureGroup to own the state, for example for Expand all and Collapse all buttons. For a single Disclosure, use isExpanded and onExpandedChange.
DisclosureTrigger always renders a heading and a full-width button. For a different layout, put any React Aria button with slot="trigger" inside the Disclosure, such as a link-style Button below a preview. It gets aria-expanded and aria-controls automatically, and group-data-expanded/disclosure: classes can react to the state.
You get every Pro feature for 14 days. We don't ask for a card up front, and you can pick a plan at any point during the trial.
Anyone who can sign in to your workspace. Guests with view-only access to shared links are free.
In AWS regions of your choice (US, EU or APAC), encrypted at rest with AES-256 and in transit with TLS 1.3.
Yes. Teams on annual plans get a migration engineer and import tools for the most common alternatives.
import { Disclosure, DisclosureGroup, DisclosurePanel, DisclosureTrigger,} from "@/components/ui/disclosure";import { Link } from "@/components/ui/link";const faqs = [ { id: "trial", q: "How does the free trial work?", a: "You get every Pro feature for 14 days. We don't ask for a card up front, and you can pick a plan at any point during the trial.", }, { id: "seats", q: "What counts as a seat?", a: "Anyone who can sign in to your workspace. Guests with view-only access to shared links are free.", }, { id: "security", q: "Where is my data stored?", a: "In AWS regions of your choice (US, EU or APAC), encrypted at rest with AES-256 and in transit with TLS 1.3.", }, { id: "migrate", q: "Can you help us migrate?", a: "Yes. Teams on annual plans get a migration engineer and import tools for the most common alternatives.", },];export default function DisclosureRecipeFaq() { return ( <section className="grid w-full max-w-3xl gap-8 md:grid-cols-[1fr_1.6fr]"> <div className="grid content-start gap-2"> <h2 className="font-semibold text-xl tracking-tight"> Frequently asked questions </h2> <p className="text-muted-foreground text-sm"> Can't find what you're looking for?{" "} <Link href="#">Talk to our team</Link>. </p> </div> <DisclosureGroup defaultExpandedKeys={["trial"]}> {faqs.map((f) => ( <Disclosure key={f.id} id={f.id}> <DisclosureTrigger indicator="plus">{f.q}</DisclosureTrigger> <DisclosurePanel>{f.a}</DisclosurePanel> </Disclosure> ))} </DisclosureGroup> </section> );}
Each trigger is a <button> with aria-expanded and aria-controls pointing at its panel, inside an <h3> so sections appear in the heading outline.
The panel has role="group" by default and is labelled by its trigger. Pass role="region" to DisclosurePanel for important sections that should be landmarks; use it sparingly.
Collapsed panels use hidden="until-found" where the browser supports it, so find-in-page can reveal and expand them.
The height animation is disabled when the user prefers reduced motion.
Triggers use their text as the accessible name; icons and indicators are decorative.
The panel animates height to the --disclosure-panel-height variable that React Aria sets when it opens and closes (h-(--disclosure-panel-height) overflow-hidden transition-[height] duration-200). Reuse the same classes on your own DisclosurePanel from React Aria to get the same motion.
className on DisclosureGroup, Disclosure and DisclosurePanel accepts a string or a function of their render props.
className on DisclosureTrigger goes to the button, e.g. className="py-2 text-xs" for denser lists.
The panel's padding and text color come from the variant and are on its inner element. Override them with a child wrapper or [&>div]:… on DisclosurePanel.
DisclosureTrigger takes only these props. It renders an <h3>; for another heading level or other button props, use a custom trigger with slot="trigger".