Skip to content
Pebble Pro is here at $49 one timeSee what’s included ↗
Pebble
ComponentsDocsMCPPricing
⌕ Find a component↗Log in
All components 673

Categories

Core inputs & forms46Buttons & actions33Navigation36Overlays & feedback49Data display33Charts & data viz40Cards & layout43Marketing blocks38App shells & pages37AI interfaces40Text & typography effects33Backgrounds & visual effects38Motion & transitions32Numbers & counters32Media34E-commerce333D & interactive36Themed sets40
Start with the essentials.Installation guide ↗Connect your agent ↗
← All componentsCards & layout

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.json

Props

PropTypeDefaultDescription
type"single" | "multiple"—One open section at a time, or any number.
collapsiblebooleanfalsesingle only: lets the open section close.
defaultValue / valuestring | string[]—Open section(s), uncontrolled or controlled.
onValueChange(value) => void—Called when the open section(s) change.
disabledbooleanfalseOn 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>
  );
}
Pebble

Small details.
Better interfaces.

React components to explore, make your own, and build on.

Library

  • Components
  • Installation
  • Pricing

Resources

  • For agents
  • About
  • Support

Legal

  • License
  • Terms
  • Privacy
  • Final sale

Built by Pebble

A product of Praxys Solutions LLC ↗

Pebble Pro is live

$49 one time, lifetime updates ↗