PhiaUi.Components.Collapsible (phia_ui v0.1.17)

Copy Markdown View Source

Collapsible section component using exclusively Phoenix.LiveView.JS — no JS hooks required.

A lightweight show/hide container with an accessible trigger button and a content panel. Unlike Accordion, Collapsible is a standalone primitive for a single expandable section — not a list. Use it for things like advanced filter panels, inline help text, expandable code blocks, or "show more" UI.

All interactivity is expressed as Phoenix.LiveView.JS commands compiled directly into the phx-click attribute, so transitions execute without a server round-trip.

Sub-components

FunctionElementPurpose
collapsible/1divRoot container — requires a unique :id
collapsible_trigger/1buttonToggle button with aria-expanded/aria-controls
collapsible_content/1divThe show/hide panel

Example — Closed by Default

The simplest case: a section that starts collapsed. A user clicks the trigger to reveal the content.

<.collapsible id="advanced-options">
  <.collapsible_trigger collapsible_id="advanced-options" open={false}>
    <span>Advanced Options</span>
    <.icon name="chevron-down" />
  </.collapsible_trigger>
  <.collapsible_content id="advanced-options-content" open={false}>
    <.input name="timeout" label="Request timeout (ms)" value="5000" />
    <.input name="retries" label="Max retries" value="3" />
  </.collapsible_content>
</.collapsible>

Example — Open by Default

Pass open={true} to start the panel expanded. Useful for sections that show important content that should be immediately visible.

<.collapsible id="current-filters" open={true}>
  <.collapsible_trigger collapsible_id="current-filters" open={true}>
    Active Filters (3)
  </.collapsible_trigger>
  <.collapsible_content id="current-filters-content" open={true}>
    <.badge :for={f <- @active_filters}>{f.label}</.badge>
  </.collapsible_content>
</.collapsible>

Example — Server-Controlled State

When you need the server to control expand/collapse (e.g. after a form submission reveals an error section), pass a LiveView assign to all three :open attrs. The server can push a re-render to change the state:

# LiveView
def handle_event("submit", params, socket) do
  case validate(params) do
    {:error, _} -> {:noreply, assign(socket, :show_errors, true)}
    {:ok, _}    -> {:noreply, assign(socket, :show_errors, false)}
  end
end

# Template
<.collapsible id="error-panel" open={@show_errors}>
  <.collapsible_trigger collapsible_id="error-panel" open={@show_errors}>
    Validation Errors
  </.collapsible_trigger>
  <.collapsible_content id="error-panel-content" open={@show_errors}>
    <.alert variant="destructive">Fix the issues below.</.alert>
  </.collapsible_content>
</.collapsible>

Example — Inline Help Text

A compact pattern for revealing contextual help without navigating away:

<.collapsible id="help-webhook">
  <.collapsible_trigger collapsible_id="help-webhook" open={false}
    class="text-sm text-muted-foreground hover:text-foreground">
    What is a webhook URL?
  </.collapsible_trigger>
  <.collapsible_content id="help-webhook-content" open={false}>
    <p class="text-sm text-muted-foreground mt-2">
      A webhook URL is an HTTP endpoint that receives real-time event
      notifications from our system. See the docs for details.
    </p>
  </.collapsible_content>
</.collapsible>

Coordination Between Sub-components

All three sub-components must agree on the :open value to keep the rendered HTML consistent with the JS.toggle client state:

After the first client-side toggle, the JS manages visibility without the server needing to know — unless you push a re-render.

Accessibility

  • Trigger has aria-expanded (set from :open) and aria-controls pointing at the content panel ID
  • Screen readers announce the control relationship and current state
  • Keyboard: the trigger is a <button> — naturally focusable and activatable with Enter or Space

Summary

Functions

Renders the collapsible root container.

Renders the collapsible content panel.

Renders the collapsible trigger button.

Functions

collapsible(assigns)

Renders the collapsible root container.

The :id is the coordination point between the three sub-components. collapsible_trigger/1 uses it to build aria-controls and the JS.toggle target (#{id}-content). collapsible_content/1 must be given the matching ID (id="{collapsible_id}-content").

Attributes

  • id (:string) (required) - Unique ID for the root container. The trigger and content sub-components derive their IDs from this value, so it must be unique on the page. collapsible_trigger/1 receives this as :collapsible_id. collapsible_content/1 must be given id="{this-id}-content".

  • open (:boolean) - Initial open state — only used for container-level styling; see trigger and content. Defaults to false.

  • class (:string) - Additional CSS classes for the root <div>. Defaults to nil.

  • Global attributes are accepted. Extra HTML attributes forwarded to the root <div>.

Slots

collapsible_content(assigns)

Renders the collapsible content panel.

Hidden by default via style="display: none;" when open={false}. Using an inline style rather than a Tailwind class is intentional — JS.toggle relies on toggling the element's display style property, so a CSS class like hidden would conflict if it sets !important.

The overflow-hidden class prevents content from bleeding outside the bounds when the panel is partially hidden during future CSS transition enhancements.

The ID must match "{collapsible_id}-content" exactly for the trigger's JS.toggle and aria-controls to work correctly.

Attributes

  • id (:string) (required) - ID of the content panel. Must follow the pattern "{collapsible_id}-content" because collapsible_trigger/1 targets this exact ID in its JS.toggle command and builds aria-controls from it.

  • open (:boolean) - Initial visibility state. When false (default), renders style="display: none;". When true, renders without the style override so the element is visible. After the first client-side toggle the JS manages visibility — this only affects the server-rendered initial HTML.

    Defaults to false.

  • class (:string) - Additional CSS classes for the content panel. Defaults to nil.

  • Global attributes are accepted. Extra HTML attributes forwarded to the content <div>.

Slots

  • inner_block (required) - Content shown when the collapsible is open.

collapsible_trigger(assigns)

Renders the collapsible trigger button.

On click, fires Phoenix.LiveView.JS.toggle/1 targeting the content panel identified by collapsible_id <> "-content". This executes client-side without a server round-trip.

The trigger is a semantic <button type="button"> so it is keyboard accessible out of the box: Enter and Space both fire the click handler.

The aria-expanded attribute reflects the current open state so screen readers announce "expanded" or "collapsed" appropriately.

Attributes

  • collapsible_id (:string) (required) - ID of the parent collapsible/1. Used to:

    1. Build aria-controls="{collapsible_id}-content" for ARIA linkage
    2. Build the JS.toggle target "#{collapsible_id}-content" for the click handler
  • open (:boolean) - Current open state. Controls the aria-expanded attribute on the button. Should match the :open value passed to collapsible_content/1 so the ARIA state and visual state agree on initial render.

    Defaults to false.

  • class (:string) - Additional CSS classes for the trigger button. Defaults to nil.

  • Global attributes are accepted. Extra HTML attributes forwarded to the <button>.

Slots

  • inner_block (required) - Trigger button content — text, icon, or both.