component

Accordion

A collapsible disclosure component following the W3C ARIA accordion pattern. Each item has an <h3>-wrapped trigger <button> paired with a role="region" panel. Adjacent interactive controls (action buttons, drag handles) live as siblings of the trigger inside the header row — independently focusable, never nested inside a button.

Find-in-page (Ctrl/⌘+F) still reveals text in collapsed panels: the panel carries hidden="until-found" when closed, and the browser’s beforematch event opens the matching item (Chromium; Firefox/Safari skip the auto-reveal, matching their native <details> behavior).

Expansion Behavior

The type prop controls how many items can be open at once. single (default) collapses the previously open item when another opens — keeping the view focused. multiple lets any number of items be open simultaneously.

Use collapsible (default true) on single accordions to allow the open item to be collapsed back to nothing.

Single

Pindoba is a design system providing framework-agnostic UI components for Astro, React, and Svelte.

Multiple

Pindoba is a design system providing framework-agnostic UI components for Astro, React, and Svelte.

Install individual packages via pnpm, for example: pnpm add @pindoba/svelte-accordion.

Disabled Item

This item can be expanded and collapsed normally.

---
// GENERATED from packages/demos/accordion/variants.demo.tsx — edit the source, not this file.
import Accordion from "@pindoba/astro-accordion";
import { AccordionItem } from "@pindoba/astro-accordion";
import { stack } from "@pindoba/styled-system/patterns";
---

<div
  class={stack({
    gap: "xl",
    direction: "column",
  })}
>
  <div>
    <h3>Single</h3>
    <Accordion type="single" collapsible>
      <AccordionItem value="item-1" title="What is Pindoba?" open>
        <p>
          Pindoba is a design system providing framework-agnostic UI components
          for Astro, React, and Svelte.
        </p>
      </AccordionItem>
      <AccordionItem value="item-2" title="How do I install it?">
        <p>
          Install individual packages via pnpm, for example:{" "}
          <code>pnpm add @pindoba/svelte-accordion</code>.
        </p>
      </AccordionItem>
      <AccordionItem value="item-3" title="Is it customizable?">
        <p>
          Yes — every component exposes a <code>passThrough</code> prop for
          custom styles and HTML attributes on each slot.
        </p>
      </AccordionItem>
    </Accordion>
  </div>

  <div>
    <h3>Multiple</h3>
    <Accordion type="multiple">
      <AccordionItem value="item-1" title="What is Pindoba?" open>
        <p>
          Pindoba is a design system providing framework-agnostic UI components
          for Astro, React, and Svelte.
        </p>
      </AccordionItem>
      <AccordionItem value="item-2" title="How do I install it?" open>
        <p>
          Install individual packages via pnpm, for example:{" "}
          <code>pnpm add @pindoba/svelte-accordion</code>.
        </p>
      </AccordionItem>
      <AccordionItem value="item-3" title="Is it customizable?">
        <p>
          Yes — every component exposes a <code>passThrough</code> prop for
          custom styles and HTML attributes on each slot.
        </p>
      </AccordionItem>
    </Accordion>
  </div>

  <div>
    <h3>Disabled Item</h3>
    <Accordion type="single" collapsible>
      <AccordionItem value="item-1" title="Available Section" open>
        <p>This item can be expanded and collapsed normally.</p>
      </AccordionItem>
      <AccordionItem value="item-2" title="Disabled Section" disabled>
        <p>This item is not accessible when disabled.</p>
      </AccordionItem>
      <AccordionItem value="item-3" title="Another Available Section">
        <p>This item is also available for interaction.</p>
      </AccordionItem>
    </Accordion>
  </div>
</div>

Size

Three sizes scale the trigger padding, content padding, and font size together. The size is set once on the root <Accordion> and inherited by every item.

Small

Small size uses tighter padding and a smaller font — suitable for dense layouts or secondary content areas.

Medium (default)

Medium size is the default — balanced padding and font size for most use cases.

Large

Large size uses generous padding and a larger font — ideal for prominent content or marketing pages.

Per-item override

This item uses the root's size="sm".

---
// GENERATED from packages/demos/accordion/size.demo.tsx — edit the source, not this file.
import Accordion from "@pindoba/astro-accordion";
import { AccordionItem } from "@pindoba/astro-accordion";
import { stack } from "@pindoba/styled-system/patterns";
---

<div
  class={stack({
    gap: "xl",
    direction: "column",
  })}
