Fluxon.Components.Navlist
(Fluxon v3.0.0)
A compound navigation system for building structured menus, sidebars, and section indexes.
Navlist is a pure-markup component family with no client-side state. It composes a <nav>
container, optional section headings, and link items that delegate to Phoenix.Component.link/1
for navigation. Items render an active state via the data-active attribute, which makes the
current selection styleable from CSS without conditional class logic on the server.
The system is composed of a few pieces meant to compose together:
navlist/1: the container that provides spacing, an optional heading, and stacking of items.navheading/1: an<h3>label used to group related items inside or above a list.navlink/1: an interactive item that renders as a link or button depending on which attributes are passed through.
A typical structure looks like this:
navlist
├── navheading (optional, rendered automatically when `heading` is set)
├── navlink
├── navlink
└── navlinkMultiple navlists stacked one after another receive automatic top spacing through a CSS sibling
selector ([&+[data-part=navlist]]:mt-6), so adjacent sections separate visually without extra
wrappers.
Choosing between navlist and tabs
Use navlist for navigation that changes the page or route (sidebars, settings indexes,
docs menus). Use Fluxon.Components.Tabs for in-page panel switching where content swaps
inside the same view without a navigation event.
Usage
A simple navigation list with a heading and three items:
<.navlist heading="Main Navigation">
<.navlink navigate={~p"/dashboard"} active>
<.icon name="hero-home" class="size-5" /> Dashboard
</.navlink>
<.navlink navigate={~p"/projects"}>
<.icon name="hero-folder" class="size-5" /> Projects
</.navlink>
<.navlink navigate={~p"/settings"}>
<.icon name="hero-cog-6-tooth" class="size-5" /> Settings
</.navlink>
</.navlist>The heading attribute is a shortcut for rendering a navheading/1 as the first child. For
fully custom heading markup, omit heading and place a navheading/1 (or any element) inside
the slot directly.
Sections
Stack multiple navlists to group items into labeled sections. Adjacent navlists are separated automatically with top margin:
<.navlist heading="Workspace">
<.navlink navigate={~p"/dashboard"} active>
<.icon name="hero-home" class="size-5" /> Dashboard
</.navlink>
<.navlink navigate={~p"/projects"}>
<.icon name="hero-folder" class="size-5" /> Projects
</.navlink>
</.navlist>
<.navlist heading="Account">
<.navlink navigate={~p"/profile"}>
<.icon name="hero-user" class="size-5" /> Profile
</.navlink>
<.navlink navigate={~p"/preferences"}>
<.icon name="hero-cog-6-tooth" class="size-5" /> Preferences
</.navlink>
</.navlist>Active State
Mark the current item with the active attribute. Active styling is applied through the
data-active attribute on the rendered element, so it can be combined or extended with
any data-active: Tailwind variant on a custom class:
<.navlink navigate={~p"/dashboard"} active={@current_path == "/dashboard"}>
Dashboard
</.navlink>Computing active from a LiveView assign keeps the source of truth on the server. Because the
active state is expressed as a data attribute, no JavaScript is required to keep styles in sync.
Navigation Modes
navlink/1 forwards every link-related attribute to Phoenix.Component.link/1, so the same
item can perform different kinds of navigation depending on which attribute is set:
| Attribute | Behavior |
|---|---|
navigate | Full LiveView navigation to a new live session. Use for top-level destinations. |
patch | Patch the current LiveView with a new URL. Use for tabs and filters that share state. |
href | Standard browser navigation. Use for external links or non-LiveView routes. |
phx-click | Client or server event without navigation. Use for toggles and expandable groups. |
Standard anchor attributes (target, download, rel, hreflang, type, referrerpolicy),
link options (replace, method, csrf_token), and button attributes (disabled,
formaction, name, value, etc.) are all accepted via the global :rest attribute.
<.navlink patch={~p"/messages?folder=inbox"} replace>Inbox</.navlink>
<.navlink navigate={~p"/billing"}>Billing</.navlink>
<.navlink href="https://docs.example.com" target="_blank" rel="noopener">
Documentation
</.navlink>Icons and Trailing Content
The link slot is a flex row, so icons and trailing badges align horizontally with consistent
spacing. Use ml-auto on the trailing element to push it to the right edge:
<.navlist heading="Inbox">
<.navlink href="/inbox/unread">
<.icon name="hero-inbox" class="size-5" />
Unread
<.badge variant="pill" color="red" class="ml-auto">23</.badge>
</.navlink>
<.navlink href="/inbox/starred">
<.icon name="hero-star" class="size-5" />
Starred
<.badge variant="pill" class="ml-auto">5</.badge>
</.navlink>
<.navlink href="/inbox/archive">
<.icon name="hero-archive-box" class="size-5" />
Archive
</.navlink>
</.navlist>Icons rendered with the icon helper inherit a default size-4 through the link's internal
styles when they carry the .icon class, so you can omit an explicit size in simple cases and
pass a class (for example class="size-5") only when you want a larger icon.
Custom Headings
When the built-in heading attribute is not enough, render navheading/1 directly inside the
list. This is useful for uppercase labels, captions, or headings that need badges and icons:
<.navlist>
<.navheading class="text-xs uppercase tracking-wider text-foreground-softest">
Customers
</.navheading>
<.navlink navigate={~p"/customers"}>Customers</.navlink>
<.navlink navigate={~p"/segments"}>Segments</.navlink>
</.navlist>Expandable Groups
Navlist has no built-in expand/collapse behavior. Hierarchical groups are composed by combining
navlink/1 with Phoenix.LiveView.JS.toggle_attribute/1 on a shared data-expanded attribute,
then animating a sibling container with Tailwind grid utilities:
<.navlist heading="Sales">
<.navlink phx-click={JS.toggle_attribute({"data-expanded", ""})}>
<.icon name="hero-users" class="size-5" /> Customers
<.icon
name="hero-chevron-right"
class="size-3 ml-auto text-zinc-500 in-data-expanded:rotate-90 transition-transform duration-200"
/>
</.navlink>
<div class="grid grid-rows-[0fr] [[data-expanded]+&]:grid-rows-[1fr] transition-all duration-200">
<div class="overflow-hidden px-4 border-l ml-3">
<.navlist>
<.navlink navigate={~p"/customers/active"}>Active</.navlink>
<.navlink navigate={~p"/customers/at-risk"}>At Risk</.navlink>
</.navlist>
</div>
</div>
<.navlink navigate={~p"/subscriptions"}>
<.icon name="hero-arrow-path" class="size-5" /> Subscriptions
</.navlink>
</.navlist>The pattern relies on three pieces:
JS.toggle_attribute({"data-expanded", ""})toggles a marker attribute on the trigger.- The collapsible panel uses
grid-rows-[0fr]collapsed andgrid-rows-[1fr]when expanded, which animates height smoothly through a CSS transition. - The chevron rotates via the
in-data-expanded:rotate-90Tailwind variant, reading the expansion state from the trigger ancestor.
Because the toggle is a client-side attribute change, expand/collapse state is preserved across LiveView patches without round-tripping to the server.
Persisting expanded state
Client-side toggles are reset on full page navigation. To remember which groups are open
across navigations, drive the data-expanded attribute from a server assign instead of
toggling it on the client.
Examples
Sidebar with sections, icons, and a counter:
<aside class="w-64 p-4">
<.navlist heading="Workspace">
<.navlink navigate={~p"/dashboard"} active={@section == :dashboard}>
<.icon name="hero-home" class="size-5" /> Dashboard
</.navlink>
<.navlink navigate={~p"/inbox"} active={@section == :inbox}>
<.icon name="hero-inbox" class="size-5" /> Inbox
<.badge variant="pill" color="red" class="ml-auto">{@unread_count}</.badge>
</.navlink>
<.navlink navigate={~p"/projects"} active={@section == :projects}>
<.icon name="hero-folder" class="size-5" /> Projects
</.navlink>
</.navlist>
<.navlist heading="Account">
<.navlink navigate={~p"/profile"}>
<.icon name="hero-user" class="size-5" /> Profile
</.navlink>
<.navlink navigate={~p"/billing"}>
<.icon name="hero-credit-card" class="size-5" /> Billing
</.navlink>
</.navlist>
</aside>Settings index that patches the current LiveView:
<.navlist heading="Settings">
<.navlink :for={tab <- @tabs} patch={~p"/settings?tab=#{tab.id}"} active={@active == tab.id}>
<.icon name={tab.icon} class="size-5" /> {tab.label}
</.navlink>
</.navlist>Action item that triggers a server event with a confirmation prompt:
<.navlist heading="Danger Zone">
<.navlink phx-click="archive_workspace" data-confirm="Archive this workspace?">
<.icon name="hero-archive-box" class="size-5" /> Archive workspace
</.navlink>
<.navlink
phx-click="delete_workspace"
data-confirm="This cannot be undone. Continue?"
class="text-danger data-active:bg-danger/10"
>
<.icon name="hero-trash" class="size-5" /> Delete workspace
</.navlink>
</.navlist>Rendering items from a list with :for:
<.navlist heading="Customers">
<.navlink
:for={item <- @nav_items}
navigate={item.path}
active={item.path == @current_path}
>
<.icon name={item.icon} class="size-5" />
<span class="grow">{item.label}</span>
<.badge :if={item.badge} color="blue">{item.badge}</.badge>
</.navlink>
</.navlist>Custom-styled navlinks for a marketing-style sidebar:
<.navlist>
<.navheading class="text-xs uppercase font-medium text-zinc-400">
Customers
</.navheading>
<.navlink
:for={
{icon, label, badge, path, active} <- [
{"hero-users", "Customers", nil, ~p"/customers", false},
{"hero-shopping-bag", "Subscriptions", "23", ~p"/subscriptions", true},
{"hero-cube", "Products", nil, ~p"/products", false},
{"hero-tag", "Coupons", nil, ~p"/coupons", false}
]
}
navigate={path}
active={active}
class={[
"group py-1.5 relative",
"hover:text-blue-600 hover:bg-white hover:shadow-sm",
"hover:ring-1 ring-zinc-200",
"hover:after:absolute hover:after:inset-y-0 hover:after:left-0",
"hover:after:my-1.5 hover:after:w-1 hover:after:bg-blue-600",
"hover:after:rounded-r-md"
]}
>
<.icon class="size-5 text-zinc-500 group-hover:text-blue-600" name={icon} />
<span class="grow">{label}</span>
<.badge :if={badge} color="blue">{badge}</.badge>
</.navlink>
</.navlist>
Summary
Components
Renders an <h3> heading styled to label a group of navigation items.
Renders an interactive navigation item that delegates to Phoenix.Component.link/1.
Renders a <nav> container that stacks navigation items with consistent spacing.