Overview of the Phoenix and LiveView Headless Menu Component
The Mishka Chelekom Headless Menu
ships zero opinionated styling
- only the semantic markup, full WAI-ARIA
wiring, and paired-presence state
(
data-open
/
data-closed
).
Click (or, with
open_on_hover
, hover) the
trigger
to open the popup, anchored by side/
align
with edge-flip and viewport clamp. It is composed from per-row parts - item, checkbox item, radio group / radio item, link item, group / group label, separator, and
submenu trigger / submenu popup
- so you can either use the idiomatic
:item
/
:submenu
slots for flat rows or compose the
menu_*
row components as children for arbitrary order and nested submenus - even mix both.
Every part 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 Menu example - flat items (Phoenix headless)
<.menu id="baseui-menu-hero" side_offset={8} trigger_class="flex h-8 items-center justify-center gap-1.5 border border-neutral-950 dark:border-white bg-white dark:bg-neutral-950 pl-3 pr-2 text-sm font-normal text-neutral-950 dark:text-white select-none hover:not-data-[disabled]:bg-neutral-100 dark:hover:not-data-[disabled]:bg-neutral-800 data-[popup-open]:bg-neutral-100 dark:data-[popup-open]:bg-neutral-800" popup_class="relative origin-[var(--transform-origin)] border border-neutral-950 dark:border-white bg-white dark:bg-neutral-950 py-1 text-neutral-950 dark:text-white shadow-[0.25rem_0.25rem_0] shadow-black/12 dark:shadow-none outline-hidden transition-[scale,opacity] duration-100 ease-out data-[ending-style]:scale-[0.98] data-[ending-style]:opacity-0 data-[starting-style]:scale-[0.98] data-[starting-style]:opacity-0" > <:trigger> Song <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M12 6H4l4 4.5z" /></svg> </:trigger> <:item class="flex cursor-default py-2 pr-8 pl-4 text-sm select-none outline-hidden data-[highlighted]:relative data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:-z-10 data-[highlighted]:before:bg-neutral-950 dark:data-[highlighted]:before:bg-white data-[highlighted]:text-white dark:data-[highlighted]:text-neutral-950"> Add to Library </:item> <:item class="flex cursor-default py-2 pr-8 pl-4 text-sm select-none outline-hidden data-[highlighted]:relative data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:-z-10 data-[highlighted]:before:bg-neutral-950 dark:data-[highlighted]:before:bg-white data-[highlighted]:text-white dark:data-[highlighted]:text-neutral-950"> Add to Playlist </:item> <:item type="separator" class="mx-1 my-1 h-px bg-neutral-950 dark:bg-white" /> <:item class="flex cursor-default py-2 pr-8 pl-4 text-sm select-none outline-hidden data-[highlighted]:relative data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:-z-10 data-[highlighted]:before:bg-neutral-950 dark:data-[highlighted]:before:bg-white data-[highlighted]:text-white dark:data-[highlighted]:text-neutral-950"> Play Next </:item> <:item class="flex cursor-default py-2 pr-8 pl-4 text-sm select-none outline-hidden data-[highlighted]:relative data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:-z-10 data-[highlighted]:before:bg-neutral-950 dark:data-[highlighted]:before:bg-white data-[highlighted]:text-white dark:data-[highlighted]:text-neutral-950"> Play Last </:item> <:item type="separator" class="mx-1 my-1 h-px bg-neutral-950 dark:bg-white" /> <:item class="flex cursor-default py-2 pr-8 pl-4 text-sm select-none outline-hidden data-[highlighted]:relative data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:-z-10 data-[highlighted]:before:bg-neutral-950 dark:data-[highlighted]:before:bg-white data-[highlighted]:text-white dark:data-[highlighted]:text-neutral-950"> Favorite </:item> <:item class="flex cursor-default py-2 pr-8 pl-4 text-sm select-none outline-hidden data-[highlighted]:relative data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:-z-10 data-[highlighted]:before:bg-neutral-950 dark:data-[highlighted]:before:bg-white data-[highlighted]:text-white dark:data-[highlighted]:text-neutral-950"> Share </:item> </.menu>
Base UI Menu example - labeled radio and checkbox groups (Phoenix headless)
<.menu id="baseui-menu-group-labels" side_offset={8} trigger_class="flex h-8 items-center justify-center gap-1.5 border border-neutral-950 dark:border-white bg-white dark:bg-neutral-950 pl-3 pr-2 text-sm font-normal text-neutral-950 dark:text-white select-none hover:not-data-[disabled]:bg-neutral-100 dark:hover:not-data-[disabled]:bg-neutral-800 data-[popup-open]:bg-neutral-100 dark:data-[popup-open]:bg-neutral-800" popup_class="relative origin-[var(--transform-origin)] border border-neutral-950 dark:border-white bg-white dark:bg-neutral-950 py-1 text-neutral-950 dark:text-white shadow-[0.25rem_0.25rem_0] shadow-black/12 dark:shadow-none outline-hidden transition-[scale,opacity] duration-100 ease-out data-[ending-style]:scale-[0.98] data-[ending-style]:opacity-0 data-[starting-style]:scale-[0.98] data-[starting-style]:opacity-0" > <:trigger> View <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M12 6H4l4 4.5z" /></svg> </:trigger> <:check_icon> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor"><path d="m2.5 8.5 4 4 7-9" /></svg> </:check_icon> <:item type="label" class="py-2 pr-8 pl-[2.125rem] text-sm text-neutral-500 dark:text-neutral-400 select-none"> Sort </:item> <:item type="radio" name="view-sort" value="date" checked={true} class="grid grid-cols-[1rem_1fr] items-center gap-2 py-2 pr-8 pl-2.5 text-sm select-none outline-hidden data-[highlighted]:relative data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:-z-10 data-[highlighted]:before:bg-neutral-950 dark:data-[highlighted]:before:bg-white data-[highlighted]:text-white dark:data-[highlighted]:text-neutral-950" indicator_class="col-start-1 data-[unchecked]:invisible"> <span class="col-start-2">Date</span> </:item> <:item type="radio" name="view-sort" value="name" class="grid grid-cols-[1rem_1fr] items-center gap-2 py-2 pr-8 pl-2.5 text-sm select-none outline-hidden data-[highlighted]:relative data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:-z-10 data-[highlighted]:before:bg-neutral-950 dark:data-[highlighted]:before:bg-white data-[highlighted]:text-white dark:data-[highlighted]:text-neutral-950" indicator_class="col-start-1 data-[unchecked]:invisible"> <span class="col-start-2">Name</span> </:item> <:item type="radio" name="view-sort" value="type" class="grid grid-cols-[1rem_1fr] items-center gap-2 py-2 pr-8 pl-2.5 text-sm select-none outline-hidden data-[highlighted]:relative data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:-z-10 data-[highlighted]:before:bg-neutral-950 dark:data-[highlighted]:before:bg-white data-[highlighted]:text-white dark:data-[highlighted]:text-neutral-950" indicator_class="col-start-1 data-[unchecked]:invisible"> <span class="col-start-2">Type</span> </:item> <:item type="separator" class="mx-1 my-1 h-px bg-neutral-950 dark:bg-white" /> <:item type="label" class="py-2 pr-8 pl-[2.125rem] text-sm text-neutral-500 dark:text-neutral-400 select-none"> Workspace </:item> <:item type="checkbox" checked={true} class="grid grid-cols-[1rem_1fr] items-center gap-2 py-2 pr-8 pl-2.5 text-sm select-none outline-hidden data-[highlighted]:relative data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:-z-10 data-[highlighted]:before:bg-neutral-950 dark:data-[highlighted]:before:bg-white data-[highlighted]:text-white dark:data-[highlighted]:text-neutral-950" indicator_class="col-start-1 data-[unchecked]:invisible"> <span class="col-start-2">Minimap</span> </:item> <:item type="checkbox" checked={true} class="grid grid-cols-[1rem_1fr] items-center gap-2 py-2 pr-8 pl-2.5 text-sm select-none outline-hidden data-[highlighted]:relative data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:-z-10 data-[highlighted]:before:bg-neutral-950 dark:data-[highlighted]:before:bg-white data-[highlighted]:text-white dark:data-[highlighted]:text-neutral-950" indicator_class="col-start-1 data-[unchecked]:invisible"> <span class="col-start-2">Search</span> </:item> <:item type="checkbox" checked={false} class="grid grid-cols-[1rem_1fr] items-center gap-2 py-2 pr-8 pl-2.5 text-sm select-none outline-hidden data-[highlighted]:relative data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:-z-10 data-[highlighted]:before:bg-neutral-950 dark:data-[highlighted]:before:bg-white data-[highlighted]:text-white dark:data-[highlighted]:text-neutral-950" indicator_class="col-start-1 data-[unchecked]:invisible"> <span class="col-start-2">Sidebar</span> </:item> </.menu>
Base UI Menu example - nested submenu (Phoenix headless)
<.menu id="baseui-menu-submenu" side_offset={8} trigger_class="flex h-8 items-center justify-center gap-1.5 border border-neutral-950 dark:border-white bg-white dark:bg-neutral-950 pl-3 pr-2 text-sm font-normal text-neutral-950 dark:text-white select-none hover:not-data-[disabled]:bg-neutral-100 dark:hover:not-data-[disabled]:bg-neutral-800 data-[popup-open]:bg-neutral-100 dark:data-[popup-open]:bg-neutral-800" popup_class="relative origin-[var(--transform-origin)] border border-neutral-950 dark:border-white bg-white dark:bg-neutral-950 py-1 text-neutral-950 dark:text-white shadow-[0.25rem_0.25rem_0] shadow-black/12 dark:shadow-none outline-hidden transition-[scale,opacity] duration-100 ease-out data-[ending-style]:scale-[0.98] data-[ending-style]:opacity-0 data-[starting-style]:scale-[0.98] data-[starting-style]:opacity-0" > <:trigger> Song <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M12 6H4l4 4.5z" /></svg> </:trigger> <.menu_item class="flex cursor-default py-2 pr-6 pl-4 text-sm select-none outline-hidden data-[highlighted]:relative data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:-z-10 data-[highlighted]:before:bg-neutral-950 dark:data-[highlighted]:before:bg-white data-[highlighted]:text-white dark:data-[highlighted]:text-neutral-950"> Add to Library </.menu_item> <.menu_submenu id="baseui-menu-submenu-playlist" label="Add to Playlist" trigger_class="flex cursor-default items-center justify-between gap-4 py-2 pr-2 pl-4 text-sm select-none outline-hidden data-[popup-open]:relative data-[popup-open]:before:absolute data-[popup-open]:before:inset-x-1 data-[popup-open]:before:inset-y-0 data-[popup-open]:before:-z-10 data-[popup-open]:before:bg-neutral-100 dark:data-[popup-open]:before:bg-neutral-800 data-[highlighted]:relative data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:-z-10 data-[highlighted]:before:bg-neutral-950 dark:data-[highlighted]:before:bg-white data-[highlighted]:text-white dark:data-[highlighted]:text-neutral-950" popup_class="relative origin-[var(--transform-origin)] border border-neutral-950 dark:border-white bg-white dark:bg-neutral-950 py-1 text-neutral-950 dark:text-white shadow-[0.25rem_0.25rem_0] shadow-black/12 dark:shadow-none outline-hidden transition-[scale,opacity] duration-100 ease-out data-[ending-style]:scale-[0.98] data-[ending-style]:opacity-0 data-[starting-style]:scale-[0.98] data-[starting-style]:opacity-0" > <:chevron> <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M6 12V4l4.5 4z" /></svg> </:chevron> <.menu_item class="flex cursor-default py-2 pr-6 pl-4 text-sm select-none outline-hidden data-[highlighted]:relative data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:-z-10 data-[highlighted]:before:bg-neutral-950 dark:data-[highlighted]:before:bg-white data-[highlighted]:text-white dark:data-[highlighted]:text-neutral-950"> Get Up! </.menu_item> <.menu_item class="flex cursor-default py-2 pr-6 pl-4 text-sm select-none outline-hidden data-[highlighted]:relative data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:-z-10 data-[highlighted]:before:bg-neutral-950 dark:data-[highlighted]:before:bg-white data-[highlighted]:text-white dark:data-[highlighted]:text-neutral-950"> Inside Out </.menu_item> <.menu_separator class="mx-1 my-1 h-px bg-neutral-950 dark:bg-white" /> <.menu_item class="flex cursor-default py-2 pr-6 pl-4 text-sm select-none outline-hidden data-[highlighted]:relative data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:-z-10 data-[highlighted]:before:bg-neutral-950 dark:data-[highlighted]:before:bg-white data-[highlighted]:text-white dark:data-[highlighted]:text-neutral-950"> New playlist… </.menu_item> </.menu_submenu> <.menu_separator class="mx-1 my-1 h-px bg-neutral-950 dark:bg-white" /> <.menu_item class="flex cursor-default py-2 pr-6 pl-4 text-sm select-none outline-hidden data-[highlighted]:relative data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:-z-10 data-[highlighted]:before:bg-neutral-950 dark:data-[highlighted]:before:bg-white data-[highlighted]:text-white dark:data-[highlighted]:text-neutral-950"> Play Next </.menu_item> <.menu_item class="flex cursor-default py-2 pr-6 pl-4 text-sm select-none outline-hidden data-[highlighted]:relative data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:-z-10 data-[highlighted]:before:bg-neutral-950 dark:data-[highlighted]:before:bg-white data-[highlighted]:text-white dark:data-[highlighted]:text-neutral-950"> Play Last </.menu_item> </.menu>
Parts & data attributes
Each part carries a stable
data-part
and a structural
chelekom-menu__*
class you can target, plus paired-presence state for open/closed transitions.
Accessibility
Follows the
WAI-ARIA Menu Button pattern
: focus moves into the popup on open and is restored to the trigger on close.
Up/Down navigate, Home/End jump to the first/last enabled row,
ArrowRight
opens a submenu,
ArrowLeft
or
Escape
closes it, and typing performs typeahead. Enter or Space activates the highlighted row.