>
  <div>
    <h3>Small</h3>
    <Accordion type="single" collapsible size="sm">
      <AccordionItem value="item-1" title="Compact section" open>
        <p>
          Small size uses tighter padding and a smaller font — suitable for
          dense layouts or secondary content areas.
        </p>
      </AccordionItem>
      <AccordionItem value="item-2" title="Another compact section">
        <p>
          Inherits <code>size="sm"</code> from the root — no need to repeat on
          each item.
        </p>
      </AccordionItem>
    </Accordion>
  </div>

  <div>
    <h3>Medium (default)</h3>
    <Accordion type="single" collapsible size="md">
      <AccordionItem value="item-1" title="Standard section" open>
        <p>
          Medium size is the default — balanced padding and font size for most
          use cases.
        </p>
      </AccordionItem>
      <AccordionItem value="item-2" title="Another standard section">
        <p>
          Inherits <code>size="md"</code> from the root.
        </p>
      </AccordionItem>
    </Accordion>
  </div>

  <div>
    <h3>Large</h3>
    <Accordion type="single" collapsible size="lg">
      <AccordionItem value="item-1" title="Spacious section" open>
        <p>
          Large size uses generous padding and a larger font — ideal for
          prominent content or marketing pages.
        </p>
      </AccordionItem>
      <AccordionItem value="item-2" title="Another spacious section">
        <p>
          Inherits <code>size="lg"</code> from the root.
        </p>
      </AccordionItem>
    </Accordion>
  </div>

  <div>
    <h3>Per-item override</h3>
    <Accordion type="single" collapsible size="sm">
      <AccordionItem value="item-1" title="Small (inherited)" open>
        <p>
          This item uses the root's <code>size="sm"</code>.
        </p>
      </AccordionItem>
      <AccordionItem value="item-2" title="Large (override)" size="lg">
        <p>
          This item sets its own <code>size="lg"</code>, which overrides the
          root's <code>sm</code>.
        </p>
      </AccordionItem>
      <AccordionItem value="item-3" title="Small again (inherited)">
        <p>Back to inheriting from the root.</p>
      </AccordionItem>
    </Accordion>
  </div>
</div>

Items Array

For data-driven usage, pass an items array to <Accordion> and it renders one <AccordionItem> per entry. Root-level props (size, feedback, background, …) act as defaults; each entry can override via its own fields. Body content is provided per framework: named slots in Astro (<Fragment slot="<value>">), named snippets or the snippets map in Svelte (keyed by each item’s value). For rich custom trigger content, fall back to slot composition with <AccordionItem> directly.

Items array with cascaded defaults

Rendered from a named snippet matching the item's value. Inherits size="md" and feedback="primary" from the root.

Per-item overrides

Uses root defaults.

Item sets size: "lg", which wins over the root's sm.

With subtitles

---
// GENERATED from packages/demos/accordion/items.demo.tsx — edit the source, not this file.
import Accordion from "@pindoba/astro-accordion";
import { stack } from "@pindoba/styled-system/patterns";
---

<div
  class={stack({
    gap: "xl",
    direction: "column",
  })}
>
  <div>
    <h3>Items array with cascaded defaults</h3>
    <Accordion
      type="single"
      collapsible
      size="md"
      feedback="primary"
      items={[
        { value: "first", title: "First", open: true },
        { value: "second", title: "Second" },
        { value: "third", title: "Third" },
      ]}
    >
      <p slot="first">
        Rendered from a named snippet matching the item's <code>value</code>.
        Inherits <code>size="md"</code> and <code>feedback="primary"</code> from
        the root.
      </p>
      <p slot="second">Same defaults as the first.</p>
      <p slot="third">Also inherits from the root.</p>
    </Accordion>
  </div>

  <div>
    <h3>Per-item overrides</h3>

    <Accordion
      type="multiple"
      size="sm"
      feedback="neutral"
      items={[
        {
          value: "default",
          title: "Default (size=sm, feedback=neutral)",
          open: true,
        },
        {
          value: "larger",
          title: "Overrides size",
          size: "lg",
          open: true,
        },
        {
          value: "danger",
          title: "Overrides feedback",
          feedback: "danger",
        },
        { value: "disabled", title: "Disabled", disabled: true },
      ]}
    >
      <p slot="default">Uses root defaults.</p>
      <p slot="larger">
        Item sets <code>size: "lg"</code>, which wins over the root's{" "}
        <code>sm</code>.
      </p>
      <p slot="danger">
        Item sets <code>feedback: "danger"</code>, which wins over the root's{" "}
        <code>neutral</code>.
      </p>
      <p slot="disabled">Set per item.</p>
    </Accordion>
  </div>

  <div>
    <h3>With subtitles</h3>
    <Accordion
      type="single"
      collapsible
      size="md"
      items={[
        {
          value: "auth",
          title: "Authentication",
          subtitle: "OAuth providers and session settings",
        },
        {
          value: "db",
          title: "Database",
          subtitle: "Connection strings, pooling, migrations",
        },
        {
          value: "storage",
          title: "Storage",
          subtitle: "File uploads and CDN",
        },
      ]}
    >
      <p slot="auth">Configure sign-in options.</p>
      <p slot="db">Manage database connectivity.</p>
      <p slot="storage">Configure object storage and CDN.</p>
    </Accordion>
  </div>
