Corex.Accordion (Corex v0.1.0-rc.1)

View Source

Phoenix implementation of the Zag.js Accordion.

Anatomy

Minimal

<.accordion
class="accordion"
items={
  Corex.Content.new([
    %{label: "Lorem ipsum dolor sit amet", content: "Consectetur adipiscing elit."},
    %{label: "Duis dictum gravida odio ac pharetra?", content: "Nullam eget vestibulum ligula."},
    %{label: "Donec condimentum ex mi", content: "Congue molestie ipsum gravida a."}
  ])
}
/>

With slots

With items and <:indicator> slot so every item shares the same indicator markup.

<.accordion
class="accordion"
items={
  Corex.Content.new([
    %{label: "Lorem ipsum dolor sit amet", content: "Consectetur adipiscing elit."},
    %{label: "Duis dictum gravida odio ac pharetra?", content: "Nullam eget vestibulum ligula."},
    %{label: "Donec condimentum ex mi", content: "Congue molestie ipsum gravida a."}
  ])
}
>
<:indicator>
  <.heroicon name="hero-chevron-right" />
</:indicator>
</.accordion>

Custom slots

With items, customize each item using slots with :let={item} to access the item and its meta data

<.accordion
  class="accordion"
  value="lorem"
  items={
    Corex.Content.new([
      %{
        value: "lorem",
        label: "Lorem ipsum dolor sit amet",
        content: "Consectetur adipiscing elit. Sed sodales ullamcorper tristique.",
        meta: %{indicator: "hero-arrow-long-right", icon: "hero-chat-bubble-left-right"}
      },
      %{
        label: "Duis dictum gravida odio ac pharetra?",
        content: "Nullam eget vestibulum ligula, at interdum tellus.",
        meta: %{indicator: "hero-chevron-right", icon: "hero-device-phone-mobile"}
      },
      %{
        value: "donec",
        label: "Donec condimentum ex mi",
        content: "Congue molestie ipsum gravida a. Sed ac eros luctus.",
        disabled: true,
        meta: %{indicator: "hero-chevron-double-right", icon: "hero-phone"}
      }
    ])
  }
>
  <:trigger :let={item}>
    <.heroicon name={item.meta.icon} />{item.label}
  </:trigger>
  <:content :let={item}><p>{item.content}</p></:content>
  <:indicator :let={item}>
    <.heroicon name={item.meta.indicator} />
  </:indicator>
</.accordion>

Manual slots

With an empty items list, use multiple :trigger, :content, and optional :indicator slots.

Each slot takes a value string that ties the three together.

<.accordion class="accordion" value="lorem">
  <:trigger value="lorem">
    <.heroicon name="hero-chevron-right" /> Lorem ipsum dolor sit amet
  </:trigger>
  <:content value="lorem"><p>Consectetur adipiscing elit. Sed sodales ullamcorper tristique.</p></:content>
  <:indicator value="lorem">
    <.heroicon name="hero-chevron-down" />
  </:indicator>

  <:trigger value="duis">
    <.heroicon name="hero-chevron-right" /> Duis dictum gravida odio ac pharetra?
  </:trigger>
  <:content value="duis"><p>Nullam eget vestibulum ligula, at interdum tellus.</p></:content>
  <:indicator value="duis">
    <.heroicon name="hero-chevron-down" />
  </:indicator>
</.accordion>

Compound

Take full structural control with the accordion_root, accordion_item, accordion_trigger, accordion_content, and accordion_indicator sub-components.

Manual items

<.accordion :let={ctx} compound class="accordion">
  <.accordion_root ctx={ctx}>
    <.accordion_item :let={item} ctx={ctx} value="lorem">
      <.accordion_trigger item={item}>
        Lorem ipsum dolor sit amet
        <:indicator>
          <.accordion_indicator item={item}>
            <.heroicon name="hero-chevron-right" />
          </.accordion_indicator>
        </:indicator>
      </.accordion_trigger>
      <.accordion_content item={item}>
        <p>Consectetur adipiscing elit. Sed sodales ullamcorper tristique.</p>
      </.accordion_content>
    </.accordion_item>
    <.accordion_item :let={item} ctx={ctx} value="duis">
      <.accordion_trigger item={item}>
        Duis dictum gravida odio ac pharetra?
        <:indicator>
          <.accordion_indicator item={item}>
            <.heroicon name="hero-chevron-right" />
          </.accordion_indicator>
        </:indicator>
      </.accordion_trigger>
      <.accordion_content item={item}>
        <p>Nullam eget vestibulum ligula, at interdum tellus.</p>
      </.accordion_content>
    </.accordion_item>
    <.accordion_item :let={item} ctx={ctx} value="donec">
      <.accordion_trigger item={item}>
        Donec condimentum ex mi
        <:indicator>
          <.accordion_indicator item={item}>
            <.heroicon name="hero-chevron-right" />
          </.accordion_indicator>
        </:indicator>
      </.accordion_trigger>
      <.accordion_content item={item}>
        <p>Congue molestie ipsum gravida a. Sed ac eros luctus.</p>
      </.accordion_content>
    </.accordion_item>
  </.accordion_root>
