overlays
Dropdown
Compound menu with keyboard navigation, left/right alignment, danger and disabled items, icons, and shortcut badges.
Copied!
Examples
Basic menu
Right aligned
<div class="flex justify-end">
<x-ui.dropdown align="right" label="Staff account">
<x-ui.dropdown.item>Profile & store access</x-ui.dropdown.item>
<x-ui.dropdown.item>Billing · IDR</x-ui.dropdown.item>
<x-ui.dropdown.separator />
<x-ui.dropdown.item variant="danger">Sign out</x-ui.dropdown.item>
</x-ui.dropdown>
</div>
<div class="flex justify-end">
<div
class="relative inline-block text-left"
x-data="{
open: false,
show() { this.open = true },
hide() { this.open = false; this.$nextTick(() => this.$refs.trigger?.focus()) },
toggle() { this.open ? this.hide() : this.show() },
}"
x-on:click.outside="if (open) hide()"
x-on:keydown.escape.window="if (open) hide()"
>
<button
type="button"
x-ref="trigger"
class="inline-flex h-9 items-center gap-2 rounded-control border border-border bg-surface-raised px-3.5 text-sm font-medium text-fg shadow-2xs transition-colors duration-150 hover:border-border-strong hover:bg-surface-sunken motion-safe:active:translate-y-px focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-surface"
aria-haspopup="menu"
x-bind:aria-expanded="open.toString()"
x-on:click="toggle()"
>
Staff account
<svg class="size-4 shrink-0 text-fg-subtle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="m6 9 6 6 6-6" /></svg>
</button>
<div
x-ref="menu"
role="menu"
x-show="open"
x-cloak
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0 scale-95 translate-y-0.5"
x-transition:enter-end="opacity-100 scale-100 translate-y-0"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100 scale-100 translate-y-0"
x-transition:leave-end="opacity-0 scale-95 translate-y-0.5"
class="absolute right-0 z-40 mt-2 min-w-48 origin-top-right rounded-card border border-border bg-surface-raised p-1 shadow-lg ring-1 ring-border/60"
>
<button type="button" role="menuitem" tabindex="-1" class="flex min-h-10 w-full items-center rounded-control px-3 py-2.5 text-left text-sm text-fg hover:bg-surface-sunken focus-visible:ring-2 focus-visible:ring-ring" x-on:click="hide()" x-on:keydown.enter.prevent="hide()" x-on:keydown.space.prevent="hide()">Profile & store access</button>
<button type="button" role="menuitem" tabindex="-1" class="flex min-h-10 w-full items-center rounded-control px-3 py-2.5 text-left text-sm text-fg hover:bg-surface-sunken focus-visible:ring-2 focus-visible:ring-ring" x-on:click="hide()" x-on:keydown.enter.prevent="hide()" x-on:keydown.space.prevent="hide()">Billing · IDR</button>
<div class="my-1 h-px bg-border" role="separator"></div>
<button type="button" role="menuitem" tabindex="-1" class="flex min-h-10 w-full items-center rounded-control px-3 py-2.5 text-left text-sm text-danger hover:bg-danger-soft focus-visible:ring-2 focus-visible:ring-ring" x-on:click="hide()" x-on:keydown.enter.prevent="hide()" x-on:keydown.space.prevent="hide()">Sign out</button>
</div>
</div>
</div>
Disabled item
<x-ui.dropdown label="More">
<x-ui.dropdown.item>Share link</x-ui.dropdown.item>
<x-ui.dropdown.item variant="disabled">Archive (needs Owner)</x-ui.dropdown.item>
<x-ui.dropdown.item>Export CSV</x-ui.dropdown.item>
</x-ui.dropdown>
<div
class="relative inline-block text-left"
x-data="{
open: false,
show() { this.open = true },
hide() { this.open = false; this.$nextTick(() => this.$refs.trigger?.focus()) },
toggle() { this.open ? this.hide() : this.show() },
}"
x-on:click.outside="if (open) hide()"
x-on:keydown.escape.window="if (open) hide()"
>
<button
type="button"
x-ref="trigger"
class="inline-flex h-9 items-center gap-2 rounded-control border border-border bg-surface-raised px-3.5 text-sm font-medium text-fg shadow-2xs transition-colors duration-150 hover:border-border-strong hover:bg-surface-sunken focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-surface"
aria-haspopup="menu"
x-bind:aria-expanded="open.toString()"
x-on:click="toggle()"
>
More
</button>
<div
x-ref="menu"
role="menu"
x-show="open"
x-cloak
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0 scale-95 translate-y-0.5"
x-transition:enter-end="opacity-100 scale-100 translate-y-0"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100 scale-100 translate-y-0"
x-transition:leave-end="opacity-0 scale-95 translate-y-0.5"
class="absolute left-0 z-40 mt-2 min-w-48 origin-top-left rounded-card border border-border bg-surface-raised p-1 shadow-lg ring-1 ring-border/60"
>
<button type="button" role="menuitem" tabindex="-1" class="flex min-h-10 w-full items-center rounded-control px-3 py-2.5 text-left text-sm text-fg hover:bg-surface-sunken focus-visible:ring-2 focus-visible:ring-ring" x-on:click="hide()" x-on:keydown.enter.prevent="hide()" x-on:keydown.space.prevent="hide()">Share link</button>
<button type="button" role="menuitem" tabindex="-1" disabled aria-disabled="true" class="flex min-h-10 w-full cursor-not-allowed items-center rounded-control px-3 py-2.5 text-left text-sm text-fg-subtle opacity-60">Archive (needs Owner)</button>
<button type="button" role="menuitem" tabindex="-1" class="flex min-h-10 w-full items-center rounded-control px-3 py-2.5 text-left text-sm text-fg hover:bg-surface-sunken focus-visible:ring-2 focus-visible:ring-ring" x-on:click="hide()" x-on:keydown.enter.prevent="hide()" x-on:keydown.space.prevent="hide()">Export CSV</button>
</div>
</div>
Props API
| Prop | Type | Default | Description |
|---|---|---|---|
align
Type
left|right
Default
left
|
left|right
|
left
|
Menu alignment relative to the trigger. |
label
Type
string
Default
Open menu
|
string
|
Open menu
|
Default trigger label when the trigger slot is omitted. |
item.variant
Type
default|danger|disabled
Default
default
|
default|danger|disabled
|
default
|
Visual and interactive treatment for dropdown items. |
item.shortcut
Type
string|null
Default
null
|
string|null
|
null
|
Optional keyboard shortcut badge shown on the trailing edge. |
item.href
Type
string|null
Default
null
|
string|null
|
null
|
When set, the item renders as an anchor menuitem. |
Accessibility notes
Architected with WCAG 2.2 AA patterns in mind. These notes document the exact keyboard and ARIA behavior for this component.
- Trigger exposes aria-haspopup="menu" and x-bind:aria-expanded. The menu uses role="menu" and items use role="menuitem".
- ArrowDown and ArrowUp move between enabled items, skipping disabled ones. Home and End jump to the first and last enabled item.
- Escape closes the menu and restores focus to the trigger. Tab closes without forcing focus restoration so natural tab order continues.
- Click outside closes the menu. Opening with ArrowDown on the trigger focuses the first enabled item.
- Disabled items set disabled and aria-disabled="true" and are excluded from the keyboard roving list.
Built by Praesepe
Architecture & custom software partner
Praesepe uses these exact UI primitives to ship SME dashboards, inventory systems, invoicing desks, and internal tools faster, without rebuilding a design system for every client.
- Same Blade + Tailwind 4 + Alpine 3 stack you see in this docs site
- Opinionated for Indonesian SME ops: invoices, stock, roles, approvals
- Architected with WCAG 2.2 AA patterns in mind for keyboard and ARIA