</div>

Feedback Colors

Accordion items use Panel under the hood — pass feedback and border on the root to color all items consistently. Use feedback for semantic meaning: primary for key content, success for completed flows, warning for notices, and danger for critical information.

Neutral

Neutral is the default feedback color — use it for standard content without any special semantic meaning.

Primary

Primary feedback applies the brand color palette — use it to highlight important content or onboarding steps.

Success

Success feedback conveys positive outcomes — use it for completed steps, confirmation flows, or resolved states.

Warning

Warning feedback draws attention to caution states — use it for deprecation notices, rate limits, or soft restrictions.

Danger

Danger feedback signals critical or destructive information — use it for breaking changes, error states, or irreversible actions.

---
// GENERATED from packages/demos/accordion/feedback.demo.tsx — edit the source, not this file.
import Accordion from "@pindoba/astro-accordion";
import { AccordionItem } from "@pindoba/astro-accordion";
import { stack } from "@pindoba/styled-system/patterns";
---

<div
  class={stack({
    gap: "xl",
    direction: "column",
  })}
>
  <div>
    <h3>Neutral</h3>
    <Accordion type="single" collapsible feedback="neutral">
      <AccordionItem value="item-1" title="General information" open>
        <p>
          Neutral is the default feedback color — use it for standard content
          without any special semantic meaning.
        </p>
      </AccordionItem>
      <AccordionItem value="item-2" title="Additional details">
        <p>Each item inherits the feedback color and border from the root.</p>
      </AccordionItem>
    </Accordion>
  </div>

  <div>
    <h3>Primary</h3>
    <Accordion type="single" collapsible feedback="primary">
      <AccordionItem value="item-1" title="Getting started" open>
        <p>
          Primary feedback applies the brand color palette — use it to highlight
          important content or onboarding steps.
        </p>
      </AccordionItem>
      <AccordionItem value="item-2" title="Next steps">
        <p>Each item inherits the feedback color and border from the root.</p>
      </AccordionItem>
    </Accordion>
  </div>

  <div>
    <h3>Success</h3>
    <Accordion type="single" collapsible feedback="success">
      <AccordionItem value="item-1" title="Completed tasks" open>
        <p>
          Success feedback conveys positive outcomes — use it for completed
          steps, confirmation flows, or resolved states.
        </p>
      </AccordionItem>
      <AccordionItem value="item-2" title="What's next">
        <p>Each item inherits the feedback color and border from the root.</p>
      </AccordionItem>
    </Accordion>
  </div>

  <div>
    <h3>Warning</h3>
    <Accordion type="single" collapsible feedback="warning">
      <AccordionItem value="item-1" title="Deprecation notice" open>
        <p>
          Warning feedback draws attention to caution states — use it for
          deprecation notices, rate limits, or soft restrictions.
        </p>
      </AccordionItem>
      <AccordionItem value="item-2" title="Migration guide">
        <p>Each item inherits the feedback color and border from the root.</p>
      </AccordionItem>
    </Accordion>
  </div>

  <div>
    <h3>Danger</h3>
    <Accordion type="single" collapsible feedback="danger">
      <AccordionItem value="item-1" title="Breaking changes" open>
        <p>
          Danger feedback signals critical or destructive information — use it
          for breaking changes, error states, or irreversible actions.
        </p>
      </AccordionItem>
      <AccordionItem value="item-2" title="How to resolve">
        <p>Each item inherits the feedback color and border from the root.</p>
      </AccordionItem>
    </Accordion>
  </div>
</div>

