Dock

v0.0.10

v0.0.10StableHeadlessStatelessAccessibleJS-moduleApache-2.0
Installation mix mishka.ui.gen.headless dock
Source View Source

Overview of the Phoenix and LiveView Headless Dock Component

The Mishka Chelekom Headless Dock is a bar of destinations for Phoenix and LiveView, usually pinned to the bottom of a phone-sized layout. It renders a <nav> with an accessible name. The active destination gets aria-current="page" as well as data-active, so a screen reader announces it as the current one instead of only coloring it.

Each item is a link when it has navigate, patch or href, a button when it has on_select, and a plain <span> otherwise. The slot body is the icon and label is the text. The root takes class, and the parts take item_class, icon_class and label_class.

Base UI Dock example - three destinations in a frame (Phoenix headless)

The frame is relative and the dock is absolute inset-x-0 bottom-0, so the bar sits at the bottom edge of the frame instead of the viewport. contained records that choice on the root as data-contained. The Inbox item is active.

Base UI Dock example - three destinations in a frame (Phoenix headless)

heex
<div class="relative h-40 w-72 overflow-hidden rounded-lg border border-neutral-200 bg-white dark:border-neutral-800 dark:bg-neutral-950">
  <.dock
    id="baseui-dock-hero"
    label="Sections"
    contained
    class="absolute inset-x-0 bottom-0 flex items-stretch justify-around border-t border-neutral-200 bg-white p-1 dark:border-neutral-800 dark:bg-neutral-950"
    item_class="flex flex-1 flex-col items-center gap-1 rounded-md py-2 text-xs text-neutral-600 hover:bg-neutral-100 data-[active]:text-neutral-950 dark:text-neutral-400 dark:hover:bg-neutral-800 dark:data-[active]:text-white"
  >
    <:item label="Home" href="#">
      <.icon name="hero-home" class="size-5" />
    </:item>
    <:item label="Inbox" href="#" active>
      <.icon name="hero-inbox" class="size-5" />
    </:item>
    <:item label="Settings" href="#">
      <.icon name="hero-cog-6-tooth" class="size-5" />
    </:item>
  </.dock>
</div>

Item types and on_select

An item with an address is a link. navigate and patch keep LiveView navigation working, and href is a normal link. An item with on_select is a <button type="button"> for a dock that switches a panel instead of a route. on_select takes a LiveView event name, which receives %{"index" => index}, or a Phoenix.LiveView.JS command. An item with neither is plain text and takes no tab stop.

heex
<.dock label="Sections" contained class="flex justify-around">
  <:item label="Home" on_select="show_panel" active={@panel == 0}>
    <.icon name="hero-home" class="size-5" />
  </:item>
  <:item label="Inbox" on_select="show_panel" active={@panel == 1}>
    <.icon name="hero-inbox" class="size-5" />
  </:item>
</.dock>
elixir
def handle_event("show_panel", %{"index" => index}, socket) do
  {:noreply, assign(socket, :panel, index)}
end

disabled sets data-disabled and aria-disabled="true", and a button item also gets the native disabled attribute. A link keeps its address, so leave the address out too when the item must not be followed.

Position, contained and show_labels

The component adds no positioning of its own. position (bottom by default, or top) is written to data-position, and contained to data-contained. Use those two attributes in your classes to choose fixed or absolute placement. show_labels={false} sets data-hidden on every label. The label text stays in the DOM, so style data-[hidden]:sr-only to hide it visually and keep it for screen readers.

heex
<.dock
  label="Primary"
  position="bottom"
  class="fixed inset-x-0 flex justify-around bg-white data-[position=bottom]:bottom-0 data-[position=top]:top-0 data-[contained]:absolute"
  label_class="data-[hidden]:sr-only"
  show_labels={false}
>
  <:item label="Home" navigate={~p"/"}><.icon name="hero-home" class="size-5" /></:item>
  <:item label="Settings" navigate={~p"/settings"}><.icon name="hero-cog-6-tooth" class="size-5" /></:item>
</.dock>

Parts & data attributes

Each part carries a stable data-part and a structural chelekom-dock__* class you can target, plus the state attributes below.

Partdata-partState / ARIAPurpose
Rootrootaria-label, data-position, data-containedThe nav landmark.
Itemitemdata-index, data-active, data-disabled, aria-current, aria-disabledA link, a button or a span, depending on the item attributes.
Iconiconaria-hiddenWraps the slot body.
Labellabeldata-hiddenThe destination's name. Rendered only when the item has a label.

Accessibility

There is no formal WAI-ARIA APG pattern for a dock. It follows the navigation landmark practice: a nav with an accessible name, destinations as links, and aria-current="page" on the current one. Tab moves through the items. Enter follows a link and presses a button, and Space also presses a button. A destination with no address and no on_select is text, so it never takes a tab stop it cannot use.