Menubar

v0.0.10

v0.0.10StableHeadlessStatelessAccessibleJSApache-2.0
Installation mix mishka.ui.gen.headless menubar
Source View Source

Overview of the Phoenix and LiveView Headless Menubar Component

The Mishka Chelekom Headless Menubar ships zero opinionated styling - only the semantic markup, full WAI-ARIA wiring, roving tabindex across the top-level menus, and the defining menubar behaviour - switching the open menu when you hover or arrow to another trigger while one is already open. Every part - menu, trigger and popup - is addressable through a *_class attribute, so you style it entirely with your own Tailwind classes while the accessibility and behavior keep working underneath.

Base UI Menubar example (Phoenix headless)

Base UI Menubar example (Phoenix headless)

heex
<.menubar
  id="baseui-menubar-hero"
  class="flex items-center"
  menu_class="relative"
  trigger_class="h-8 border-0 bg-transparent px-3 text-sm font-normal text-neutral-950 select-none data-[popup-open]:bg-neutral-100 data-[pressed]:bg-neutral-100 data-[disabled]:text-neutral-500 dark:text-white dark:focus-visible:bg-neutral-800 dark:data-[popup-open]:bg-neutral-800 dark:data-[pressed]:bg-neutral-800 dark:data-[disabled]:text-neutral-400 focus-visible:bg-neutral-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-neutral-950 focus-visible:relative dark:focus-visible:outline-white"
  popup_class="absolute left-0 top-full mt-1 z-10 origin-[var(--transform-origin)] border border-neutral-950 bg-white py-1 text-neutral-950 shadow-[0.25rem_0.25rem_0] shadow-black/12 outline-none transition-[scale,opacity] duration-100 ease-out data-[ending-style]:scale-[0.98] data-[ending-style]:opacity-0 data-[instant]:transition-none data-[starting-style]:scale-[0.98] data-[starting-style]:opacity-0 data-[closed]:hidden dark:border-white dark:bg-neutral-950 dark:text-white dark:shadow-none"