</.accordion>

From a list

<.accordion :let={ctx} compound id="faq" class="accordion">
  <.accordion_root ctx={ctx}>
    <.accordion_item :for={entry <- @items} :let={item} ctx={ctx} value={entry.value}>
      <.accordion_trigger item={item}>
        {entry.label}
        <:indicator>
          <.accordion_indicator item={item}>
            <.heroicon name="hero-chevron-right" />
          </.accordion_indicator>
        </:indicator>
      </.accordion_trigger>
      <.accordion_content item={item}>
        <p>{entry.content}</p>
      </.accordion_content>
    </.accordion_item>
  </.accordion_root>
</.accordion>

API

Requires a stable id on <.accordion>.

FunctionActionReturns
set_value/2Set open items (client)%Phoenix.LiveView.JS{}
set_value/3Set open items (server)socket
value/2Read open items (client)%Phoenix.LiveView.JS{}
value/3Read open items (server)socket
focused/2Read focused item (client)%Phoenix.LiveView.JS{}
focused/3Read focused item (server)socket
item_state/3Read one item state (client)%Phoenix.LiveView.JS{}
item_state/4Read one item state (server)socket

Events

Pick an event name and pass it to on_* on <.accordion>.

Server events

EventWhenPayload
on_value_change="items_changed"Open items change%{"id" => id, "value" => values} — list of open item value strings
on_focus_change="focus_changed"Focused item changes%{"id" => id, "value" => value} — item value or nil

on_value_change

<.accordion
  id="faq"
  class="accordion"
  on_value_change="items_changed"
  items={
    Corex.Content.new([
      %{label: "Lorem ipsum dolor sit amet", content: "Consectetur adipiscing elit."},
      %{label: "Duis dictum gravida odio ac pharetra?", content: "Nullam eget vestibulum ligula."},
      %{label: "Donec condimentum ex mi", content: "Congue molestie ipsum gravida a."}
    ])
  }
>
  <:indicator><.heroicon name="hero-chevron-right" /></:indicator>
</.accordion>
def handle_event("items_changed", %{"id" => _id, "value" => values}, socket) do
  {:noreply, assign(socket, :open_items, values)}
end

on_focus_change

<.accordion
  id="faq"
  class="accordion"
  on_focus_change="focus_changed"
  items={
    Corex.Content.new([
      %{label: "Lorem ipsum dolor sit amet", content: "Consectetur adipiscing elit."},
      %{label: "Duis dictum gravida odio ac pharetra?", content: "Nullam eget vestibulum ligula."},
      %{label: "Donec condimentum ex mi", content: "Congue molestie ipsum gravida a."}
    ])
  }
>
  <:indicator><.heroicon name="hero-chevron-right" /></:indicator>
</.accordion>
def handle_event("focus_changed", %{"id" => _id, "value" => item}, socket) do
  {:noreply, assign(socket, :focused_item, item)}
end

Client events

EventWhenevent.detail
on_value_change_client="items-changed"Open items changeid, value, previousValue, added, removed
on_focus_change_client="focus-changed"Focused item changesid, value

on_value_change_client

<.accordion
  id="faq"
  class="accordion"
  on_value_change_client="items-changed"
  items={
    Corex.Content.new([
      %{label: "Lorem ipsum dolor sit amet", content: "Consectetur adipiscing elit."},
      %{label: "Duis dictum gravida odio ac pharetra?", content: "Nullam eget vestibulum ligula."},
      %{label: "Donec condimentum ex mi", content: "Congue molestie ipsum gravida a."}
    ])
  }
>
  <:indicator><.heroicon name="hero-chevron-right" /></:indicator>
</.accordion>
document.getElementById("faq")?.addEventListener("items-changed", (e) => {
  console.log(e.detail.value, e.detail.added, e.detail.removed);
});

on_focus_change_client

<.accordion
  id="faq"
  class="accordion"
  on_focus_change_client="focus-changed"
  items={
    Corex.Content.new([
      %{label: "Lorem ipsum dolor sit amet", content: "Consectetur adipiscing elit."},
      %{label: "Duis dictum gravida odio ac pharetra?", content: "Nullam eget vestibulum ligula."},
      %{label: "Donec condimentum ex mi", content: "Congue molestie ipsum gravida a."}
    ])
  }
