Skip to main content
Urbicon UI

Collapsible

A single expand/collapse panel with a default or custom trigger, the low-level primitive behind Accordion.

Playground

Design tokens are named values — colors, spacing, radii — that form the single source of truth for your design system. They bridge the gap between design tools and code.

Variant Style variant
Size Style variant
<Collapsible>
  <p class="text-text-secondary text-sm">
    Design tokens are named values — colors, spacing, radii — that form the single source of
    truth for your design system. They bridge the gap between design tools and code.
  </p>
</Collapsible>

01 Examples

A Collapsible is a single panel: title sets its trigger text and the default slot is the content it reveals. Leave it uncontrolled with defaultOpen, or drive it from your own state with bind:open. For a set of panels where only one stays open at a time, reach for Accordion instead.

FAQ item

Stack independent Collapsibles to build a FAQ list where any number of panels can be open at once.

Named values (colors, spacing, radii) that form the single source of truth for your design system. They bridge the gap between design tools and code.

No. Every component ships with sensible defaults. Tailwind helps when you want to override styles via slotClasses, but it's optional.

Yes. The components are SSR-safe and hydrate without layout shift. See the SvelteKit adapter docs for setup details.

<div class="flex w-full max-w-lg flex-col gap-3">
  <Collapsible variant="card" title="What are design tokens?" defaultOpen>
    <p class="text-text-secondary text-sm leading-relaxed">
      Named values (colors, spacing, radii) that form the single source of truth for your
      design system. They bridge the gap between design tools and code.
    </p>
  </Collapsible>
  <Collapsible variant="card" title="Do I need to learn Tailwind?">
    <p class="text-text-secondary text-sm leading-relaxed">
      No. Every component ships with sensible defaults. Tailwind helps when you want to
      override styles via <code class="bg-surface-base rounded px-1.5 py-0.5 text-xs"
        >slotClasses</code
      >, but it's optional.
    </p>
  </Collapsible>
  <Collapsible variant="card" title="Can I use this with SvelteKit?">
    <p class="text-text-secondary text-sm leading-relaxed">
      Yes. The components are SSR-safe and hydrate without layout shift. See the SvelteKit
      adapter docs for setup details.
    </p>
  </Collapsible>
</div>

Controlled section toggle

Drive the open state from outside via bind:open. Useful for filter panels, settings sections, or any UI that coordinates state with the rest of the page.
closed

This panel is controlled via bind:open. Toggle it with the button above or by clicking the trigger.

<div class="flex w-full max-w-lg flex-col gap-4">
  <div class="flex items-center gap-3">
    <Button size="sm" variant="outlined" onclick={() => (controlledOpen = !controlledOpen)}>
      {controlledOpen ? 'Hide filters' : 'Show filters'}
    </Button>
    <Badge size="xs" intent={controlledOpen ? 'success' : 'neutral'} variant="soft">
      {controlledOpen ? 'open' : 'closed'}
    </Badge>
  </div>
  <Collapsible variant="card" bind:open={controlledOpen} title="Advanced filters">
    <p class="text-text-secondary text-sm">
      This panel is controlled via <code class="bg-surface-base rounded px-1.5 py-0.5 text-xs"
        >bind:open</code
      >. Toggle it with the button above or by clicking the trigger.
    </p>
  </Collapsible>
</div>

Release-notes item with custom trigger

Replace the default trigger via the trigger snippet to surface rich metadata (icons, badges, secondary text) while keeping the expand/collapse mechanics.

Features: New Collapsible component, improved Accordion internals, Stepper navigation.

Fixes: Dialog focus trap on Safari, Tooltip positioning near edges.

<div class="w-full max-w-lg">
  <Collapsible variant="card" defaultOpen>
    {#snippet trigger({
      open,
      toggle,
      triggerId,
      contentId
    }: {
      open: boolean;
      toggle: () => void;
      triggerId: string;
      contentId: string;
    })}
      <!-- The custom trigger fills the card's own top edge, so its hover
           fill has to carry the frame's radius — otherwise it squares off
           the rounded corners it sits in (invisible at the 2px default,
           obvious in a theme that rounds containers). Open: only the top
           two, the body continues the fill below. -->
      <button
        id={triggerId}
        type="button"
        onclick={toggle}
        aria-expanded={open}
        aria-controls={contentId}
        class="hover:bg-surface-hover rounded-t-contain focus-visible:ring-primary/50 flex w-full items-center gap-3 px-4 py-3 text-left transition-colors focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-inset"
      >
        <div
          class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg"
        >
          <ClipboardListIcon size={16} />
        </div>
        <div class="flex-1">
          <p class="text-text-primary text-sm font-semibold">Release Notes v3.2</p>
          <p class="text-text-tertiary text-xs">3 new features, 2 bug fixes</p>
        </div>
        <Badge size="xs" intent={open ? 'primary' : 'neutral'} variant="soft">
          {open ? 'Expanded' : 'Collapsed'}
        </Badge>
      </button>
    {/snippet}
    <div class="space-y-2 px-4 pb-4">
      <p class="text-text-secondary text-sm">
        <strong>Features:</strong> New Collapsible component, improved Accordion internals, Stepper
        navigation.
      </p>
      <p class="text-text-secondary text-sm">
        <strong>Fixes:</strong> Dialog focus trap on Safari, Tooltip positioning near edges.
      </p>
    </div>
  </Collapsible>