>
  <:menu label="File">
    <button
      type="button"
      role="menuitem"
      class="flex w-full cursor-default items-center justify-between gap-4 py-2 pr-4 pl-4 text-left text-sm leading-4 font-normal outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-white data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:bg-neutral-950 data-[highlighted]:before:content-[''] dark:data-[highlighted]:text-neutral-950 dark:data-[highlighted]:before:bg-white"
    >
      New
    </button>
    <button
      type="button"
      role="menuitem"
      class="flex w-full cursor-default items-center justify-between gap-4 py-2 pr-4 pl-4 text-left text-sm leading-4 font-normal outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-white data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:bg-neutral-950 data-[highlighted]:before:content-[''] dark:data-[highlighted]:text-neutral-950 dark:data-[highlighted]:before:bg-white"
    >
      Open
    </button>
    <button
      type="button"
      role="menuitem"
      class="flex w-full cursor-default items-center justify-between gap-4 py-2 pr-4 pl-4 text-left text-sm leading-4 font-normal outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-white data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:bg-neutral-950 data-[highlighted]:before:content-[''] dark:data-[highlighted]:text-neutral-950 dark:data-[highlighted]:before:bg-white"
    >
      Save
    </button>
    <button
      type="button"
      role="menuitem"
      class="flex w-full cursor-default items-center justify-between gap-4 py-2 pr-2 pl-4 text-left text-sm leading-4 font-normal outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-white data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:bg-neutral-950 data-[highlighted]:before:content-[''] dark:data-[highlighted]:text-neutral-950 dark:data-[highlighted]:before:bg-white"
    >
      Export
      <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" style="display: block;">
        <path d="M6 12V4l4.5 4z" />
      </svg>
    </button>
    <div
      data-part="separator"
      role="separator"
      class="mx-1 my-1 h-px bg-neutral-950 dark:bg-white"
    >
    </div>
    <button
      type="button"
      role="menuitem"
      class="flex w-full cursor-default items-center justify-between gap-4 py-2 pr-4 pl-4 text-left text-sm leading-4 font-normal outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-white data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:bg-neutral-950 data-[highlighted]:before:content-[''] dark:data-[highlighted]:text-neutral-950 dark:data-[highlighted]:before:bg-white"
    >
      Print
    </button>
  </:menu>

  <:menu label="Edit">
    <button
      type="button"
      role="menuitem"
      class="flex w-full cursor-default items-center justify-between gap-4 py-2 pr-4 pl-4 text-left text-sm leading-4 font-normal outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-white data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:bg-neutral-950 data-[highlighted]:before:content-[''] dark:data-[highlighted]:text-neutral-950 dark:data-[highlighted]:before:bg-white"
    >
      Cut
    </button>
    <button
      type="button"
      role="menuitem"
      class="flex w-full cursor-default items-center justify-between gap-4 py-2 pr-4 pl-4 text-left text-sm leading-4 font-normal outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-white data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:bg-neutral-950 data-[highlighted]:before:content-[''] dark:data-[highlighted]:text-neutral-950 dark:data-[highlighted]:before:bg-white"
    >
      Copy
    </button>
    <button
      type="button"
      role="menuitem"
      class="flex w-full cursor-default items-center justify-between gap-4 py-2 pr-4 pl-4 text-left text-sm leading-4 font-normal outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-white data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:bg-neutral-950 data-[highlighted]:before:content-[''] dark:data-[highlighted]:text-neutral-950 dark:data-[highlighted]:before:bg-white"
    >
      Paste
    </button>
  </:menu>

  <:menu label="View">
    <button
      type="button"
      role="menuitem"
      class="flex w-full cursor-default items-center justify-between gap-4 py-2 pr-4 pl-4 text-left text-sm leading-4 font-normal outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-white data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:bg-neutral-950 data-[highlighted]:before:content-[''] dark:data-[highlighted]:text-neutral-950 dark:data-[highlighted]:before:bg-white"
    >
      Zoom In
    </button>
    <button
      type="button"
      role="menuitem"
      class="flex w-full cursor-default items-center justify-between gap-4 py-2 pr-4 pl-4 text-left text-sm leading-4 font-normal outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-white data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:bg-neutral-950 data-[highlighted]:before:content-[''] dark:data-[highlighted]:text-neutral-950 dark:data-[highlighted]:before:bg-white"
    >
      Zoom Out
    </button>
    <button
      type="button"
      role="menuitem"
      class="flex w-full cursor-default items-center justify-between gap-4 py-2 pr-2 pl-4 text-left text-sm leading-4 font-normal outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-white data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:bg-neutral-950 data-[highlighted]:before:content-[''] dark:data-[highlighted]:text-neutral-950 dark:data-[highlighted]:before:bg-white"
    >
      Layout
      <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" style="display: block;">
        <path d="M6 12V4l4.5 4z" />
      </svg>
    </button>
    <div
      data-part="separator"
      role="separator"
      class="mx-1 my-1 h-px bg-neutral-950 dark:bg-white"
    >
    </div>
    <button
      type="button"
      role="menuitem"
      class="flex w-full cursor-default items-center justify-between gap-4 py-2 pr-4 pl-4 text-left text-sm leading-4 font-normal outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-white data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:bg-neutral-950 data-[highlighted]:before:content-[''] dark:data-[highlighted]:text-neutral-950 dark:data-[highlighted]:before:bg-white"
    >
      Full Screen
    </button>
  </:menu>

  <:menu label="Help" disabled></:menu>
</.menubar>

Parts & data attributes

Each part carries a stable data-part and a structural chelekom-menubar__* class you can target, plus paired-presence state for open/closed menu popups.

Partdata-partState / ARIAPurpose
Root-role="menubar", aria-orientation, aria-disabled, data-orientation, data-modal, data-has-submenu-open, data-disabledCarries the Menubar hook; reads orientation, loop, modal and disabled from its data attributes.
Menumenu-Wraps one top-level trigger and its popup.
Triggertriggerrole="menuitem", aria-haspopup, aria-expanded, aria-controls, aria-disabled, data-popup-open, data-pressed, data-disabledThe button that opens a menu; part of the roving tabindex.
Popuppopuprole="menu", aria-label, data-open, data-closedThe list of items for one menu.

Accessibility

Follows the WAI-ARIA Menubar pattern: arrow keys move focus between top-level menus along the configured orientation (looping past the ends when enabled), Home/End jump to the first/last menu, Enter/Space or a cross-axis arrow opens a menu, Escape closes it, and once a menu is open, hovering or arrowing to another trigger switches the open menu. Arrow Up/Down move between items inside an open menu.