Accordion

Vertical stack of collapsible sections with single or multiple open panels.

Basic

.heex
<.accordion>
  <.accordion_item>
    <:header>What is the refund policy?</:header>
    <:panel>
      Request a refund within 15 days of purchase if the product does not meet your needs.
    </:panel>
  </.accordion_item>

  <.accordion_item>
    <:header>Can I upgrade my license later?</:header>
    <:panel>
      Yes. Upgrade at any time and pay only the difference between your current and target tier.
    </:panel>
  </.accordion_item>

  <.accordion_item>
    <:header>Do you offer technical support?</:header>
    <:panel>
      Email support is included with every plan. Priority response is included with Pro.
    </:panel>
  </.accordion_item>
</.accordion>

Default expanded item

All data is encrypted at rest and in transit using AES-256 and TLS 1.3.
.heex
<.accordion>
  <.accordion_item expanded>
    <:header>How is my data stored?</:header>
    <:panel>
      All data is encrypted at rest and in transit using AES-256 and TLS 1.3.
    </:panel>
  </.accordion_item>

  <.accordion_item>
    <:header>Can I export my data?</:header>
    <:panel>
      Yes. Export everything to JSON or CSV from Settings, Account, Export.
    </:panel>
  </.accordion_item>

  <.accordion_item>
    <:header>Where do I report a bug?</:header>
    <:panel>
      Use the support widget in the bottom-right corner of any page.
    </:panel>
  </.accordion_item>
</.accordion>

Multiple sections open

Use multiple to keep several panels open at once.

Free standard shipping on orders over $50. Two-day shipping is $9.95.
Free returns within 30 days. Pre-paid label included with every order.
.heex
<.accordion multiple>
  <.accordion_item expanded>
    <:header>Shipping</:header>
    <:panel>Free standard shipping on orders over $50.</:panel>
  </.accordion_item>

  <.accordion_item expanded>
    <:header>Returns</:header>
    <:panel>Free returns within 30 days.</:panel>
  </.accordion_item>

  <.accordion_item>
    <:header>Warranty</:header>
    <:panel>Two-year limited warranty against manufacturing defects.</:panel>
  </.accordion_item>
</.accordion>

Always one open

prevent_all_closed refuses to collapse the last open item, so the panel never goes empty. Pair with an initial expanded item.

Try to collapse this item while no other is open. The accordion keeps it expanded.
.heex
<.accordion prevent_all_closed>
  <.accordion_item expanded>
    <:header>Overview</:header>
    <:panel>Try to collapse this item; it stays open.</:panel>
  </.accordion_item>

  <.accordion_item>
    <:header>Details</:header>
    <:panel>Opening another item lets the previous one close.</:panel>
  </.accordion_item>

  <.accordion_item>
    <:header>Notes</:header>
    <:panel>At least one item is always expanded.</:panel>
  </.accordion_item>
</.accordion>

Disabled item

Disabled items don't toggle on click and are skipped by arrow-key navigation.

.heex
<.accordion>
  <.accordion_item>
    <:header>Free plan</:header>
    <:panel>Get started with the basics.</:panel>
  </.accordion_item>

  <.accordion_item>
    <:header>Pro plan</:header>
    <:panel>Unlock advanced features and priority support.</:panel>
  </.accordion_item>

  <.accordion_item disabled>
    <:header>Enterprise plan (contact sales)</:header>
    <:panel>Custom contracts and dedicated support.</:panel>
  </.accordion_item>
</.accordion>

Headers with icons and badges

The :header slot accepts any inline content, including icons and badges.

