PhoenixKitWeb.Components.Core.NavTabs (phoenix_kit v2.15.0)

Copy Markdown View Source

Universal tab component for PhoenixKit.

Supports two modes with identical visual appearance:

Navigation tabs — each tab carries a URL, renders as <.link>:

<.nav_tabs active_tab="general" tabs={[
  %{id: "general", label: "General", icon: "hero-cog-6-tooth", navigate: Routes.path("/admin/settings")},
  %{id: "advanced", label: "Advanced", navigate: Routes.path("/admin/settings/advanced")}
]} />

Event tabs — no URL, uses on_change via phx-click:

<.nav_tabs active_tab={@active_tab} on_change="switch_tab" tabs={[
  %{id: "oban", label: "Oban Jobs"},
  %{id: "scheduler", label: "Scheduler"}
]} />

With badges (works in both modes):

<.nav_tabs active_tab={@tab} tabs={[
  %{id: "followers", label: "Followers", patch: Routes.path("/connections?tab=followers"), badge: @followers_count},
  %{id: "following", label: "Following", patch: Routes.path("/connections?tab=following"), badge: @following_count}
]} />

Tab map keys

Required: :id, :label

Optional: :icon (Heroicon name), :badge (count/text), :badge_class (a daisyUI tone such as "badge-warning", which wins over the active-tab default), and at most one link key — :navigate, :patch, or :path.

Every optional key treats nil as absent, so the common badge: if(count > 0, do: count) renders no badge rather than an empty one.

The link keys mirror Phoenix.Component.link/1 rather than inventing a parallel vocabulary: :navigate for a full LiveView navigation, :patch to stay in the current LiveView (query-param tabs want this — a :navigate there remounts and loses socket state). Both pass through VERBATIM, again like link/1 — build them with your module's Paths helpers (or Routes.path/1 yourself). :path is the legacy spelling: the same link KIND as :navigate, but with different prefix rules — it is the one key the component still runs through Routes.path/1, because its callers predate the link keys and have always passed unprefixed paths. The two are NOT interchangeable: swapping path: for navigate: while keeping an unprefixed value under-prefixes, and the reverse double-prefixes. Setting more than one link key raises; a key whose value is nil counts as absent, so a conditional path is safe.

A tab with no link key renders as a button and needs on_change; without it the tab is inert and the component logs a warning rather than raising — a dead tab should not take a whole LiveView down. A strip may freely mix link tabs and event tabs.

Event payload

Event tabs dispatch phx-value-tab, so handlers match on:

def handle_event("switch_tab", %{"tab" => id}, socket)

The key is deliberately not configurable, and deliberately not value: LiveView's extractMeta overwrites meta.value with the element's own .value DOM property, so a <button> (whose .value is "") silently delivers an empty string unless a native value= attribute is also set. Standardising on tab removes the trap rather than making it selectable.

Variants

variant={:boxed} (default) is the filled strip used across admin pages. variant={:plain} drops the frame for tabs that sit inside an already-framed container — a filter row inside a picker, say. variant={:border} is daisyUI's underline look, the convention on show-page and settings tab strips.

It exists because class can only ADD to the container: with the frame baked in, a caller had no way to take it off, and hand-rolling the markup was the only escape. That is how the copies started.

Summary

Functions

The daisyUI class list for a single tab. See tablist_class/2.

The daisyUI class list for a tablist container.

Functions

tab_class(active?, extra \\ nil)

The daisyUI class list for a single tab. See tablist_class/2.

tablist_class(variant \\ :boxed, extra \\ nil)

The daisyUI class list for a tablist container.

Exposed so anything rendering tab-styled markup that is NOT a tab strip (segmented form controls, for instance) can share one definition instead of repeating the class string. A daisyUI rename should be a change here, not a sweep across every call site — tabs-boxed became tabs-box in daisyUI 5 and had to be fixed in 21 places across 8 repositories.