Emphasis

emphasis follows panel’s three-tier model. primary fills each item with the feedback accent surface and contrast text. secondary (default) uses the feedback-tinted soft surface — the standard accordion look. tertiary keeps the surface neutral but tints the trigger text and border with the feedback color, useful when a tinted background would compete with the surrounding page.

Set emphasis per <AccordionItem> for slot-composed accordions (each child carries its own panel surface). When you use the items array on <Accordion>, the root-level emphasis acts as a default for every item — same shape as background, border, and padding.

Primary

Primary emphasis fills each item with the feedback's accent surface and uses contrast text. Use it for the most prominent accordion sections.

Secondary (default)

Secondary emphasis paints each item with the feedback's soft surface. This is the default — most accordions use it.

Tertiary

Tertiary emphasis keeps the surface neutral but tints the text and border with the feedback color. Useful when a tinted background would compete with surrounding content.

---
// GENERATED from packages/demos/accordion/emphasis.demo.tsx — edit the source, not this file.
import Accordion from "@pindoba/astro-accordion";
import { AccordionItem } from "@pindoba/astro-accordion";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "xl", direction: "column" })}>
  <div>
    <h3>Primary</h3>
    <Accordion type="single" feedback="primary">
      <AccordionItem
        value="item-1"
        title="Filled accent surface"
        emphasis="primary"
        open
      >
        <p>
          Primary emphasis fills each item with the feedback's accent surface
          and uses contrast text. Use it for the most prominent accordion
          sections.
        </p>
      </AccordionItem>
      <AccordionItem value="item-2" title="More detail" emphasis="primary">
        <p>The trigger and indicator read against the bold surface.</p>
      </AccordionItem>
    </Accordion>
  </div>

  <div>
    <h3>Secondary (default)</h3>
    <Accordion type="single" feedback="primary">
      <AccordionItem value="item-1" title="Feedback-tinted soft surface" open>
        <p>
          Secondary emphasis paints each item with the feedback's soft surface.
          This is the default — most accordions use it.
        </p>
      </AccordionItem>
      <AccordionItem value="item-2" title="More detail">
        <p>Trigger text picks up the feedback's accent color.</p>
      </AccordionItem>
    </Accordion>
  </div>

  <div>
    <h3>Tertiary</h3>
    <Accordion type="single" feedback="primary">
      <AccordionItem
        value="item-1"
        title="Neutral surface, feedback text"
        emphasis="tertiary"
        open
      >
        <p>
          Tertiary emphasis keeps the surface neutral but tints the text and
          border with the feedback color. Useful when a tinted background would
          compete with surrounding content.
        </p>
      </AccordionItem>
      <AccordionItem value="item-2" title="More detail" emphasis="tertiary">
        <p>The trigger and content read against neutral surface.</p>
      </AccordionItem>
    </Accordion>
  </div>
</div>

Rich Triggers

Accordion items use Banner internally for their trigger area. This means you can add a subtitle, inline icons via headingLeading / headingTrailing, and a trailing element at the far right of the trigger row — all without replacing the whole trigger. These slots are decorative — they render inside the trigger button, so don’t put interactive controls (like <button> or links) here. For interactive header controls, see Header Actions below.

Subtitle

Heading Trailing

Leading Icon

Trailing

---
// GENERATED from packages/demos/accordion/rich-trigger.demo.tsx — edit the source, not this file.
import Accordion from "@pindoba/astro-accordion";
import { AccordionItem } from "@pindoba/astro-accordion";
import Stamp from "@pindoba/astro-stamp";
import {
  KeyRound,
  Database,
  HardDrive,
  User,
  Bell,
  Shield,
  Settings,
} from "@lucide/astro";
import { stack } from "@pindoba/styled-system/patterns";
import { css } from "@pindoba/styled-system/css";

const badge = css({
  display: "inline-flex",
  alignItems: "center",
  px: "xs",
  py: "3xs",
  borderRadius: "full",
  fontSize: "xs",
  fontWeight: "semibold",
  background: "colorPalette.surface.valley",
  color: "colorPalette.text",
});
---

<div
  class={stack({
    gap: "xl",
    direction: "column",
  })}