</div>

02 Without the layout

Collapsible wraps trigger and content in one element. When the revealed content has to be a sibling instead — the next row of the same grid, a panel in another column — useDisclosure gives you the same wiring on its own: the open state, one toggle, and the two attribute records to spread. Everything else, including the layout and the animation, is yours.

Detail row on the same grid

The hook takes the ids you generate and hands back triggerProps and contentProps. Reach for it only when the sibling layout is the point; inside one wrapper, Collapsible is less code.
Rebuild the deployment pipeline

Three stages, two of them cacheable. Because the detail is its own row, the trigger cell keeps its size and the table columns stay aligned down the whole list.

<script lang="ts">
  import { ChevronDownIcon, useDisclosure } from '@urbicon-ui/blocks';

  let open = $state(false);
  const propsId = $props.id();

  const detail = useDisclosure(() => ({
    open,
    triggerId: `entry-${propsId}-trigger`,
    contentId: `entry-${propsId}-detail`,
    onOpenChange: (next) => (open = next)
  }));
</script>

<div class="border-border-subtle grid w-full max-w-md grid-cols-[1fr_auto] gap-x-3 border-b py-2">
  <span class="text-text-primary self-center text-sm">Rebuild the deployment pipeline</span>

  <button
    {...detail.triggerProps}
    type="button"
    onclick={detail.toggle}
    class="text-text-tertiary hover:text-text-primary focus-visible:ring-primary/50 rounded-sm p-1 focus-visible:ring-2 focus-visible:outline-none"
  >
    <ChevronDownIcon size={16} class={detail.open ? 'rotate-180' : ''} />
    <span class="sr-only">{detail.open ? 'Hide' : 'Show'} details</span>
  </button>

  <!-- The revealed region is a SIBLING row spanning both columns, not a child
       of the trigger's cell. It stays mounted so the height can animate, which
       is what makes contentProps.inert load-bearing. -->
  <div
    {...detail.contentProps}
    class="col-span-2 grid overflow-hidden transition-[grid-template-rows] duration-[var(--blocks-collapse-duration)]"
    style:grid-template-rows={detail.open ? '1fr' : '0fr'}
  >
    <p class="text-text-secondary overflow-hidden text-sm">
      Three stages, two of them cacheable. Because the detail is its own row, the trigger cell keeps
      its size and the table columns stay aligned down the whole list.
    </p>
  </div>
</div>

03 Customization

Frosted glass

class tints the panel and slotClasses recolours the trigger, chevron and content. It keeps the card radius tier, padding and the expand animation. Only the fill, border, blur and text change, in raw colours because glass has no token equivalent.
Frosted surfaces read best over a photograph or gradient, where the blur lifts the panel off the busy background behind it.
<div class="w-full max-w-sm">
  <Collapsible
    variant="card"
    defaultOpen
    title="Frosted Glass"
    class="border-white/20 bg-white/10 shadow-[var(--blocks-shadow-lg)] backdrop-blur-xl"
    slotClasses={{
      trigger: 'text-white hover:text-white',
      chevron: 'text-white/60',
      contentInner: 'text-sm text-white/80'
    }}
  >
    Frosted surfaces read best over a photograph or gradient, where the blur lifts the panel
    off the busy background behind it.
  </Collapsible>
</div>

This is one of five ways to restyle a block. See Customization for class, slotClasses, unstyled, preset and provider-level overrides.

04 Accessibility

Built-in ARIA

The default trigger uses aria-expanded and aria-controls to link to the content panel. The content panel has role="region" with aria-labelledby pointing back to the trigger. The data-state attribute exposes open / closed for CSS-only styling.

Keyboard Navigation

Tab moves focus to the trigger. Enter / Space toggle the content. Focus rings use focus-visible: so they only appear on keyboard navigation.

Custom Triggers

When using the trigger snippet, the component passes triggerId and contentId so you can wire up aria-expanded and aria-controls yourself. The content region always gets the correct aria-labelledby.

Reduced Motion

The expand/collapse is tied to the --blocks-collapse-duration token. Under prefers-reduced-motion that token collapses to 1 ms, so the panel opens and closes without a visible slide.

05 API Reference

18 props
18 props 1 required
Prop
Type
Default
Description

06 Types

Local type definitions used by this component.

3 types
Name
Kind
Category
Used by
Description

07 Installation

Import

import { Collapsible } from '@urbicon-ui/blocks';