Overview of the Phoenix and LiveView Headless Context Menu Component
The Mishka Chelekom Headless Context Menu
ships zero opinionated styling
- only the semantic markup, full WAI-ARIA
wiring, and paired-presence state
(
data-open
/
data-closed
).
Right-click or long-press the trigger
area to open the popup
at the cursor, clamped to the viewport. It is composed from per-row function
components - item, checkbox item, radio group / radio item, link item, separator, group, and
submenu
- so you can either use the idiomatic
:item
/
:submenu
slots for flat rows or compose the
context_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 Context Menu example - flat items (Phoenix headless)
<.context_menu id="baseui-context_menu-hero" trigger_class="flex h-[12rem] w-[15rem] items-center justify-center rounded-none border border-neutral-950 bg-white text-neutral-950 select-none font-normal dark:border-white dark:bg-neutral-950 dark:text-white" popup_class="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-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 dark:border-white dark:bg-neutral-950 dark:text-white dark:shadow-none" > <:trigger>Right click here</:trigger> <:item class="flex cursor-default py-2 pr-8 pl-4 text-sm leading-4 outline-hidden 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-[''] data-[disabled]:text-neutral-500 dark:data-[highlighted]:text-neutral-950 dark:data-[highlighted]:before:bg-white dark:data-[disabled]:text-neutral-400"> Add to Library </:item> <:item class="flex cursor-default py-2 pr-8 pl-4 text-sm leading-4 outline-hidden 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-[''] data-[disabled]:text-neutral-500 dark:data-[highlighted]:text-neutral-950 dark:data-[highlighted]:before:bg-white dark:data-[disabled]:text-neutral-400"> 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 leading-4 outline-hidden 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-[''] data-[disabled]:text-neutral-500 dark:data-[highlighted]:text-neutral-950 dark:data-[highlighted]:before:bg-white dark:data-[disabled]:text-neutral-400"> Play Next </:item> <:item class="flex cursor-default py-2 pr-8 pl-4 text-sm leading-4 outline-hidden 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-[''] data-[disabled]:text-neutral-500 dark:data-[highlighted]:text-neutral-950 dark:data-[highlighted]:before:bg-white dark:data-[disabled]:text-neutral-400"> 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 leading-4 outline-hidden 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-[''] data-[disabled]:text-neutral-500 dark:data-[highlighted]:text-neutral-950 dark:data-[highlighted]:before:bg-white dark:data-[disabled]:text-neutral-400"> Favorite </:item> <:item class="flex cursor-default py-2 pr-8 pl-4 text-sm leading-4 outline-hidden 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-[''] data-[disabled]:text-neutral-500 dark:data-[highlighted]:text-neutral-950 dark:data-[highlighted]:before:bg-white dark:data-[disabled]:text-neutral-400"> Share </:item> </.context_menu>
Base UI Context Menu example - nested submenu (Phoenix headless)
<.context_menu id="baseui-context_menu-submenu" trigger_class="flex h-[12rem] w-[15rem] items-center justify-center rounded-none border border-neutral-950 bg-white text-neutral-950 select-none font-normal dark:border-white dark:bg-neutral-950 dark:text-white" popup_class="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-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 dark:border-white dark:bg-neutral-950 dark:text-white dark:shadow-none" > <:trigger>Right click here</:trigger> <.context_menu_item class="flex cursor-default py-2 pr-6 pl-4 text-sm leading-4 outline-hidden select-none data-[popup-open]:relative data-[popup-open]:z-0 data-[popup-open]:before:absolute data-[popup-open]:before:inset-x-1 data-[popup-open]:before:inset-y-0 data-[popup-open]:before:z-[-1] data-[popup-open]:before:bg-neutral-100 data-[popup-open]:before:content-[''] 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-[''] data-[highlighted]:data-[popup-open]:before:bg-neutral-950 data-[disabled]:text-neutral-500 dark:data-[popup-open]:before:bg-neutral-800 dark:data-[highlighted]:text-neutral-950 dark:data-[highlighted]:before:bg-white dark:data-[highlighted]:data-[popup-open]:before:bg-white dark:data-[disabled]:text-neutral-400"> Add to Library </.context_menu_item> <.context_menu_submenu id="baseui-context_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 leading-4 outline-hidden select-none data-[popup-open]:relative data-[popup-open]:z-0 data-[popup-open]:before:absolute data-[popup-open]:before:inset-x-1 data-[popup-open]:before:inset-y-0 data-[popup-open]:before:z-[-1] data-[popup-open]:before:bg-neutral-100 data-[popup-open]:before:content-[''] 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-[''] data-[highlighted]:data-[popup-open]:before:bg-neutral-950 data-[disabled]:text-neutral-500 dark:data-[popup-open]:before:bg-neutral-800 dark:data-[highlighted]:text-neutral-950 dark:data-[highlighted]:before:bg-white dark:data-[highlighted]:data-[popup-open]:before:bg-white dark:data-[disabled]:text-neutral-400" popup_class="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-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 dark:border-white dark:bg-neutral-950 dark:text-white dark:shadow-none" > <:chevron> <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" style="display: block"> <path d="M6 12V4l4.5 4z" /> </svg> </:chevron> <.context_menu_item class="flex cursor-default py-2 pr-6 pl-4 text-sm leading-4 outline-hidden 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-['']"> Get Up! </.context_menu_item> <.context_menu_item class="flex cursor-default py-2 pr-6 pl-4 text-sm leading-4 outline-hidden 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-['']"> Inside Out </.context_menu_item> <.context_menu_item class="flex cursor-default py-2 pr-6 pl-4 text-sm leading-4 outline-hidden 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-['']"> Night Beats </.context_menu_item> <.context_menu_separator class="mx-1 my-1 h-px bg-neutral-950 dark:bg-white" /> <.context_menu_item class="flex cursor-default py-2 pr-6 pl-4 text-sm leading-4 outline-hidden 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-['']"> New playlist… </.context_menu_item> </.context_menu_submenu> <.context_menu_separator class="mx-1 my-1 h-px bg-neutral-950 dark:bg-white" /> <.context_menu_item class="flex cursor-default py-2 pr-6 pl-4 text-sm leading-4 outline-hidden 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-['']"> Play Next </.context_menu_item> <.context_menu_item class="flex cursor-default py-2 pr-6 pl-4 text-sm leading-4 outline-hidden 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-['']"> Play Last </.context_menu_item> <.context_menu_separator class="mx-1 my-1 h-px bg-neutral-950 dark:bg-white" /> <.context_menu_item class="flex cursor-default py-2 pr-6 pl-4 text-sm leading-4 outline-hidden 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-['']"> Favorite </.context_menu_item> <.context_menu_item class="flex cursor-default py-2 pr-6 pl-4 text-sm leading-4 outline-hidden 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-['']"> Share </.context_menu_item> </.context_menu>
Parts & data attributes
Each part carries a stable
data-part
and a structural
chelekom-context_menu__*
class you can target, plus paired-presence state for open/closed transitions.
Accessibility
Follows the
WAI-ARIA Menu pattern
: focus moves into the popup on open and is restored to the previous element 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.