Horizontal tab navigation component for page-level and section-level routing.
TabsNav renders accessible tab-style navigation using <a> links rather
than <button> elements, making it suitable for route-based navigation where
each tab corresponds to a different URL or live action.
When to use TabsNav vs Tabs
| Component | Use case |
|---|---|
TabsNav | Route-based navigation — tabs link to different pages/actions |
Tabs | Content panels — tabs switch content on the same page |
Use TabsNav with @live_action to highlight the current section:
<.tabs_nav>
<.tabs_nav_item href="/dashboard" active={@live_action == :index}>
Dashboard
</.tabs_nav_item>
<.tabs_nav_item href="/analytics" active={@live_action == :analytics}>
Analytics
</.tabs_nav_item>
</.tabs_nav>Visual variants
Three visual styles are available via the :variant attribute:
| Variant | Appearance | Best for |
|---|---|---|
:underline | Bottom border indicator on active tab (default) | Standard page navigation |
:pills | Filled primary background on active tab, rounded | Compact section switching |
:segment | Segmented control inside a muted container, no border | Dense dashboards |
Underline variant (default)
<.tabs_nav>
<.tabs_nav_item href="/overview" active>Overview</.tabs_nav_item>
<.tabs_nav_item href="/analytics">Analytics</.tabs_nav_item>
<.tabs_nav_item href="/reports">Reports</.tabs_nav_item>
</.tabs_nav>Pills variant
<.tabs_nav variant={:pills}>
<.tabs_nav_item href="#overview" variant={:pills} active>Overview</.tabs_nav_item>
<.tabs_nav_item href="#analytics" variant={:pills}>Analytics</.tabs_nav_item>
</.tabs_nav>Segment variant
The segment variant wraps items inside a bg-muted container with p-1,
creating the look of a segmented control (similar to iOS segment pickers):
<.tabs_nav variant={:segment}>
<.tabs_nav_item href="#day" variant={:segment} active>Day</.tabs_nav_item>
<.tabs_nav_item href="#week" variant={:segment}>Week</.tabs_nav_item>
<.tabs_nav_item href="#month" variant={:segment}>Month</.tabs_nav_item>
</.tabs_nav>Driven by live_action
defmodule MyAppWeb.ReportsLive do
use MyAppWeb, :live_view
def mount(_params, _session, socket) do
{:ok, socket}
end
def handle_params(%{"section" => section}, _uri, socket) do
{:noreply, assign(socket, section: section)}
end
def render(assigns) do
~H"""
<.tabs_nav>
<.tabs_nav_item href="/reports" active={@section == "overview"}>
Overview
</.tabs_nav_item>
<.tabs_nav_item href="/reports/revenue" active={@section == "revenue"}>
Revenue
</.tabs_nav_item>
<.tabs_nav_item href="/reports/users" active={@section == "users"}>
Users
</.tabs_nav_item>
</.tabs_nav>
<%!-- Render different content based on section --%>
<%= case @section do %>
<% "overview" -> %> <.overview_content />
<% "revenue" -> %> <.revenue_report data={@revenue} />
<% "users" -> %> <.users_report data={@users} />
<% end %>
"""
end
endDashboard tabs with phx-click (no navigation)
When you want tab-like navigation without changing the URL, use phx-click
on an in-page anchor and drive state from LiveView assigns:
<.tabs_nav variant={:pills}>
<.tabs_nav_item
href="#"
variant={:pills}
active={@view == "chart"}
phx-click="set_view"
phx-value-view="chart"
>
Chart
</.tabs_nav_item>
<.tabs_nav_item
href="#"
variant={:pills}
active={@view == "table"}
phx-click="set_view"
phx-value-view="table"
>
Table
</.tabs_nav_item>
</.tabs_nav>Accessibility
role="tablist"on the<nav>container witharia-orientation="horizontal"role="tab"on each<a>itemaria-selected="true|false"reflects active statetabindex="0"on the active item;tabindex="-1"on inactive items (keyboard navigation follows the ARIA tabs pattern)- Disabled items: no
href,pointer-events-none opacity-50
Summary
Functions
Renders a horizontal tab navigation container.
Renders an individual tab navigation item inside tabs_nav/1.