Accordion
Vertically stacked sections that expand to reveal their content.
Orders ship within two business days and arrive in three to five days after that.
Installation
Terminal
pnpm dlx @dowel-ui/cli add accordionnpm packages installed: radix-ui.
Accessibility
Each trigger is a button inside a heading, so screen reader users can navigate the sections by heading. aria-expanded and aria-controls tie the trigger to its panel, and arrow keys move between triggers.
Props
AccordionItem
Every prop of Radix UI’s Accordion.Item, plus className.
AccordionTrigger
Every prop of Radix UI’s Accordion.Trigger, plus className.
AccordionContent
Every prop of Radix UI’s Accordion.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
Used in
Whole screens assembled from this component. Installing one brings this and everything else it needs with it.
Source
This is exactly what dowel add accordion writes into your project, with imports rewritten to your own path alias.
ui/accordion.tsx
"use client";
import { Accordion as AccordionPrimitive } from "radix-ui";
import type { ComponentPropsWithRef } from "react";
import { focusRing } from "@/lib/styles";
import { cn } from "@/lib/utils";
/**
* Vertically stacked sections that expand to reveal their content.
*
* Use `type="single"` for a set where one answer at a time is the point (an
* FAQ), and `type="multiple"` where sections are independent (settings groups).
*/
export const Accordion = AccordionPrimitive.Root;
export type AccordionItemProps = ComponentPropsWithRef<typeof AccordionPrimitive.Item>;
export function AccordionItem({ className, ...props }: AccordionItemProps) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn("border-b border-border last:border-b-0", className)}
{...props}
/>
);
}
export type AccordionTriggerProps = ComponentPropsWithRef<typeof AccordionPrimitive.Trigger>;
export function AccordionTrigger({ className, children, ...props }: AccordionTriggerProps) {
return (
// The primitive requires the trigger to sit inside a Header so the button
// carries a heading role — without it the sections are unreachable by
// heading navigation, which is how many screen reader users move around.
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
"flex flex-1 items-start justify-between gap-4 py-4 text-start text-sm font-medium",
"transition-colors duration-[var(--duration-fast)] hover:text-primary",
"disabled:pointer-events-none disabled:opacity-55",
"[&[data-state=open]>svg]:rotate-180",
focusRing,
className,
)}
{...props}
>
{children}
<svg
viewBox="0 0 24 24"
fill="none"
aria-hidden="true"
className="size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-[var(--duration-normal)] ease-[var(--ease-out-quint)]"
>
<path
d="m6 9 6 6 6-6"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
);
}
export type AccordionContentProps = ComponentPropsWithRef<typeof AccordionPrimitive.Content>;
export function AccordionContent({ className, children, ...props }: AccordionContentProps) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className={cn(
// The height keyframes read --radix-accordion-content-height, which the
// primitive measures; overflow-hidden keeps the content clipped as the
// box grows rather than spilling over the section below.
"overflow-hidden text-sm text-muted-foreground",
"data-[state=closed]:animate-accordion-close data-[state=open]:animate-accordion-open",
className,
)}
{...props}
>
<div className={cn("pb-4")}>{children}</div>
</AccordionPrimitive.Content>
);
}