Collapsible
A single section that opens and closes, without an accordion's set semantics.
Installation
Terminal
pnpm dlx @dowel-ui/cli add collapsiblenpm packages installed: radix-ui.
Accessibility
The trigger carries aria-expanded and points at the region it controls, both from the primitive. Reach for this rather than an Accordion of one item: a single-item Accordion gives its trigger a heading role and a position in a list of one, neither of which is true.
Props
Collapsible
Every prop of Radix UI’s Collapsible.Root, plus className.
CollapsibleTrigger
Every prop of Radix UI’s Collapsible.Trigger, plus className.
CollapsibleContent
Every prop of Radix UI’s Collapsible.Content, plus className.
Quality
8/8 checks, measured from the source and its tests
- Tested — passes
- axe assertion — passes
- Keyboard tested — does not apply
- Storybook examples — passes
- Accessibility documented — passes
- Semantic tokens only — passes
- Motion from tokens — passes
- className merged — passes
- Visible focus — does not apply
- No fixed widths — passes
Source
This is exactly what dowel add collapsible writes into your project, with imports rewritten to your own path alias.
ui/collapsible.tsx
"use client";
import { Collapsible as CollapsiblePrimitive } from "radix-ui";
import type { ComponentPropsWithRef } from "react";
import { cn } from "@/lib/utils";
/**
* A section that opens and closes.
*
* Unlike Accordion, there is no set: one region, one trigger, no notion of
* "only one open at a time". Reaching for an Accordion with a single item
* gives that item a `heading` role and a position in a list of one, both of
* which are noise.
*
* The height animation is driven by the primitive's own measured height rather
* than a guess, so content of any size opens smoothly, and it derives from
* `--duration-*` so the motion scale applies.
*/
export type CollapsibleProps = ComponentPropsWithRef<typeof CollapsiblePrimitive.Root>;
/**
* Wrapped rather than re-exported.
*
* A bare `export const Collapsible = CollapsiblePrimitive.Root` leaves the
* inferred type pointing into the primitive's own package, which TypeScript
* cannot name in declaration output (TS2883). It also gives every other
* component's `data-slot` a hole where this one should be.
*/
export function Collapsible({ className, ...props }: CollapsibleProps) {
return <CollapsiblePrimitive.Root data-slot="collapsible" className={className} {...props} />;
}
export function CollapsibleTrigger({
className,
...props
}: ComponentPropsWithRef<typeof CollapsiblePrimitive.Trigger>) {
return (
<CollapsiblePrimitive.Trigger
data-slot="collapsible-trigger"
className={cn("outline-none", className)}
{...props}
/>
);
}
export function CollapsibleContent({
className,
...props
}: ComponentPropsWithRef<typeof CollapsiblePrimitive.Content>) {
return (
<CollapsiblePrimitive.Content
data-slot="collapsible-content"
className={cn(
"overflow-hidden",
"data-[state=closed]:animate-collapse-up data-[state=open]:animate-collapse-down",
className,
)}
{...props}
/>
);
}