>
  <div>
    <h3>Subtitle</h3>
    <Accordion type="single" collapsible>
      <AccordionItem
        value="item-1"
        title="Authentication"
        subtitle="Configure OAuth providers and session settings"
      >
        <Stamp
          slot="leading"
          feedback="neutral"
          emphasis="secondary"
          shape="square"
        >
          <KeyRound size={16} />
        </Stamp>
        <p>
          Add a <code>subtitle</code> to provide secondary context below the
          title without losing the clickable trigger area.
        </p>
      </AccordionItem>
      <AccordionItem
        value="item-2"
        title="Database"
        subtitle="Connection strings, pooling, and migrations"
      >
        <Stamp
          slot="leading"
          feedback="neutral"
          emphasis="secondary"
          shape="square"
        >
          <Database size={16} />
        </Stamp>
        <p>Configure your database connection and migration settings here.</p>
      </AccordionItem>
      <AccordionItem
        value="item-3"
        title="Storage"
        subtitle="File uploads and CDN configuration"
      >
        <Stamp
          slot="leading"
          feedback="neutral"
          emphasis="secondary"
          shape="square"
        >
          <HardDrive size={16} />
        </Stamp>
        <p>Set up file storage providers and CDN delivery options.</p>
      </AccordionItem>
    </Accordion>
  </div>

  <div>
    <h3>Heading Trailing</h3>
    <Accordion type="multiple">
      <AccordionItem value="item-1" title="Open Issues">
        <span slot="heading-trailing" class={badge}>
          12
        </span>
        <p>
          Use <code>headingTrailing</code> to place a badge, count, or status
          indicator inline with the title text.
        </p>
      </AccordionItem>
      <AccordionItem value="item-2" title="Pull Requests">
        <span slot="heading-trailing" class={badge}>
          3
        </span>
        <p>Active pull requests waiting for review.</p>
      </AccordionItem>
      <AccordionItem value="item-3" title="Deployments">
        <span slot="heading-trailing" class={badge}>
          1
        </span>
        <p>Recent deployment activity and logs.</p>
      </AccordionItem>
    </Accordion>
  </div>

  <div>
    <h3>Leading Icon</h3>
    <Accordion type="single" collapsible>
      <AccordionItem value="item-1" title="Profile">
        <Stamp
          slot="heading-leading"
          feedback="primary"
          emphasis="secondary"
          shape="circle"
          size="xs"
        >
          <User size={12} />
        </Stamp>
        <p>
          Use <code>headingLeading</code> to prepend a Stamp icon directly
          beside the title text.
        </p>
      </AccordionItem>
      <AccordionItem value="item-2" title="Notifications">
        <Stamp
          slot="heading-leading"
          feedback="warning"
          emphasis="secondary"
          shape="circle"
          size="xs"
        >
          <Bell size={12} />
        </Stamp>
        <p>Notification preferences and delivery settings.</p>
      </AccordionItem>
      <AccordionItem value="item-3" title="Security">
        <Stamp
          slot="heading-leading"
          feedback="success"
          emphasis="secondary"
          shape="circle"
          size="xs"
        >
          <Shield size={12} />
        </Stamp>
        <p>Two-factor authentication and session management.</p>
      </AccordionItem>
      <AccordionItem value="item-4" title="Preferences">
        <Stamp
          slot="heading-leading"
          feedback="neutral"
          emphasis="secondary"
          shape="circle"
          size="xs"
        >
          <Settings size={12} />
        </Stamp>
        <p>General application preferences and defaults.</p>
      </AccordionItem>
    </Accordion>
  </div>

  <div>
    <h3>Trailing</h3>
    <Accordion type="single" collapsible>
      <AccordionItem value="item-1" title="Environment Variables">
        <span
          slot="trailing"
          class={css({
            fontSize: "xs",
            color: "neutral.text.muted",
            fontWeight: "normal",
            flexShrink: 0,
          })}
        >
          12 variables
        </span>
        <p>
          Use <code>trailing</code> to place content at the far right of the
          trigger row — outside the title/subtitle, before the expand indicator.
        </p>
      </AccordionItem>
      <AccordionItem value="item-2" title="Build Cache">
        <span
          slot="trailing"
          class={css({
            fontSize: "xs",
            color: "success.text",
            fontWeight: "semibold",
            flexShrink: 0,
          })}
        >
          Active
        </span>
        <p>Current build cache status and invalidation controls.</p>
      </AccordionItem>
    </Accordion>
  </div>
</div>

Header Actions

When an item needs interactive controls in its header — a delete button, an overflow menu, a drag handle — use the adjacent slots: actionsLeading (left of the trigger) and actionsTrailing (right of the trigger). They render as siblings of the trigger inside the header row, so activating them doesn’t toggle the accordion and they’re independently focusable.

