Fluxon.Components.Tabs (Fluxon v1.1.0)
A comprehensive tabs system for creating accessible, interactive tabbed interfaces.
This component provides a flexible solution for organizing content into tabbed sections across your application. It offers a structured set of components working together to create accessible and keyboard-navigable tabs.
Components
The tabs system consists of three main components working together in a specific hierarchy:
tabs
: The main container providing structure and JavaScript functionality.tabs_list
: Navigation container holding the interactive tab buttons.tabs_panel
: Content panels associated with each tab, displayed one at a time.
tabs
├── tabs_list
│ └── tab slots (interactive buttons)
└── tabs_panels
└── panel content (one active at a time)
Usage Examples
Basic Tabs
Create a simple tabbed interface with multiple static panels:
<.tabs id="my-tabs">
<.tabs_list active_tab="settings">
<:tab name="profile">Profile</:tab>
<:tab name="settings">Settings</:tab>
<:tab name="notifications">Notifications</:tab>
</.tabs_list>
<.tabs_panel name="profile">
Profile content here...
</.tabs_panel>
<.tabs_panel name="settings" active>
Settings content here...
</.tabs_panel>
<.tabs_panel name="notifications">
Notifications content here...
</.tabs_panel>
</.tabs>
Note: In static HTML or non-LiveView scenarios, manually setting active_tab
on tabs_list
and active
on the corresponding tabs_panel
works directly. For LiveView usage, see the "LiveView Integration" section.
Visual Variants
The tabs_list
component supports three distinct visual styles via the variant
attribute:
<!-- Default underlined style -->
<.tabs_list variant="default">
<:tab name="tab1">Default Tab</:tab>
</.tabs_list>
<!-- Segmented button-like style -->
<.tabs_list variant="segmented">
<:tab name="tab1">Segmented Tab</:tab>
</.tabs_list>
<!-- Ghost style with subtle backgrounds -->
<.tabs_list variant="ghost">
<:tab name="tab1">Ghost Tab</:tab>
</.tabs_list>
Rich Tab Content
Tabs can include icons, badges, or any other HEEx content:
<.tabs_list>
<:tab name="messages">
<.icon name="hero-envelope" class="icon" />
Messages
<.badge class="ml-2">3</.badge>
</:tab>
<:tab name="settings">
<.icon name="hero-cog-6-tooth" class="icon" />
Settings
</:tab>
</.tabs_list>
Dynamic Tabs
Generate tabs dynamically from a collection using for
:
<!--
Assuming @tabs_data = [%{id: "t1", title: "Tab One"}, %{id: "t2", title: "Tab Two"}]
And @active_tab = "t1" (managed by LiveView, see below)
-->
<.tabs id="dynamic-tabs">
<.tabs_list active_tab={@active_tab}>
<:tab :for={tab_data <- @tabs_data} name={tab_data.id} phx-click={JS.push("set_tab", value: %{tab: tab_data.id})}>
<%= tab_data.title %>
</:tab>
</.tabs_list>
<.tabs_panel :for={tab_data <- @tabs_data} name={tab_data.id} active={@active_tab == tab_data.id}>
Content for <%= tab_data.title %>...
</.tabs_panel>
</.tabs>
Nested Tabs
The component supports nesting for complex interfaces:
<.tabs id="parent-tabs">
<.tabs_list variant="segmented">
<:tab name="profile">Profile</:tab>
<:tab name="settings">Settings</:tab>
</.tabs_list>
<.tabs_panel name="profile">
<.tabs id="profile-tabs">
<.tabs_list variant="ghost">
<:tab name="personal">Personal Info</:tab>
<:tab name="preferences">Preferences</:tab>
</.tabs_list>
<.tabs_panel name="personal">
Personal information content...
</.tabs_panel>
<.tabs_panel name="preferences">
Preferences content...
</.tabs_panel>
</.tabs>
</.tabs_panel>
<.tabs_panel name="settings">
Settings content...
</.tabs_panel>
</.tabs>
LiveView Integration
When using tabs within Phoenix LiveView, managing the active state requires synchronizing with the LiveView's assigns to prevent the active tab from resetting during patches.
1. Using Assigns and handle_event
:
Manage the active tab in the LiveView's assigns and update it using phx-click
events on the tabs.
<.tabs id="lv-sync-tabs">
<.tabs_list active_tab={@active_tab}>
<:tab name="profile" phx-click={JS.push("set_active_tab", value: %{tab: "profile"})}>
Profile
</:tab>
<:tab name="settings" phx-click={JS.push("set_active_tab", value: %{tab: "settings"})}>
Settings
</:tab>
</.tabs_list>
<.tabs_panel name="profile" active={@active_tab == "profile"}>
Profile content...
</.tabs_panel>
<.tabs_panel name="settings" active={@active_tab == "settings"}>
Settings content...
</.tabs_panel>
</.tabs>
In your LiveView module:
def mount(_params, _session, socket) do
{:ok, assign(socket, :active_tab, "profile")} # Set initial tab
end
def handle_event("set_active_tab", %{"tab" => tab}, socket) do
{:noreply, assign(socket, :active_tab, tab)}
end
2. Using URL Parameters and push_patch
:
For state persistence across full page reloads or sharing links, store the active tab name in the URL parameters.
<.tabs id="lv-url-tabs">
<.tabs_list active_tab={@active_tab}>
<:tab name="profile" phx-click={JS.push("set_tab", value: %{tab: "profile"})}>
Profile
</:tab>
<:tab name="settings" phx-click={JS.push("set_tab", value: %{tab: "settings"})}>
Settings
</:tab>
</.tabs_list>
<.tabs_panel name="profile" active={@active_tab == "profile"}>...</.tabs_panel>
<.tabs_panel name="settings" active={@active_tab == "settings"}>...</.tabs_panel>
</.tabs>
def mount(_params, _session, socket) do
# Initial tab state is set by handle_params based on URL
{:ok, socket}
end
def handle_params(params, _uri, socket) do
active_tab = params["tab"] || "profile" # Default if param missing
{:noreply, assign(socket, :active_tab, active_tab)}
end
def handle_event("set_tab", %{"tab" => tab}, socket) do
# Update URL, which triggers handle_params to update assigns
current_path = URI.parse(socket.assigns.current_path).path
{:noreply, push_patch(socket, to: current_path <> "?tab=#{tab}")}
end
Accessibility/Keyboard Navigation
This component suite is designed with accessibility in mind, automatically incorporating essential ARIA attributes (role="tablist"
, role="tab"
, role="tabpanel"
, aria-selected
, aria-controls
, aria-labelledby
) and managing focus according to best practices.
Keyboard Support
Key | Element Focus | Description |
---|---|---|
Tab | Document | Focuses the active tab button when tabbing into the tab list. |
→ / ↓ | Tab button | Moves focus to and activates the next tab, wrapping to first if at end. |
← / ↑ | Tab button | Moves focus to and activates the previous tab, wrapping to last if at start. |
Home | Tab button | Moves focus to and activates the first tab in the list. |
End | Tab button | Moves focus to and activates the last tab in the list. |
Focus Management Details
- Only the active tab button is included in the page's default
Tab
sequence (tabindex="0"
). - Non-active tab buttons have
tabindex="-1"
to be removed from the default sequence but remain focusable via arrow keys. - Activating a tab via keyboard immediately displays its associated panel and moves focus to the newly active tab button.
- Focus remains within the active tab's panel content when navigating inside it until the user tabs out of the panel or uses tab list keyboard navigation.
Summary
Components
Renders a tabs container with support for dynamic content and keyboard navigation.
Renders a list of interactive tabs with support for different visual styles.
Renders a tab panel that displays content when its corresponding tab is active.
Components
Renders a tabs container with support for dynamic content and keyboard navigation.
This component serves as the foundation for building tabbed interfaces, providing proper
structure, accessibility features, and JavaScript functionality. It works in conjunction
with tabs_list
and tabs_panel
components to create comprehensive tabbed interfaces.
Attributes
id
(:string
) - A unique identifier for the tabs container. If not provided, a unique ID will be generated.class
(:any
) - Additional CSS classes to be applied to the tabs container. These are merged with the component's base styles.Defaults to
nil
.Global attributes are accepted. Allows passing additional HTML attributes (e.g.,
data-*
attributes, custom ARIA roles/properties if needed beyond the defaults) directly to the maindiv
container.
Slots
inner_block
(required) - The primary content area for the tabs component. This slot typically houses one<.tabs_list>
and one or more<.tabs_panel>
components, defining the navigation and content areas.
Renders a list of interactive tabs with support for different visual styles.
This component provides the navigation interface for the tabs system, managing tab
selection, keyboard navigation, and visual styling. It's designed to work within
the tabs
component.
Attributes
class
(:any
) - Additional CSS classes to be applied to the tablist container. These are merged with the component's base styles and variant-specific styles.Defaults to
nil
.active_tab
(:string
) - Thename
attribute of the tab that should be initially active. If not provided, the component defaults to activating the first tab defined within the:tab
slot. In LiveView scenarios, this should typically be bound to an assign (e.g.,active_tab={@active_tab}
).variant
(:string
) - The visual style variant of the tabs. Available options:"default"
: Underlined style with bottom border indicator"segmented"
: Button-like style with background and shadow"ghost"
: Subtle style with background indicator
Defaults to
"default"
.
Slots
tab
(required) - Defines an individual interactive tab button within the list.- Requires a
name
attribute (string) which must correspond to thename
of a<.tabs_panel>
. - Any additional attributes (e.g.,
class
,phx-click
,id
,data-*
) are passed directly to the underlying<button>
element. The content inside the<:tab>
tag becomes the button's label.
- Requires a
inner_block
(required) - The main content area within the<.tabs_list>
component, typically containing only the<:tab>
slots.
Basic Usage
<.tabs_list>
<:tab name="tab1">First Tab</:tab>
<:tab name="tab2">Second Tab</:tab>
</.tabs_list>
Visual Variants
<.tabs_list variant="segmented">
<:tab name="tab1">
<.icon name="hero-home" class="icon" /> Home
</:tab>
<:tab name="tab2">
<.icon name="hero-cog-6-tooth" class="icon" /> Settings
</:tab>
</.tabs_list>
Custom Styling
<.tabs_list class="gap-4">
<:tab name="tab1" class="font-bold">
Custom Tab
</:tab>
</.tabs_list>
Renders a tab panel that displays content when its corresponding tab is active.
This component provides the content container for each tab, managing visibility
and accessibility attributes. It's designed to work within the tabs
component.
Attributes
name
(:string
) (required) - The unique identifier for this panel. This value must exactly match thename
attribute of its corresponding<:tab>
within the<.tabs_list>
. This linkage is essential for functionality and accessibility.class
(:any
) - Additional CSS classes to be applied to the panel element. Defaults tonil
.active
(:boolean
) - Controls the visibility of the panel. Set totrue
if this panel corresponds to the currently active tab. In LiveView, this is typically determined by comparing the panel'sname
with the state variable holding the active tab name (e.g.,active={@active_tab == "settings"}
).Defaults to
false
.Global attributes are accepted. Allows passing additional HTML attributes (e.g.,
data-*
, custom styling IDs) directly to the panel'sdiv
container.
Slots
inner_block
(required) - The content to be displayed within this panel when its corresponding tab is active.
Basic Usage
<.tabs_panel name="tab1" active>
Content for the first tab...
</.tabs_panel>
<.tabs_panel name="tab2">
Content for the second tab...
</.tabs_panel>
With Rich Content
<.tabs_panel name="settings" class="space-y-4">
<h3 class="text-lg font-medium">Settings</h3>
<.form for={@form} phx-submit="save">
<!-- Form fields -->
</.form>
</.tabs_panel>