Accordion
Stacked sections that expand and collapse, built on the Radix Accordion primitive.
Preview
Each active member counts as one seat, prorated to the day. Guests who only comment are free, so Malik’s reviewers never show up on the invoice.
Order 4821
Placed by Tomás · Oct 30
Linen notebook, A5 × 2$38.00
Brass desk lamp$1,184.00
Cork coaster set$62.50
Install
Add the editable source with the shadcn CLI. Your project also needs the shared theme tokens this component uses. Setup and requirements. The Pebble installer is not published yet.
npx shadcn@latest add https://pebbleui.com/r/accordion.jsonProps
| Prop | Type | Default | Description |
|---|---|---|---|
| type | "single" | "multiple" | — | One open section at a time, or any number. |
| collapsible | boolean | false | single only: lets the open section close. |
| defaultValue / value | string | string[] | — | Open section(s), uncontrolled or controlled. |
| onValueChange | (value) => void | — | Called when the open section(s) change. |
| disabled | boolean | false | On Accordion or AccordionItem: blocks toggling. |
Build notes
Built on the Radix Accordion primitive, which supplies the open state, `aria-expanded`/`aria-controls`, the `region` role and the `--radix-accordion-content-height` variable (the measured panel height). The open/close motion is two keyframes, `accordion-down` (height 0 to that variable) and `accordion-up` (the reverse), timed by the motion tokens (see the Pebble paragraph), shipped in the registry item's `css` so `shadcn add` installs them. They run only under `motion-safe:` and are keyed on `data-state`; the panel has `overflow-hidden` so content is clipped while the height animates. The chevron rotates 180 degrees with a CSS transition on the trigger's `data-state=open`. **Pebble (2026-11-03).** Added `variant`: `card` (default) wraps the list in a raised Pebble surface (`bg-card`, `shadow-raised-sm`, hairline ring, inset highlight, 6 px padding) and turns an open section into a recessed well (`bg-muted` with an inset shade) so the open section reads as pressed into the card; `flush` keeps the bare list for use inside another surface. Sections are divided by an inset hairline drawn with `before:` rather than a border, hidden above the first item and on either side of an open well so the well is not cut by lines. The chevron sits in a 24 px key that rises (card fill, `shadow-raised-xs`, hairline) when its section opens, and the glyph turns 180 degrees on `ease-move`. Height animation reuses the existing `accordion-down` / `accordion-up` keyframes with Pebble timing (`--motion-base` + `--motion-enter` opening, `--motion-fast` + `--motion-exit` closing), and the panel's inner text fades and drops 4 px into place with `starting:`. `@starting-style` also applies on first render, so a section open by default fades in once on page load. Hover fill and the chevron brighten only on enabled triggers. The divider hiding around an open section applies to `card` only; `flush` keeps its dividers. Reduced motion removes the height animation, rotation and fade. <!-- PEBBLE-LICENSE -->
View source
registry/layout/accordion/accordion.tsx
"use client";
/* PEBBLE-LICENSE
Built by Pebble. Component: accordion.
First-party license: MIT
The following first-party terms apply only to original Pebble material.
MIT License
Copyright (c) 2026 Praxys Solutions LLC (Pebble)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
END PEBBLE-LICENSE */
import * as React from "react";
import { Accordion as AccordionPrimitive } from "radix-ui";
import { cn } from "@/lib/utils";
export type AccordionProps = React.ComponentProps<typeof AccordionPrimitive.Root> & {
/**
* `card` (default): one raised card; the open section sinks into a recessed
* well. `flush`: no container, sections split by hairlines.
*/
variant?: "card" | "flush";
};
// Pebble: the variant is written to `data-variant` on the root and the parts
// style themselves from it with group selectors, so no context is needed.
export function Accordion({ variant = "card", className, ...props }: AccordionProps) {
return (
<AccordionPrimitive.Root
data-slot="accordion"
data-variant={variant}
className={cn(
"group/accordion",
variant === "card" &&
"rounded-xl bg-card p-1.5 shadow-raised-sm ring-1 inset-shadow-highlight ring-hairline",
className,
)}
{...(props as React.ComponentProps<typeof AccordionPrimitive.Root>)}
/>
);
}
export function AccordionItem({
className,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn(
"relative",
// Hairline between sections, drawn inset so it never touches the
// rounded corners. It fades out on both sides of an open section.
"before:pointer-events-none before:absolute before:top-0 before:h-px before:bg-border before:transition-opacity before:duration-(--motion-base) first:before:hidden",
"group-data-[variant=card]/accordion:data-[state=open]:before:opacity-0 group-data-[variant=card]/accordion:[[data-state=open]+&]:before:opacity-0",
"group-data-[variant=card]/accordion:rounded-lg group-data-[variant=card]/accordion:before:inset-x-3",
"group-data-[variant=flush]/accordion:before:inset-x-0",
// Card: the open section becomes a recessed well.
"transition-[background-color,box-shadow] duration-(--motion-base) ease-enter motion-reduce:transition-none",
"group-data-[variant=card]/accordion:data-[state=open]:bg-muted group-data-[variant=card]/accordion:data-[state=open]:inset-shadow-[0_1px_2px_var(--hairline)]",
className,
)}
{...props}
/>
);
}
export function AccordionTrigger({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
"group/trigger flex flex-1 cursor-pointer items-center justify-between gap-4 text-left text-sm font-medium tracking-[-0.01em] text-foreground",
"transition-[background-color,color] duration-(--motion-fast) ease-enter motion-reduce:transition-none",
"group-data-[variant=card]/accordion:rounded-lg group-data-[variant=card]/accordion:px-3 group-data-[variant=card]/accordion:py-3",
"group-data-[variant=card]/accordion:data-[state=closed]:enabled:hover:bg-muted/60",
"group-data-[variant=flush]/accordion:rounded-md group-data-[variant=flush]/accordion:py-3.5",
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:outline-none",
"disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
{children}
{/* The chevron sits on a small key: flat while closed, raised once
the section is open. */}
<span
aria-hidden="true"
className={cn(
"inline-flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground ring-1 ring-transparent",
"transition-[background-color,box-shadow,color] duration-(--motion-base) ease-enter motion-reduce:transition-none",
"group-enabled/trigger:group-hover/trigger:text-foreground group-data-[state=open]/trigger:bg-card group-data-[state=open]/trigger:text-foreground group-data-[state=open]/trigger:shadow-raised-xs group-data-[state=open]/trigger:ring-hairline",
)}
>
<svg
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className="size-3.5 transition-transform duration-(--motion-base) ease-move group-data-[state=open]/trigger:rotate-180 motion-reduce:transition-none"
>
<path d="m4 6 4 4 4-4" />
</svg>
</span>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
);
}
export function AccordionContent({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className={cn(
"group/content overflow-hidden text-[13px]",
// motion-safe, not a motion-reduce override: the data-state rules are
// more specific and would win over animate-none.
"motion-safe:data-[state=closed]:animate-[accordion-up_var(--motion-fast)_var(--motion-exit)] motion-safe:data-[state=open]:animate-[accordion-down_var(--motion-base)_var(--motion-enter)]",
)}
{...props}
>
<div
className={cn(
"leading-relaxed text-muted-foreground",
"group-data-[variant=card]/accordion:px-3 group-data-[variant=card]/accordion:pb-3.5",
"group-data-[variant=flush]/accordion:pb-4",
// Text settles in: a fade and a small drop as the panel opens, and a
// quicker fade as it closes.
"transition-[opacity,translate] duration-(--motion-base) ease-enter starting:-translate-y-1 starting:opacity-0",
"group-data-[state=closed]/content:opacity-0 group-data-[state=closed]/content:duration-(--motion-fast)",
"motion-reduce:transition-none motion-reduce:starting:translate-y-0",
className,
)}
>
{children}
</div>
</AccordionPrimitive.Content>
);
}