There are two tiers of header slots — pick the right one for the job:

  • In-trigger, decorative (leading, trailing, headingLeading, headingTrailing, subheadingLeading, subheadingTrailing): rendered inside the trigger button. Icons, badges, counts. No interactive children.
  • Adjacent, interactive (actionsLeading, actionsTrailing): rendered next to the trigger. Buttons, menu triggers, drag handles.

Trailing actions

Use actionsTrailing for interactive controls that should sit next to the header without toggling the accordion when clicked.

Leading drag handle

Use actionsLeading for a drag handle (or any leading control). Activating it doesn't toggle the panel.

Leading + trailing together

---
// GENERATED from packages/demos/accordion/header-actions.demo.tsx — edit the source, not this file.
import Accordion from "@pindoba/astro-accordion";
import { AccordionItem } from "@pindoba/astro-accordion";
import Button from "@pindoba/astro-button";
import { Pencil, Trash, Ellipsis, GripVertical } from "@lucide/astro";
import { stack } from "@pindoba/styled-system/patterns";
import { css } from "@pindoba/styled-system/css";

const handle = css({
  display: "inline-flex",
  alignItems: "center",
  justifyContent: "center",
  p: "2xs",
  color: "neutral.text.muted",
  cursor: "grab",
  borderRadius: "xs",
  _hover: { background: "neutral.surface.hill", color: "neutral.text" },
  _focusVisible: { pindobaOutline: true },
  _active: { cursor: "grabbing" },
});
---

<div class={stack({ gap: "xl", direction: "column" })}>
  <div>
    <h3>Trailing actions</h3>
    <p>
      Use <code>actionsTrailing</code> for interactive controls that should sit
      next to the header without toggling the accordion when clicked.
    </p>
    <Accordion type="single" collapsible>
      <AccordionItem
        value="profile"
        title="Profile"
        subtitle="Name, avatar, and bio"
      >
        <Fragment slot="actions-trailing">
          <Button
            emphasis="ghost"
            size="sm"
            shape="square"
            aria-label="Edit profile"
          >
            <Pencil size={14} />
          </Button>
          <Button
            emphasis="ghost"
            size="sm"
            shape="square"
            feedback="danger"
            aria-label="Delete profile"
          >
            <Trash size={14} />
          </Button>
        </Fragment>
        <p>Click the icon buttons — they don't toggle the accordion.</p>
      </AccordionItem>
      <AccordionItem
        value="billing"
        title="Billing"
        subtitle="Plan and invoices"
      >
        <Button
          slot="actions-trailing"
          emphasis="ghost"
          size="sm"
          shape="square"
          aria-label="More"
        >
          <Ellipsis size={14} />
        </Button>
        <p>An ellipsis menu placed alongside the trigger.</p>
      </AccordionItem>
    </Accordion>
  </div>

  <div>
    <h3>Leading drag handle</h3>
    <p>
      Use <code>actionsLeading</code> for a drag handle (or any leading
      control). Activating it doesn't toggle the panel.
    </p>
    <Accordion type="multiple">
      <AccordionItem value="layer-1" title="Background">
        <button
          slot="actions-leading"
          type="button"
          class={handle}
          aria-label="Reorder Background"
        >
          <GripVertical size={16} />
        </button>
        <p>Layer settings for the background plate.</p>
      </AccordionItem>
      <AccordionItem value="layer-2" title="Foreground">
        <button
          slot="actions-leading"
          type="button"
          class={handle}
          aria-label="Reorder Foreground"
        >
          <GripVertical size={16} />
        </button>
        <Button
          slot="actions-trailing"
          emphasis="ghost"
          size="sm"
          shape="square"
          aria-label="Edit Foreground"
        >
          <Pencil size={14} />
        </Button>
        <p>Layer settings for the foreground content.</p>
      </AccordionItem>
      <AccordionItem value="layer-3" title="Overlay">
        <button
          slot="actions-leading"
          type="button"
          class={handle}
          aria-label="Reorder Overlay"
        >
          <GripVertical size={16} />
        </button>
        <p>Layer settings for the top overlay.</p>
      </AccordionItem>
    </Accordion>
  </div>

  <div>
    <h3>Leading + trailing together</h3>
    <Accordion type="single" collapsible>
      <AccordionItem
        value="task-1"
        title="Set up CI"
        subtitle="GitHub Actions and Turbo cache"
      >
        <button
          slot="actions-leading"
          type="button"
          class={handle}
          aria-label="Reorder task"
        >
          <GripVertical size={16} />
        </button>
        <Button
          slot="actions-trailing"
          emphasis="ghost"
          size="sm"
          shape="square"
          aria-label="Edit"
        >
          <Pencil size={14} />
        </Button>
        <p>Three independent focus stops: handle, trigger, edit.</p>
      </AccordionItem>
    </Accordion>
  </div>