Install the package, run the generator, and you are ready to ship in under five minutes.
.heex
<.accordion multiple>
  <.accordion_item expanded>
    <:header>
      <span class="flex items-center gap-2">
        <.icon name="hero-rocket-launch" class="size-5 text-primary" />
        <span>Getting started</span>
        <.badge color="success" size="xs">New</.badge>
      </span>
    </:header>
    <:panel>
      Install the package, run the generator, and you are ready to ship in under five minutes.
    </:panel>
  </.accordion_item>

  <.accordion_item>
    <:header>
      <span class="flex items-center gap-2">
        <.icon name="hero-shield-check" class="size-5 text-info" />
        <span>Security</span>
      </span>
    </:header>
    <:panel>
      SOC 2 Type II audited. SSO via SAML and OIDC available on all paid plans.
    </:panel>
  </.accordion_item>

  <.accordion_item>
    <:header>
      <span class="flex items-center gap-2">
        <.icon name="hero-credit-card" class="size-5 text-foreground-softer" />
        <span>Billing</span>
        <.badge color="warning" size="xs">Action needed</.badge>
      </span>
    </:header>
    <:panel>
      Update your payment method before April 30 to avoid an interruption in service.
    </:panel>
  </.accordion_item>
</.accordion>

Custom indicator

Replace the default chevron by passing an :indicator slot. Toggle visuals with the group-data-expanded/accordion-item Tailwind variant.

.heex
<.accordion>
  <.accordion_item>
    <:header>What is included in the free trial?</:header>
    <:indicator>
      <.icon
        name="hero-plus"
        class="size-4 text-foreground-softer group-data-expanded/accordion-item:hidden"
      />
      <.icon
        name="hero-minus"
        class="hidden size-4 text-foreground-softer group-data-expanded/accordion-item:block"
      />
    </:indicator>
    <:panel>
      Full access to every feature for 14 days. No credit card required.
    </:panel>
  </.accordion_item>
</.accordion>

No indicator

Pass an empty :indicator slot to suppress the chevron entirely.

Without a chevron the header reads as a plain link. Reserve this for short, list-like sections where the trigger label already implies expansion.
.heex
<.accordion>
  <.accordion_item expanded>
    <:header>Read more</:header>
    <:indicator></:indicator>
    <:panel>
      The header reads as a plain link without a chevron.
    </:panel>
  </.accordion_item>
</.accordion>

Card style

.heex
<.accordion class="space-y-3 divide-y-0">
  <.accordion_item class="rounded-xl border border-base bg-surface px-5">
    <:header class="py-4">What is the refund policy?</:header>
    <:panel class="pr-0 pb-4">
      Request a refund within 15 days of purchase.
    </:panel>
  </.accordion_item>

  <.accordion_item class="rounded-xl border border-base bg-surface px-5">
    <:header class="py-4">Can I upgrade my license later?</:header>
    <:panel class="pr-0 pb-4">
      Yes. Pay only the difference between tiers.
    </:panel>
  </.accordion_item>

  <.accordion_item class="rounded-xl border border-base bg-surface px-5">
    <:header class="py-4">Do you offer technical support?</:header>
    <:panel class="pr-0 pb-4">
      Email support is included with every plan.
    </:panel>
  </.accordion_item>
</.accordion>

Settings group

Combine multiple with per-item class overrides to group form controls into one panel.

.heex
<.accordion multiple class="rounded-xl border border-base bg-surface">
  <.accordion_item class="px-5">
    <:header class="py-4">
      <span class="flex items-center gap-2">
        <.icon name="hero-bell" class="size-5 text-foreground-softer" />
        <span>Notifications</span>
      </span>
    </:header>
    <:panel class="space-y-3 pr-0 pb-4">
      <.switch name="notif_email" label="Email" checked />
      <.switch name="notif_push" label="Push" />
      <.switch name="notif_digest" label="Weekly digest" checked />
    </:panel>
  </.accordion_item>

  <.accordion_item class="px-5">
    <:header class="py-4">
      <span class="flex items-center gap-2">
        <.icon name="hero-lock-closed" class="size-5 text-foreground-softer" />
        <span>Privacy</span>
      </span>
    </:header>
    <:panel class="space-y-3 pr-0 pb-4">
      <.switch name="public_profile" label="Public profile" />
      <.switch name="show_activity" label="Show activity" checked />
    </:panel>
  </.accordion_item>

  <.accordion_item class="px-5">
    <:header class="py-4">
      <span class="flex items-center gap-2">
        <.icon name="hero-link" class="size-5 text-foreground-softer" />
        <span>Integrations</span>
      </span>
    </:header>
    <:panel class="pr-0 pb-4">
      <.button variant="outline" size="sm">Connect GitHub</.button>
    </:panel>
  </.accordion_item>