>
  <:indicator><.heroicon name="hero-chevron-right" /></:indicator>
</.accordion>
document.getElementById("faq")?.addEventListener("focus-changed", (e) => {
  console.log(e.detail.value);
});

Patterns

Async

If items are not ready in mount/3—for example they load from the database or an external service—use assign_async/3, render inside <.async_result>, and put <.accordion_skeleton> in the :loading slot while the async assign is still pending.

defmodule MyAppWeb.AccordionAsyncLive do
  use MyAppWeb, :live_view

  def mount(_params, _session, socket) do
    socket =
      socket
      |> assign_async(:accordion, fn ->
        items =
          Corex.Content.new([
            %{
              value: "lorem",
              label: "Lorem ipsum dolor sit amet",
              content: "Consectetur adipiscing elit. Sed sodales ullamcorper tristique.",
              disabled: true
            },
            %{
              value: "duis",
              label: "Duis dictum gravida odio ac pharetra?",
              content: "Nullam eget vestibulum ligula, at interdum tellus."
            },
            %{
              value: "donec",
              label: "Donec condimentum ex mi",
              content: "Congue molestie ipsum gravida a. Sed ac eros luctus."
            }
          ])

        {:ok, %{accordion: %{items: items, value: ["duis", "donec"]}}}
      end)

    {:ok, socket}
  end

  def render(assigns) do
    ~H"""
    <.async_result :let={accordion} assign={@accordion}>
      <:loading>
        <.accordion_skeleton count={3} class="accordion" />
      </:loading>
      <:failed>Could not load accordion.</:failed>
      <.accordion
        id="async-accordion"
        class="accordion"
        items={accordion.items}
        value={accordion.value}
      />
    </.async_result>
    """
  end
end

Controlled

For server-owned open state—validation, forms, or rules that must run before items open—set controlled, bind value, and handle on_value_change in LiveView so assigns stay the source of truth.

defmodule MyAppWeb.AccordionLive do
  use MyAppWeb, :live_view

  def mount(_params, _session, socket) do
    {:ok, assign(socket, :accordion_value, ["lorem"])}
  end

  def handle_event("accordion_value_changed", %{"id" => _id, "value" => value}, socket) do
    {:noreply, assign(socket, :accordion_value, value)}
  end

  def render(assigns) do
    ~H"""
    <.accordion
      id="my-accordion"
      controlled
      value={@accordion_value}
      on_value_change="accordion_value_changed"
      class="accordion"
      items={
        Corex.Content.new([
          %{
            value: "lorem",
            label: "Lorem ipsum dolor sit amet",
            content: "Consectetur adipiscing elit."
          },
          %{
            value: "duis",
            label: "Duis dictum gravida odio ac pharetra?",
            content: "Nullam eget vestibulum ligula."
          }
        ])
      }
    />
    """
  end
end

Stream