</div>

Card

Pass a card object to frame the whole group in a Card: the group heading, subheading, eyebrow, leading and trailing actions go in the card header, an optional footer sits under the items, and the items become the card’s content. Items never become cards themselves — each keeps its disclosure pattern (<hN> → <button aria-expanded aria-controls> + role="region" panel).

Inside the card the items dissolve into flush rows: no item corners, ring or fill (the card’s surface shows through), a hairline between rows, and the open body reads as part of its row. The content is edge to edge (content.padding defaults to "none"), and a flush card lines its header text up with the row titles. Add contentBackground (or content.background) to put the rows on an inset panel over a shell — the rows then clip to the panel’s radii.inner.* corner.

In card mode the accordion-level size, background, border, radius and feedback become the card’s defaults (a card with only a heading looks like one big item panel); anything set on card wins. passThrough.card reaches the Card (its root, header, content, footer…); the card’s own passThrough wins per slot. Rich header/footer content is a snippet (Svelte), a node (React/Vue) or a card-* slot (Astro — card-trailing, card-footer-trailing, …).

Deployment settings

Applies to every environment in this project

Pushes to main and release/* need one approving review and a green pipeline.

Last changed by the platform team

Release checklist

v4.2 · 3 steps

Cut release/4.2 from main and lock merges to it.

---
// GENERATED from packages/demos/accordion/card.demo.tsx — edit the source, not this file.
import Accordion from "@pindoba/astro-accordion";
import { AccordionItem } from "@pindoba/astro-accordion";
import Button from "@pindoba/astro-button";
import { stack } from "@pindoba/styled-system/patterns";

const releaseSteps = [
  { value: "freeze", title: "Freeze the release branch", open: true },
  { value: "notes", title: "Draft the release notes" },
  { value: "rollout", title: "Roll out to 10% of regions" },
];
---

<div class={stack({ gap: "xl", direction: "column" })}>
  <Accordion
    itemHeadingLevel={4}
    card={{
      heading: "Deployment settings",
      subheading: "Applies to every environment in this project",
      footer: {
        leading: "Last changed by the platform team",
      },
    }}
  >
    <Button slot="card-trailing" size="sm" emphasis="secondary">
      New rule
    </Button>
    <Button slot="card-footer-trailing" size="sm" emphasis="ghost">
      Read the docs
    </Button>
    <AccordionItem
      value="branches"
      title="Protected branches"
      open
      headingLevel={4}
    >
      <p>
        Pushes to <code>main</code> and <code>release/*</code> need one
        approving review and a green pipeline.
      </p>
    </AccordionItem>
    <AccordionItem
      value="previews"
      title="Preview deployments"
      subtitle="Every pull request"
      headingLevel={4}
    >
      <p>Previews are torn down 48 hours after the pull request closes.</p>
    </AccordionItem>
    <AccordionItem value="secrets" title="Environment secrets" headingLevel={4}>
      <p>Secrets are encrypted at rest and injected at build time only.</p>
    </AccordionItem>
  </Accordion>

  <Accordion
    items={releaseSteps}
    itemHeadingLevel={4}
    card={{
      heading: "Release checklist",
      subheading: "v4.2 · 3 steps",
      background: "surface.base",
      border: "none",
      contentBackground: "surface.peak",
      footer: {},
    }}
  >
    <p slot="freeze">
      Cut <code>release/4.2</code> from main and lock merges to it.
    </p>
    <p slot="notes">
      Group merged pull requests by label; call out breaking changes.
    </p>
    <p slot="rollout">
      Watch error rates for an hour before widening the rollout.
    </p>
    <Button slot="card-footer-trailing" size="sm" emphasis="primary">
      Start release
    </Button>
  </Accordion>
</div>

Heading levels

The card heading renders as an <h3>, and so do the item headings by default. Set the two levels together so the group heading sits where it belongs in your page outline and the items nest one level below it: card={{ heading: "Billing", header: { headingLevel: 2 } }} with itemHeadingLevel={3} (any level from 1 to 6 works for the group heading and 2 to 6 for the items; headingLevel: "none" renders the group heading as a plain <span>). An item’s own headingLevel wins. In Astro, slot-composed <AccordionItem>s can’t read the root’s props — set headingLevel on each item (the items array does honour itemHeadingLevel).

Bare by default

Without card (or with card={false}) the accordion renders exactly as before — separate item panels, no Card. Row styling only applies in card mode: per-item surface props (background, border, radius) are for the bare stack; inside a card, style the group through card. Avoid emphasis="primary" items and a primary card inside a card-framed accordion — the rows take the card’s surface, so a filled item’s contrast text would sit on the wrong fill.

Custom Styling

The passThrough prop on AccordionItem reaches into any slot — item, header, heading, trigger, indicator, actionsLeading, actionsTrailing, content, contentInner. Use style for Panda CSS overrides and props for extra HTML attributes. On the root Accordion, passThrough.root applies to the wrapper <div>.

Custom Trigger Style

Custom Content Style

ARIA Attributes

Use passThrough.root.props on the accordion to forward HTML attributes to the root element — here adding role="region" and an aria-label for screen reader context.

---
// GENERATED from packages/demos/accordion/custom.demo.tsx — edit the source, not this file.
import Accordion from "@pindoba/astro-accordion";
import { AccordionItem } from "@pindoba/astro-accordion";
import { stack } from "@pindoba/styled-system/patterns";
import { css } from "@pindoba/styled-system/css";
---

<div
  class={stack({
    gap: "xl",
    direction: "column",
  })}
>
  <div>
    <h3>Custom Trigger Style</h3>
    <Accordion type="single" collapsible>
      <AccordionItem
        value="item-1"
        title="src/index.ts"
        passThrough={{
          trigger: {
            style: css.raw({
              fontFamily: "mono",
              fontSize: "sm",
              letterSpacing: "0.04em",
            }),
          },
        }}
      >
        <p>
          Use <code>passThrough.trigger.style</code> on an item to restyle its
          trigger — here applying a monospace font for a file-tree appearance.
        </p>
      </AccordionItem>
      <AccordionItem
        value="item-2"
        title="src/components/Button.svelte"
        passThrough={{
          trigger: {
            style: css.raw({
              fontFamily: "mono",
              fontSize: "sm",
              letterSpacing: "0.04em",
            }),
          },
        }}
      >
        <p>
          The style is applied via Panda CSS <code>css.raw()</code>, giving you
          full access to design tokens and responsive utilities.
        </p>
      </AccordionItem>
    </Accordion>
  </div>

  <div>
    <h3>Custom Content Style</h3>
    <Accordion type="single" collapsible feedback="primary">
      <AccordionItem
        value="item-1"
        title="Separated content area"
        passThrough={{
          content: {
            style: css.raw({
              borderTop: "2px solid",
              borderColor: "colorPalette.border.bold",
            }),
          },
        }}
      >
        <p>
          Use <code>passThrough.content.style</code> on an item to customize its
          content area — here adding a bold top border using the current
          feedback color palette.
        </p>
      </AccordionItem>
      <AccordionItem
        value="item-2"
        title="Another section"
        passThrough={{
          content: {
            style: css.raw({
              borderTop: "2px solid",
              borderColor: "colorPalette.border.bold",
            }),
          },
        }}
      >
        <p>Each item's content slot gets the same custom styles.</p>
      </AccordionItem>
    </Accordion>
  </div>

  <div>
    <h3>ARIA Attributes</h3>
    <Accordion
      type="single"
      collapsible
      passThrough={{
        root: {
          props: {
            role: "region",
            "aria-label": "Frequently asked questions",
          },
        },
      }}
    >
      <AccordionItem value="item-1" title="What does this do?" open>
        <p>
          Use <code>passThrough.root.props</code> on the accordion to forward
          HTML attributes to the root element — here adding{" "}
          <code>role="region"</code> and an <code>aria-label</code> for screen
          reader context.
        </p>
      </AccordionItem>
      <AccordionItem value="item-2" title="Why use passThrough?">
        <p>
          <code>passThrough</code> is the escape hatch for any customization not
          covered by component props. It keeps the public API clean while
          remaining fully flexible.
        </p>
      </AccordionItem>
    </Accordion>
  </div>
</div>
  • Components
  • Blocks