Collapsible

A single section that opens and closes, without an accordion's set semantics.

Installation

Terminal
pnpm dlx @dowel-ui/cli add collapsible

npm 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

  • Testedpasses
  • axe assertionpasses
  • Keyboard testeddoes not apply
  • Storybook examplespasses
  • Accessibility documentedpasses
  • Semantic tokens onlypasses
  • Motion from tokenspasses
  • className mergedpasses
  • Visible focusdoes not apply
  • No fixed widthspasses

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}
    />
  );
}