# `PhiaUi.Components.Tabs`
[🔗](https://github.com/charlenopires/PhiaUI/blob/v0.1.17/lib/phia_ui/components/navigation/tabs.ex#L1)

Accessible content-switching Tabs component — fully server-rendered.

`Tabs` provides accessible panel switching with a `tabs_list/1` trigger row
and `tabs_content/1` panels. The active/inactive state is controlled by the
`:default_value` attr and resolved entirely at render time — **no JavaScript
hook is required**.

## When to use Tabs vs TabsNav

| Component  | Use case                                                        |
|------------|-----------------------------------------------------------------|
| `Tabs`     | Content panels on a single page — switching hides/shows content |
| `TabsNav`  | Page-level navigation — each tab links to a different route     |

## Server-driven vs client-driven switching

### Static tabs (no server round-trip)

Pass a fixed `default_value` to show a specific tab on initial render.
For purely static content where tabs never change after mount:

    <.tabs default_value="overview">
      <.tabs_list>
        <.tabs_trigger value="overview">Overview</.tabs_trigger>
        <.tabs_trigger value="details">Details</.tabs_trigger>
      </.tabs_list>
      <.tabs_content value="overview"><p>Overview content</p></.tabs_content>
      <.tabs_content value="details"><p>Details content</p></.tabs_content>
    </.tabs>

### Server-driven switching (LiveView event)

Bind `phx-click` on triggers to update a LiveView assign, then pass it as
`default_value`. The server re-renders with the new active tab:

    <%!-- In the LiveView render function --%>
    <.tabs default_value={@active_tab}>
      <.tabs_list>
        <.tabs_trigger value="overview" phx-click="set_tab" phx-value-tab="overview">
          Overview
        </.tabs_trigger>
        <.tabs_trigger value="analytics" phx-click="set_tab" phx-value-tab="analytics">
          Analytics
        </.tabs_trigger>
      </.tabs_list>
      <.tabs_content value="overview">
        <%!-- Only rendered (not just hidden) when active --%>
        <.overview_panel metrics={@metrics} />
      </.tabs_content>
      <.tabs_content value="analytics">
        <.analytics_panel data={@analytics} />
      </.tabs_content>
    </.tabs>

    <%!-- In the LiveView module --%>
    def handle_event("set_tab", %{"tab" => tab}, socket) do
      {:noreply, assign(socket, active_tab: tab)}
    end

## Context propagation via :let

To avoid passing `default_value` / `active` manually to each trigger and
content panel, use `:let` to extract the context from `tabs/1`:

    <.tabs :let={ctx} default_value="settings">
      <.tabs_list>
        <%!-- Spread ctx into each trigger — it provides active={...} --%>
        <.tabs_trigger {ctx} value="settings">Settings</.tabs_trigger>
        <.tabs_trigger {ctx} value="billing">Billing</.tabs_trigger>
      </.tabs_list>
      <.tabs_content {ctx} value="settings">
        Settings panel
      </.tabs_content>
      <.tabs_content {ctx} value="billing">
        Billing panel
      </.tabs_content>
    </.tabs>

`tabs/1` calls `render_slot(@inner_block, %{active: @default_value})` and
sub-components receive this map via `:let`. Spreading `{ctx}` passes
`active=default_value` to the sub-component as an assign.

## WAI-ARIA implementation

| Element        | Role         | Key attributes                                |
|----------------|--------------|-----------------------------------------------|
| `tabs_list`    | `tablist`    | —                                             |
| `tabs_trigger` | `tab`        | `aria-selected`, `aria-controls`, `id`        |
| `tabs_content` | `tabpanel`   | `aria-labelledby`, `id`, hidden when inactive |

The `tabs_trigger` `id` is `"trigger-{value}"` and the `tabs_content` `id`
is `"panel-{value}"`. The cross-references (`aria-controls` / `aria-labelledby`)
connect triggers to their panels in the accessibility tree.

## Complete example — settings page

    defmodule MyAppWeb.SettingsLive do
      use MyAppWeb, :live_view

      def mount(_params, _session, socket) do
        {:ok, assign(socket, active_tab: "profile")}
      end

      def handle_event("set_tab", %{"tab" => tab}, socket) do
        {:noreply, assign(socket, active_tab: tab)}
      end

      def render(assigns) do
        ~H"""
        <div class="max-w-2xl mx-auto p-6">
          <h1 class="text-2xl font-semibold mb-6">Settings</h1>

          <.tabs default_value={@active_tab}>
            <.tabs_list class="w-full justify-start">
              <.tabs_trigger value="profile"
                phx-click="set_tab" phx-value-tab="profile"
                active={@active_tab}>
                Profile
              </.tabs_trigger>
              <.tabs_trigger value="security"
                phx-click="set_tab" phx-value-tab="security"
                active={@active_tab}>
                Security
              </.tabs_trigger>
              <.tabs_trigger value="notifications"
                phx-click="set_tab" phx-value-tab="notifications"
                active={@active_tab}>
                Notifications
              </.tabs_trigger>
            </.tabs_list>

            <.tabs_content value="profile" active={@active_tab} class="pt-4">
              <.profile_form user={@current_user} />
            </.tabs_content>
            <.tabs_content value="security" active={@active_tab} class="pt-4">
              <.security_settings />
            </.tabs_content>
            <.tabs_content value="notifications" active={@active_tab} class="pt-4">
              <.notification_preferences />
            </.tabs_content>
          </.tabs>
        </div>
        """
      end
    end

# `tabs`

Renders the tabs root wrapper and exposes active-tab context via `:let`.

`render_slot/2` is called with `%{active: @default_value}` as the context
value. Sub-components receive this via `:let` and use the `active` key to
determine whether they are the currently selected tab.

## Without :let (explicit active propagation)

    <.tabs default_value={@tab}>
      <.tabs_list>
        <.tabs_trigger value="a" active={@tab}>A</.tabs_trigger>
        <.tabs_trigger value="b" active={@tab}>B</.tabs_trigger>
      </.tabs_list>
      <.tabs_content value="a" active={@tab}>Panel A</.tabs_content>
      <.tabs_content value="b" active={@tab}>Panel B</.tabs_content>
    </.tabs>

## With :let (automatic context propagation)

    <.tabs :let={ctx} default_value="a">
      <.tabs_list>
        <.tabs_trigger {ctx} value="a">A</.tabs_trigger>
        <.tabs_trigger {ctx} value="b">B</.tabs_trigger>
      </.tabs_list>
      <.tabs_content {ctx} value="a">Panel A</.tabs_content>
      <.tabs_content {ctx} value="b">Panel B</.tabs_content>
    </.tabs>

## Attributes

* `default_value` (`:string`) (required) - Value of the tab that should be active on render. Must exactly match one
  of the `value` attrs of the `tabs_trigger/1` / `tabs_content/1` children.
  Drive this from a LiveView assign for server-side tab switching:
  `default_value={@active_tab}`.

* `variant` (`:atom`) - Visual style of the tabs. `:underline` (default) renders a bottom-border
  indicator; `:solid` renders the shadcn segmented-control look with `bg-muted`
  track; `:pill` renders rounded-full pill triggers; `:scrollable` renders a
  horizontally scrollable underline tab bar for many tabs.

  Defaults to `:underline`. Must be one of `:underline`, `:solid`, `:pill`, or `:scrollable`.
* `stacked_mobile` (`:boolean`) - When `true`, adds `flex-col sm:flex-row` to the tabs list container so
  that tab triggers stack vertically on mobile and return to horizontal
  layout on `sm:` screens and wider. Useful when there are many tabs that
  would overflow horizontally on small viewports.

  Defaults to `false`.
* `class` (`:string`) - Additional CSS classes applied to the outer wrapper `<div>`. Defaults to `nil`.
* Global attributes are accepted. HTML attributes forwarded to the wrapper div.
## Slots

* `inner_block` (required) - Tab structure: `tabs_list/1` followed by one or more `tabs_content/1` panels.
  When using `:let={ctx}`, spread `{ctx}` on each `tabs_trigger/1` and
  `tabs_content/1` to propagate the active tab value automatically.

# `tabs_content`

Renders a tab content panel (`role="tabpanel"`).

The panel is shown when `active == value` and hidden otherwise via Tailwind's
`hidden` class. Content is always rendered in the HTML (for SEO and ARIA
discoverability), but invisible when not active.

If you want to avoid rendering inactive content at all (e.g. for expensive
queries), conditionally render panels in your LiveView template instead:

    <%= if @active_tab == "analytics" do %>
      <.tabs_content value="analytics" active={@active_tab}>
        <.heavy_analytics_chart />
      </.tabs_content>
    <% end %>

## ARIA connections

- `id="panel-{value}"` — referenced by `aria-controls="panel-{value}"` on the trigger
- `aria-labelledby="trigger-{value}"` — references the trigger button's `id`

## Example

    <.tabs_content value="billing" active={@active_tab} class="pt-4">
      <.billing_form subscription={@subscription} />
    </.tabs_content>

## Attributes

* `value` (`:string`) (required) - Unique string matching the corresponding `tabs_trigger/1` value. The panel
  is visible when `active == value` and hidden (via `hidden` class) otherwise.

* `active` (`:string`) - The currently active tab value. Set to the same assign as `tabs/1`'s
  `default_value`. When using `:let={ctx}` on `tabs/1`, this is populated
  automatically via the spread `{ctx}`.

  Defaults to `nil`.
* `class` (`:string`) - Additional CSS classes applied to the panel div (e.g. `"pt-4"`). Defaults to `nil`.
* Global attributes are accepted. HTML attributes forwarded to the panel div.
## Slots

* `inner_block` (required) - Panel content — rendered only when the tab is active.

# `tabs_list`

Renders the tab trigger list container (`role="tablist"`).

The list uses `inline-flex` to size itself to its content by default.
Override with `class="w-full"` for a full-width tab bar. The `bg-muted`
background and `p-1` padding create the segmented-control look used by
shadcn/ui tabs.

## Example

    <.tabs_list>
      <.tabs_trigger value="tab1">Tab 1</.tabs_trigger>
      <.tabs_trigger value="tab2">Tab 2</.tabs_trigger>
    </.tabs_list>

    <%!-- Full-width, left-aligned tabs --%>
    <.tabs_list class="w-full justify-start border-b bg-transparent rounded-none p-0">
      ...
    </.tabs_list>

## Attributes

* `variant` (`:atom`) - Visual style — must match the `variant` on the parent `tabs/1`. Defaults to `:underline`. Must be one of `:underline`, `:solid`, `:pill`, or `:scrollable`.
* `stacked_mobile` (`:boolean`) - When `true`, adds `flex-col sm:flex-row` to make tab triggers stack
  vertically on mobile and arrange horizontally on `sm:` screens and wider.
  Propagated automatically when using `:let` context from `tabs/1`.

  Defaults to `false`.
* `class` (`:string`) - Additional CSS classes for the tab list container. Use `"w-full"` to
  stretch the list across its parent width, or `"justify-start"` to
  left-align tabs instead of center-aligning them.

  Defaults to `nil`.
* Global attributes are accepted. HTML attributes forwarded to the tablist div.
## Slots

* `inner_block` (required) - One or more `tabs_trigger/1` components.

# `tabs_trigger`

Renders a single tab trigger button.

The trigger compares `active` to `value` to determine its selected state:

- **Active**: `bg-background text-foreground shadow-sm` — visually elevated
- **Inactive**: `hover:bg-background/50` — subtle hover, muted text

The button renders with:
- `role="tab"` for screen readers
- `id="trigger-{value}"` for `aria-labelledby` cross-reference
- `aria-selected="true|false"` to announce current state
- `aria-controls="panel-{value}"` linking to the content panel

## Example

    <.tabs_trigger value="overview" active={@active_tab}
                   phx-click="set_tab" phx-value-tab="overview">
      Overview
    </.tabs_trigger>

## Attributes

* `value` (`:string`) (required) - Unique string identifying this tab trigger. Must match the `value` of the
  corresponding `tabs_content/1` panel and the `default_value` on `tabs/1`
  when this tab should be active initially.

* `variant` (`:atom`) - Visual style — must match the `variant` on the parent `tabs/1`. Defaults to `:underline`. Must be one of `:underline`, `:solid`, `:pill`, or `:scrollable`.
* `active` (`:string`) - The currently active tab value. Set to the same assign as `tabs/1`'s
  `default_value`. When `active == value`, this trigger renders in its
  selected state. When using `:let={ctx}` on `tabs/1`, this is populated
  automatically.

  Defaults to `nil`.
* `disabled` (`:boolean`) - When `true`, the trigger is non-interactive: `pointer-events-none opacity-50`.
  Useful for premium features not available on the current plan.

  Defaults to `false`.
* `class` (`:string`) - Additional CSS classes applied to the trigger button. Defaults to `nil`.
* Global attributes are accepted. HTML attributes forwarded to the button. Use `phx-click` and
  `phx-value-tab` to drive server-side tab switching:

      <.tabs_trigger value="settings" phx-click="set_tab" phx-value-tab="settings">
        Settings
      </.tabs_trigger>
 Supports all globals plus: `["phx-click", "phx-value", "phx-value-tab"]`.
## Slots

* `inner_block` (required) - Trigger label — text, icon, or both.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