Use Phoenix.LiveView.stream/3 to add or remove accordion items at runtime. Keep a list assign in sync with the stream and pass it as items. Configure dom_id to match each item element id (accordion:my-accordion:item:#{value}).

defmodule MyAppWeb.AccordionStreamLive do
  use MyAppWeb, :live_view

  @initial_items [
    %{value: "1", label: "Lorem ipsum", content: "Consectetur adipiscing elit."},
    %{value: "2", label: "Duis dictum", content: "Nullam eget vestibulum ligula."},
    %{value: "3", label: "Donec condimentum", content: "Congue molestie ipsum gravida a."}
  ]

  def mount(_params, _session, socket) do
    {:ok,
     socket
     |> stream_configure(:items, dom_id: &"accordion:my-accordion:item:#{&1.value}")
     |> stream(:items, @initial_items)
     |> assign(:items_list, @initial_items)
     |> assign(:next_id, 4)}
  end

  def handle_event("add_item", _params, socket) do
    id = to_string(socket.assigns.next_id)
    item = %{value: id, label: "Item #{id}", content: "Content for item #{id}."}

    {:noreply,
     socket
     |> stream_insert(:items, item)
     |> assign(:items_list, socket.assigns.items_list ++ [item])
     |> assign(:next_id, socket.assigns.next_id + 1)}
  end

  def render(assigns) do
    ~H"""
    <.accordion id="my-accordion" class="accordion" items={Corex.Content.new(@items_list)} />
    """
  end
end

Animation

JS

Built-in height and opacity (Web Animations API). Set animation_options with Corex.Animation.Height for duration, easing, and opacity.

<.accordion
  class="accordion"
  animation="js"
  animation_options={%Corex.Animation.Height{duration: 0.3, easing: "ease-out", opacity_start: 0, opacity_end: 1}}
  items={
    Corex.Content.new([
      %{
        label: "Lorem ipsum dolor sit amet",
        content: "Consectetur adipiscing elit. Sed sodales ullamcorper tristique."
      },
      %{
        label: "Duis dictum gravida odio ac pharetra?",
        content: "Nullam eget vestibulum ligula, at interdum tellus."
      },
      %{
        label: "Donec condimentum ex mi",
        content: "Congue molestie ipsum gravida a. Sed ac eros luctus."
      }
    ])
  }
>
  <:indicator>
    <.heroicon name="hero-chevron-right" />
  </:indicator>
</.accordion>

Instant

Items open and close immediately. Content visibility uses the native hidden attribute; there is no height animation.

<.accordion
  class="accordion"
  animation="instant"
  items={
    Corex.Content.new([
      %{
        label: "Lorem ipsum dolor sit amet",
        content: "Consectetur adipiscing elit. Sed sodales ullamcorper tristique."
      },
      %{
        label: "Duis dictum gravida odio ac pharetra?",
        content: "Nullam eget vestibulum ligula, at interdum tellus."
      },
      %{
        label: "Donec condimentum ex mi",
        content: "Congue molestie ipsum gravida a. Sed ac eros luctus."
      }
    ])
  }
>
  <:indicator>
    <.heroicon name="hero-chevron-right" />
  </:indicator>
</.accordion>

Custom (Motion)

Set animation="custom" and on_value_change_client to run Motion (or any JS) on open and close. Content stays in the DOM (hidden is not toggled). Each change fires a CustomEvent on the accordion with:

// event.detail  AccordionChangedDetail
{ id, value, previousValue, added, removed }

added and removed list which item values opened or closed so you can animate only those items. Register the listener after mount (and again after LiveView navigation if the DOM is replaced).

<.accordion
  class="accordion"
  animation="custom"
  on_value_change_client="my-accordion-changed"
  items={
    Corex.Content.new([
      %{
        label: "Lorem ipsum dolor sit amet",
        content: "Consectetur adipiscing elit. Sed sodales ullamcorper tristique."
      },
      %{
        label: "Duis dictum gravida odio ac pharetra?",
        content: "Nullam eget vestibulum ligula, at interdum tellus."
      },
      %{
        label: "Donec condimentum ex mi",
        content: "Congue molestie ipsum gravida a. Sed ac eros luctus."
      }
    ])
  }
>
  <:indicator>
    <.heroicon name="hero-chevron-right" />
  </:indicator>
</.accordion>
import { animate } from "motion"
import {
  findAccordionContent,
  animateHeightOpen,
  animateHeightClose,
} from "corex"

const reducedMotion = () =>
  window.matchMedia("(prefers-reduced-motion: reduce)").matches

document.addEventListener("my-accordion-changed", (e) => {
  const root = document.getElementById(e.detail.id)
  if (!root) return
  e.detail.added.forEach((v) => {
    const el = findAccordionContent(root, v)
    if (!el) return
    animateHeightOpen(el, { animator: animate, duration: 0.55, easing: [0.16, 1, 0.3, 1] })
    if (!reducedMotion()) {
      animate(
        el,
        { filter: ["blur(12px)", "blur(0px)"], scale: [0.96, 1] },
        { duration: 0.6, easing: [0.16, 1, 0.3, 1] },
      )
    }
  })
  e.detail.removed.forEach((v) => {
    const el = findAccordionContent(root, v)
    if (!el) return
    animateHeightClose(el, { animator: animate, duration: 0.32, easing: [0.7, 0, 0.84, 0] })
    if (!reducedMotion()) {
      animate(
        el,
        { filter: ["blur(0px)", "blur(10px)"], scale: [1, 0.97] },
        { duration: 0.3, easing: "ease-in" },
      )
    }
  })
})

Style

Target parts with data-scope and data-part, or use Corex Design: import tokens and accordion.css, then set class="accordion" on <.accordion>.

[data-scope="accordion"][data-part="root"] {}
[data-scope="accordion"][data-part="item"] {}
[data-scope="accordion"][data-part="item-trigger"] {}
[data-scope="accordion"][data-part="item-text"] {}
[data-scope="accordion"][data-part="item-content"] {}
[data-scope="accordion"][data-part="item-indicator"] {}
@import "../corex/main.css";
@import "../corex/tokens/themes/neo/light.css";
@import "../corex/components/accordion.css";

Stack modifiers on the host (class on <.accordion>). Combine axes, for example accordion accordion--accent accordion--lg.

Color

Semantic palette on the open item trigger (from design tokens).

ModifierClasses
Defaultaccordion
Accentaccordion accordion--accent
Brandaccordion accordion--brand
Alertaccordion accordion--alert
Infoaccordion accordion--info
Successaccordion accordion--success

Size

Trigger padding, gap, min-height, and content spacing.

ModifierClasses
Defaultaccordion
SMaccordion accordion--sm
MDaccordion accordion--md
LGaccordion accordion--lg
XLaccordion accordion--xl

Text

Font size on trigger and content.

ModifierClasses
Defaultaccordion
SMaccordion accordion--text-sm
XLaccordion accordion--text-xl
2XLaccordion accordion--text-2xl
4XLaccordion accordion--text-4xl

Rounded

Corner radius on trigger and content.

ModifierClasses
Defaultaccordion
Noneaccordion accordion--rounded-none
SMaccordion accordion--rounded-sm
MDaccordion accordion--rounded-md
LGaccordion accordion--rounded-lg
XLaccordion accordion--rounded-xl
Fullaccordion accordion--rounded-full

Max width

ModifierClasses
Defaultaccordion
Noneaccordion max-w-none
5XSaccordion max-w-5xs
2XSaccordion max-w-2xs
XSaccordion max-w-xs
SMaccordion max-w-sm
MDaccordion max-w-md
LGaccordion max-w-lg
XLaccordion max-w-xl
2XLaccordion max-w-2xl
5XLaccordion max-w-5xl

Summary

Components

Renders an accordion. See the module documentation for list-driven items, With slots, Custom slots, Manual and Compound modes, patterns, API, and events.

Renders a loading skeleton for the accordion component.

Compounds

Renders the content area for an accordion item.

Renders the indicator for an accordion item.

Renders an accordion item. Use inside accordion compound mode with :let={ctx}.

Renders the root container for an accordion in compound mode.

Renders the trigger button for an accordion item.

API

Read the focused item from phx-click. Dispatches corex:accordion:focused. Optional respond_to: :server (default), :client, or :both.

Read the focused item from handle_event (accordion_focused). Same replies as focused/2.

Read expanded, focused, and disabled state for one item from phx-click. Dispatches corex:accordion:item-state. Optional disabled: and respond_to: :server (default), :client, or :both.

Read item state from handle_event (accordion_item_state). Same replies as item_state/3.

Open or close items from phx-click. Pass a list (["lorem"]), a comma string ("lorem,donec"), or [] to close all.

Open or close items from handle_event. Pushes accordion_set_value (no reply event).

Read open items from phx-click. Dispatches corex:accordion:value. Optional respond_to: :server (default), :client, or :both.

Read open items from handle_event (accordion_value). Same replies as value/2.

Components

accordion(assigns)

Renders an accordion. See the module documentation for list-driven items, With slots, Custom slots, Manual and Compound modes, patterns, API, and events.

Attributes

  • id (:string) - DOM id on the accordion root. Used by set_value, value, focused, and item_state; auto-generated when omitted.

  • items (:list) - List of %Corex.Content.Item{} from Corex.Content.new/1. Defaults to [].

  • value (:any) - Initial or controlled open state: one string or a list of strings (value of each item). Defaults to [].

  • compound (:boolean) - Enable compound mode. Use with :let={ctx} and sub-components to fully control structure. Defaults to false.

  • controlled (:boolean) - When true, LiveView owns open items via value and on_value_change. Defaults to false.

  • collapsible (:boolean) - Whether the accordion is collapsible. Defaults to true.

  • multiple (:boolean) - Whether the accordion allows multiple items to be selected. Defaults to true.

  • animation (:string) - How items animate when opening or closing.

    • instant — toggle hidden immediately
    • js — built-in height and opacity (animation_options / Corex.Animation.Height)
    • custom — no built-in animation; use on_value_change_client with Motion or other JS

    Defaults to "js". Must be one of "instant", "js", or "custom".

  • animation_options (Corex.Animation.Height) - Used when animation is js. Ignored for instant and custom. See Corex.Animation.Height. Defaults to %Corex.Animation.Height{duration: 0.3, easing: "ease", opacity_start: 0.0, opacity_end: 1.0, block_interaction: false}.

  • orientation (:string) - The orientation of the accordion. Defaults to "vertical". Must be one of "horizontal", or "vertical".

  • dir (:string) - The direction of the accordion. When nil, derived from document (html lang + config :rtl_locales). Defaults to nil. Must be one of nil, "ltr", or "rtl".

  • on_value_change (:string) - LiveView event when open items change. Pick any event name.

    <.accordion
      id="faq"
      class="accordion"
      on_value_change="items_changed"
      items={
        Corex.Content.new([
          %{label: "Lorem ipsum dolor sit amet", content: "Consectetur adipiscing elit."},
          %{label: "Duis dictum gravida odio ac pharetra?", content: "Nullam eget vestibulum ligula."},
          %{label: "Donec condimentum ex mi", content: "Congue molestie ipsum gravida a."}
        ])
      }
    >
      <:indicator><.heroicon name="hero-chevron-right" /></:indicator>
    </.accordion>
    def handle_event("items_changed", %{"id" => _id, "value" => values}, socket) do
      {:noreply, assign(socket, :open_items, values)}
    end

    Defaults to nil.

  • on_value_change_client (:string) - Browser event on the accordion element when open items change (same moment as on_value_change).

    <.accordion
      id="faq"
      class="accordion"
      on_value_change_client="items-changed"
      items={
        Corex.Content.new([
          %{label: "Lorem ipsum dolor sit amet", content: "Consectetur adipiscing elit."},
          %{label: "Duis dictum gravida odio ac pharetra?", content: "Nullam eget vestibulum ligula."},
          %{label: "Donec condimentum ex mi", content: "Congue molestie ipsum gravida a."}
        ])
      }
    >
      <:indicator><.heroicon name="hero-chevron-right" /></:indicator>
    </.accordion>
    document.getElementById("faq")?.addEventListener("items-changed", (e) => {
      console.log(e.detail.value, e.detail.added, e.detail.removed);
    });

    Defaults to nil.

  • on_focus_change (:string) - LiveView event when keyboard focus moves to another item.

    <.accordion
      id="faq"
      class="accordion"
      on_focus_change="focus_changed"
      items={
        Corex.Content.new([
          %{label: "Lorem ipsum dolor sit amet", content: "Consectetur adipiscing elit."},
          %{label: "Duis dictum gravida odio ac pharetra?", content: "Nullam eget vestibulum ligula."},
          %{label: "Donec condimentum ex mi", content: "Congue molestie ipsum gravida a."}
        ])
      }
    >
      <:indicator><.heroicon name="hero-chevron-right" /></:indicator>
    </.accordion>
    def handle_event("focus_changed", %{"id" => _id, "value" => item}, socket) do
      {:noreply, assign(socket, :focused_item, item)}
    end

    Defaults to nil.

  • on_focus_change_client (:string) - Browser event on the accordion element when focus moves.

    <.accordion
      id="faq"
      class="accordion"
      on_focus_change_client="focus-changed"
      items={
        Corex.Content.new([
          %{label: "Lorem ipsum dolor sit amet", content: "Consectetur adipiscing elit."},
          %{label: "Duis dictum gravida odio ac pharetra?", content: "Nullam eget vestibulum ligula."},
          %{label: "Donec condimentum ex mi", content: "Congue molestie ipsum gravida a."}
        ])
      }
    >
      <:indicator><.heroicon name="hero-chevron-right" /></:indicator>
    </.accordion>
    document.getElementById("faq")?.addEventListener("focus-changed", (e) => {
      console.log(e.detail.value);
    });

    Defaults to nil.

  • Global attributes are accepted.

Slots

  • inner_block - Compound mode inner content. Use with the compound attribute and :let={ctx}. ctx is a map with keys: id, values, orientation, dir.

  • indicator - Optional slot after each trigger. With :items, use :let={item}. Without :items (manual mode), use one slot per item and a matching value on :trigger and :content. Accepts attributes:

    • value (:string)
    • class (:string)
  • trigger - With :items, optional custom trigger; use :let={item}. Without :items (manual mode), one slot per item with value (or default item-0, …). Accepts attributes:

    • value (:string)
    • class (:string)
    • disabled (:boolean)
  • content - With :items, optional custom content; use :let={item}. Without :items (manual mode), one slot per item with value (or default item-0, …). Accepts attributes:

    • value (:string)
    • class (:string)
    • disabled (:boolean)

accordion_skeleton(assigns)

Renders a loading skeleton for the accordion component.

Attributes

  • count (:integer) - Defaults to 3.
  • Global attributes are accepted.

Slots

  • trigger - Accepts attributes:
    • class (:string)
  • indicator - Accepts attributes:
    • class (:string)
  • content - Accepts attributes:
    • class (:string)

Compounds

accordion_content(assigns)

Renders the content area for an accordion item.

Use inside accordion_item with :let={item}, passing the yielded item as the item attr.

Attributes

  • item (:map) (required) - The item struct yielded by accordion_item via :let={item}.
  • animation (:string) - Override animation mode; defaults to the parent accordion animation from compound ctx. Defaults to nil.
  • Global attributes are accepted.

Slots

  • inner_block (required)

accordion_indicator(assigns)

Renders the indicator for an accordion item.

Use inside accordion_trigger inner block, passing the same item from accordion_item.

Attributes

  • item (:map) (required) - The item struct yielded by accordion_item via :let={item}.
  • Global attributes are accepted.

Slots

  • inner_block (required)

accordion_item(assigns)

Renders an accordion item. Use inside accordion compound mode with :let={ctx}.

Yields the %Item{} struct via :let for use in child parts.

Attributes

  • ctx (:map) (required) - The context map yielded by the parent accordion via :let={ctx}.
  • value (:string) (required) - The unique value identifying this item.
  • disabled (:boolean) - Whether the item is disabled. Defaults to false.
  • label (:string) - Visible item label for unique region names. Defaults to nil.
  • Global attributes are accepted.

Slots

  • inner_block

accordion_root(assigns)

Renders the root container for an accordion in compound mode.

Use inside accordion compound mode with :let={ctx}, wrapping all accordion_item components.

Attributes

  • ctx (:map) (required) - The context map yielded by the parent accordion via :let={ctx}.
  • Global attributes are accepted.

Slots

  • inner_block (required)

accordion_trigger(assigns)

Renders the trigger button for an accordion item.

Use inside accordion_item with :let={item}, passing the yielded item as the item attr. Place accordion_indicator inside this component's inner block if needed.

Attributes

  • item (:map) (required)
  • Global attributes are accepted.

Slots

  • inner_block (required)
  • indicator

API

focused(accordion_id, opts)

Read the focused item from phx-click. Dispatches corex:accordion:focused. Optional respond_to: :server (default), :client, or :both.

ReplyPayload
Serveraccordion_focused_response%{"id" => id, "value" => value}
Clientaccordion-focused on the accordiondetail: id, value
<.action phx-click={Corex.Accordion.focused("my-accordion", respond_to: :both)}>Focused item</.action>
<.accordion id="my-accordion" class="accordion" items={Corex.Content.new([
  %{value: "lorem", label: "Lorem", content: "Lorem body."},
  %{value: "duis", label: "Duis", content: "Duis body."},
  %{value: "donec", label: "Donec", content: "Donec body."}
])}><:indicator><.heroicon name="hero-chevron-right" /></:indicator></.accordion>
document.getElementById("my-accordion")?.dispatchEvent(
  new CustomEvent("corex:accordion:focused", {
    bubbles: false,
    detail: { respond_to: "both" }
  })
);
def handle_event("accordion_focused_response", %{"id" => _id, "value" => item}, socket) do
  {:noreply, assign(socket, :focused_item, item)}
end

focused(socket, accordion_id, opts)

Read the focused item from handle_event (accordion_focused). Same replies as focused/2.

ReplyPayload
accordion_focused_response%{"id" => id, "value" => value}
<.action phx-click="read_focus">Focused item</.action>
<.accordion id="my-accordion" class="accordion" items={Corex.Content.new([
  %{value: "lorem", label: "Lorem", content: "Lorem body."},
  %{value: "duis", label: "Duis", content: "Duis body."},
  %{value: "donec", label: "Donec", content: "Donec body."}
])}><:indicator><.heroicon name="hero-chevron-right" /></:indicator></.accordion>
def handle_event("read_focus", _params, socket) do
  {:noreply, Corex.Accordion.focused(socket, "my-accordion", respond_to: :server)}
end

def handle_event("accordion_focused_response", %{"id" => _id, "value" => item}, socket) do
  {:noreply, assign(socket, :focused_item, item)}
end

item_state(accordion_id, item_value, opts)

Read expanded, focused, and disabled state for one item from phx-click. Dispatches corex:accordion:item-state. Optional disabled: and respond_to: :server (default), :client, or :both.

ReplyPayload
Serveraccordion_item_state_response%{"id" => id, "value" => value, "state" => %{"expanded" => bool, "focused" => bool, "disabled" => bool}}
Clientaccordion-item-state on the accordiondetail: id, value, state
<.action phx-click={Corex.Accordion.item_state("my-accordion", "lorem", respond_to: :both)}>State for Lorem</.action>
<.accordion id="my-accordion" class="accordion" items={Corex.Content.new([
  %{value: "lorem", label: "Lorem", content: "Lorem body."},
  %{value: "duis", label: "Duis", content: "Duis body."},
  %{value: "donec", label: "Donec", content: "Donec body."}
])}><:indicator><.heroicon name="hero-chevron-right" /></:indicator></.accordion>
document.getElementById("my-accordion")?.dispatchEvent(
  new CustomEvent("corex:accordion:item-state", {
    bubbles: false,
    detail: { value: "lorem", respond_to: "both" }
  })
);
def handle_event("accordion_item_state_response", %{"id" => _id, "value" => item, "state" => state}, socket) do
  {:noreply, assign(socket, :item_state, {item, state})}
end

item_state(socket, accordion_id, item_value, opts)

Read item state from handle_event (accordion_item_state). Same replies as item_state/3.

ReplyPayload
accordion_item_state_response%{"id" => id, "value" => value, "state" => %{"expanded" => bool, "focused" => bool, "disabled" => bool}}
<.action phx-click="read_lorem">State for Lorem</.action>
<.accordion id="my-accordion" class="accordion" items={Corex.Content.new([
  %{value: "lorem", label: "Lorem", content: "Lorem body."},
  %{value: "duis", label: "Duis", content: "Duis body."},
  %{value: "donec", label: "Donec", content: "Donec body."}
])}><:indicator><.heroicon name="hero-chevron-right" /></:indicator></.accordion>
def handle_event("read_lorem", _params, socket) do
  {:noreply, Corex.Accordion.item_state(socket, "my-accordion", "lorem", respond_to: :server)}
end

def handle_event("accordion_item_state_response", %{"id" => _id, "value" => item, "state" => state}, socket) do
  {:noreply, assign(socket, :item_state, {item, state})}
end

set_value(accordion_id, value)

Open or close items from phx-click. Pass a list (["lorem"]), a comma string ("lorem,donec"), or [] to close all.

<.action phx-click={Corex.Accordion.set_value("my-accordion", "lorem")}>Open Lorem</.action>
<.action phx-click={Corex.Accordion.set_value("my-accordion", [])}>Close all</.action>
<.accordion id="my-accordion" class="accordion" items={Corex.Content.new([
  %{value: "lorem", label: "Lorem", content: "Lorem body."},
  %{value: "duis", label: "Duis", content: "Duis body."},
  %{value: "donec", label: "Donec", content: "Donec body."}
])}>
  <:indicator><.heroicon name="hero-chevron-right" /></:indicator>
</.accordion>
document.getElementById("my-accordion")?.dispatchEvent(
  new CustomEvent("corex:accordion:set-value", {
    bubbles: false,
    detail: { value: ["lorem"] }
  })
);

set_value(socket, accordion_id, value)

Open or close items from handle_event. Pushes accordion_set_value (no reply event).

<.action phx-click="open_lorem" phx-value-value="lorem">Open Lorem</.action>
<.accordion id="my-accordion" class="accordion" items={Corex.Content.new([
  %{value: "lorem", label: "Lorem", content: "Lorem body."},
  %{value: "duis", label: "Duis", content: "Duis body."},
  %{value: "donec", label: "Donec", content: "Donec body."}
])}><:indicator><.heroicon name="hero-chevron-right" /></:indicator></.accordion>
def handle_event("open_lorem", %{"value" => value}, socket) do
  {:noreply, Corex.Accordion.set_value(socket, "my-accordion", value)}
end

value(accordion_id, opts)

Read open items from phx-click. Dispatches corex:accordion:value. Optional respond_to: :server (default), :client, or :both.

ReplyPayload
Serveraccordion_value_response%{"id" => id, "value" => values}
Clientaccordion-value on the accordiondetail: id, value
<.action phx-click={Corex.Accordion.value("my-accordion", respond_to: :both)}>Which items are open?</.action>
<.accordion id="my-accordion" class="accordion" items={Corex.Content.new([
  %{value: "lorem", label: "Lorem", content: "Lorem body."},
  %{value: "duis", label: "Duis", content: "Duis body."},
  %{value: "donec", label: "Donec", content: "Donec body."}
])}><:indicator><.heroicon name="hero-chevron-right" /></:indicator></.accordion>
document.getElementById("my-accordion")?.dispatchEvent(
  new CustomEvent("corex:accordion:value", {
    bubbles: false,
    detail: { respond_to: "both" }
  })
);
def handle_event("accordion_value_response", %{"id" => _id, "value" => values}, socket) do
  {:noreply, assign(socket, :open_items, values)}
end

values is a list of open item value strings, or nil.

value(socket, accordion_id, opts)

Read open items from handle_event (accordion_value). Same replies as value/2.

ReplyPayload
accordion_value_response%{"id" => id, "value" => values}
<.action phx-click="read_items">Which items are open?</.action>
<.accordion id="my-accordion" class="accordion" items={Corex.Content.new([
  %{value: "lorem", label: "Lorem", content: "Lorem body."},
  %{value: "duis", label: "Duis", content: "Duis body."},
  %{value: "donec", label: "Donec", content: "Donec body."}
])}><:indicator><.heroicon name="hero-chevron-right" /></:indicator></.accordion>
def handle_event("read_items", _params, socket) do
  {:noreply, Corex.Accordion.value(socket, "my-accordion", respond_to: :server)}
end

def handle_event("accordion_value_response", %{"id" => _id, "value" => values}, socket) do
  {:noreply, assign(socket, :open_items, values)}
end

Functions

focused(accordion_id)

item_state(accordion_id, item_value)

value(accordion_id)