</.accordion>

Setup checklist

Several accordions stacked into a longer flow, each one a section.

Workspace setup

Add teammates by email or share an invite link. New members join as Editors and can be promoted any time.

Make it yours

.heex
<section class="space-y-3">
  <h3 class="text-sm font-semibold text-foreground">Workspace setup</h3>

  <.accordion class="rounded-xl border border-base bg-surface">
    <.accordion_item class="px-5">
      <:header class="py-4">
        <span class="flex flex-1 items-center gap-3">
          <.icon name="hero-check-circle-solid" class="size-5 text-success" />
          <span class="font-medium">Verify your email</span>
          <.badge color="success" class="ml-auto">Ready</.badge>
        </span>
      </:header>
      <:panel class="pr-0 pb-4">
        We sent a confirmation link to your inbox.
      </:panel>
    </.accordion_item>

    <.accordion_item expanded class="px-5">
      <:header class="py-4">
        <span class="flex items-center gap-3">
          <.icon name="hero-arrow-path" class="size-5 text-warning" />
          <span class="font-medium">Invite your teammates</span>
        </span>
      </:header>
      <:panel class="pr-0 pb-4">
        <div class="space-y-3">
          <p class="text-foreground-soft">
            Add teammates by email or share an invite link.
          </p>
          <.input name="invite_email" type="email" placeholder="alex@acme.co">
            <:outer_suffix>
              <.button variant="outline">Send invite</.button>
            </:outer_suffix>
          </.input>
        </div>
      </:panel>
    </.accordion_item>

    <.accordion_item class="px-5">
      <:header class="py-4">
        <span class="flex items-center gap-3">
          <.icon name="hero-ellipsis-horizontal-circle" class="size-5 text-foreground-softer" />
          <span class="font-medium">Choose your plan</span>
        </span>
      </:header>
      <:panel class="pr-0 pb-4">
        Pick a plan before your trial ends.
      </:panel>
    </.accordion_item>
  </.accordion>
</section>

<section class="space-y-3">
  <h3 class="text-sm font-semibold text-foreground">Make it yours</h3>

  <.accordion class="rounded-xl border border-base bg-surface">
    <.accordion_item class="px-5">
      <:header class="py-4">
        <span class="flex items-center gap-3">
          <.icon name="hero-ellipsis-horizontal-circle" class="size-5 text-foreground-softer" />
          <span class="font-medium">Upload your team logo</span>
        </span>
      </:header>
      <:panel class="pr-0 pb-4">SVG or PNG, at least 256×256.</:panel>
    </.accordion_item>

    <.accordion_item class="px-5">
      <:header class="py-4">
        <span class="flex items-center gap-3">
          <.icon name="hero-ellipsis-horizontal-circle" class="size-5 text-foreground-softer" />
          <span class="font-medium">Connect a custom domain</span>
        </span>
      </:header>
      <:panel class="pr-0 pb-4">Point a CNAME at app.flux.host.</:panel>
    </.accordion_item>
  </.accordion>
</section>

Server-driven state

expanded is re-read on every LiveView patch, so binding it to an assign makes the server the source of truth.

Sign up with email or continue with Google. Confirmation lands in your inbox immediately.
.heex
<.button_group>
  <.button
    :for={step <- @steps}
    phx-click={JS.push("focus_step", value: %{id: step.id})}
    data-active={@current_step == step.id || nil}
    class="data-active:highlight-strong data-active:font-medium"
  >
    Step {step.number}
  </.button>
</.button_group>

<.accordion id="onboarding">
  <.accordion_item
    :for={step <- @steps}
    id={"onboarding-#{step.id}"}
    expanded={@current_step == step.id}
  >
    <:header phx-click={JS.push("focus_step", value: %{id: step.id})}>
      Step {step.number}: {step.title}
    </:header>
    <:panel>{step.body}</:panel>
  </.accordion_item>
</.accordion>
.ex
def handle_event("focus_step", %{"id" => _id}, socket) do
  # Update the assign that drives `expanded` on each accordion_item
  {:noreply, socket}
end