Skip to content

Dropmenu

<loomi-dropmenu> is for quick actions: profile menus, row actions, account menus, and small command lists. It is different from <loomi-select>: a select stores a value for a form, while a dropmenu simply shows actions the user can choose.

Terminal window
npm install @loomidev/dropmenu lit
import "@loomidev/dropmenu";

Import the package once, then place <loomi-dropmenu-item> elements inside the menu. The default trigger is a horizontal ellipsis.

Invite to Project Assign Task Send Message
<loomi-dropmenu>
<loomi-dropmenu-item>Invite to Project</loomi-dropmenu-item>
<loomi-dropmenu-item>Assign Task</loomi-dropmenu-item>
<loomi-dropmenu-item>Send Message</loomi-dropmenu-item>
</loomi-dropmenu>

Swap the default ellipsis for any icon from @loomidev/icons. You can write the icon name with or without the -icon suffix.

Add to playlist Play again Company settings User settings
<loomi-dropmenu trigger="musical-note">
<loomi-dropmenu-item>Add to playlist</loomi-dropmenu-item>
<loomi-dropmenu-item>Play again</loomi-dropmenu-item>
</loomi-dropmenu>
<loomi-dropmenu trigger="cog-6-tooth">
<loomi-dropmenu-item>Company settings</loomi-dropmenu-item>
<loomi-dropmenu-item>User settings</loomi-dropmenu-item>
</loomi-dropmenu>

Menus open on click by default. Use trigger-on="mouseover" when the surrounding UI already behaves like a hover menu.

Add to playlist Play again
<loomi-dropmenu trigger="musical-note" trigger-on="mouseover">
<loomi-dropmenu-item>Add to playlist</loomi-dropmenu-item>
<loomi-dropmenu-item>Play again</loomi-dropmenu-item>
</loomi-dropmenu>

Use the trigger slot when the trigger should look like text, an avatar row, or a custom control. The component wraps this slot in its own button, so keep the slotted markup non-interactive.

Options Add to playlist Play again
John C. Doe
Tech, IT Support
Deactivate my account Delete Profile
<loomi-dropmenu>
<span slot="trigger" style="font-weight:600">Options</span>
<loomi-dropmenu-item>Add to playlist</loomi-dropmenu-item>
<loomi-dropmenu-item>Play again</loomi-dropmenu-item>
</loomi-dropmenu>
<loomi-dropmenu>
<div slot="trigger" style="display:flex;align-items:center;gap:0.5rem;box-shadow:0 1px 2px rgb(0 0 0 / 0.1);padding:0 1rem;border-radius:0.375rem">
<loomi-avatar size="small" image="/john.jpg"></loomi-avatar>
<div>
<div><strong>John C. Doe</strong></div>
<div style="font-size:0.875rem">Tech, IT Support</div>
</div>
<loomi-icon name="chevron-down" style="width:1rem;height:1rem"></loomi-icon>
</div>
<loomi-dropmenu-item>Deactivate my account</loomi-dropmenu-item>
<loomi-dropmenu-item>Delete Profile</loomi-dropmenu-item>
</loomi-dropmenu>

Each <loomi-dropmenu-item> can be plain text, a link, or markup that you handle with a regular click listener.

Go to Library Show a Modal
<loomi-dropmenu trigger="light-bulb">
<loomi-dropmenu-item><a href="/library" target="_blank">Go to Library</a></loomi-dropmenu-item>
<loomi-dropmenu-item id="show-modal-item">Show a Modal</loomi-dropmenu-item>
</loomi-dropmenu>
<script type="module">
import { showLoomiModal } from "@loomidev/modal";
document.getElementById("show-modal-item").addEventListener("click", () => showLoomiModal("dropmenu-demo"));
</script>

Use header for a non-clickable section label. It keeps the same spacing as the rest of the menu but does not get pointer or hover behavior.

Project Invite Delete
<loomi-dropmenu>
<loomi-dropmenu-item header>Project</loomi-dropmenu-item>
<loomi-dropmenu-item icon="paper-airplane">Invite</loomi-dropmenu-item>
<loomi-dropmenu-item icon="trash">Delete</loomi-dropmenu-item>
</loomi-dropmenu>
Edit Profile
<loomi-dropmenu-item icon="pencil-square">Edit Profile</loomi-dropmenu-item>

By default an item’s icon sits on the left. Set icon-right on the menu to flip every item, or set it on one item to flip just that row.

Edit Profile
<loomi-dropmenu icon-right>
<loomi-dropmenu-item icon="pencil-square">Edit Profile</loomi-dropmenu-item>
</loomi-dropmenu>
Edit Delete
<loomi-dropmenu>
<loomi-dropmenu-item>Edit</loomi-dropmenu-item>
<loomi-dropmenu-item divider></loomi-dropmenu-item>
<loomi-dropmenu-item>Delete</loomi-dropmenu-item>
</loomi-dropmenu>

Use divided on the menu itself when every row should have a thin separator.

Add to playlist Play again
<loomi-dropmenu divided>
<loomi-dropmenu-item>Add to playlist</loomi-dropmenu-item>
<loomi-dropmenu-item>Play again</loomi-dropmenu-item>
</loomi-dropmenu>

Set checkbox on an item to turn it into a toggle row. It shows a checkmark when checked, keeps the menu open on click (unlike a normal item), and fires a change event you can use to sync application state.

Appearance Status Bar Activity Bar Panel
<loomi-dropmenu>
<loomi-dropmenu-item header>Appearance</loomi-dropmenu-item>
<loomi-dropmenu-item checkbox checked>Status Bar</loomi-dropmenu-item>
<loomi-dropmenu-item checkbox>Activity Bar</loomi-dropmenu-item>
<loomi-dropmenu-item checkbox>Panel</loomi-dropmenu-item>
</loomi-dropmenu>
<script type="module">
document.querySelectorAll("loomi-dropmenu-item[checkbox]").forEach((item) => {
item.addEventListener("change", () => console.log(item.textContent.trim(), item.checked));
});
</script>

Set radio plus a shared group name to make a set of items mutually exclusive, similar to a native radio group. Give each item a value; the previously checked item in the same group is unchecked automatically.

Panel Position Top Bottom Right
<loomi-dropmenu>
<loomi-dropmenu-item header>Panel Position</loomi-dropmenu-item>
<loomi-dropmenu-item radio group="position" value="top">Top</loomi-dropmenu-item>
<loomi-dropmenu-item radio group="position" value="bottom" checked>Bottom</loomi-dropmenu-item>
<loomi-dropmenu-item radio group="position" value="right">Right</loomi-dropmenu-item>
</loomi-dropmenu>
<script type="module">
document.querySelectorAll("loomi-dropmenu-item[radio]").forEach((item) => {
item.addEventListener("change", () => console.log("position:", item.value));
});
</script>

Set disabled on an item to make it non-interactive: it’s skipped by arrow-key navigation, dimmed, and clicks on it are blocked.

GitHub Support API
<loomi-dropmenu>
<loomi-dropmenu-item>GitHub</loomi-dropmenu-item>
<loomi-dropmenu-item>Support</loomi-dropmenu-item>
<loomi-dropmenu-item disabled>API</loomi-dropmenu-item>
</loomi-dropmenu>

Set variant="destructive" for irreversible actions like deleting a resource. It tints the label, icon, and hover state red.

Edit Delete
<loomi-dropmenu>
<loomi-dropmenu-item icon="pencil-square">Edit</loomi-dropmenu-item>
<loomi-dropmenu-item divider></loomi-dropmenu-item>
<loomi-dropmenu-item variant="destructive" icon="trash">Delete</loomi-dropmenu-item>
</loomi-dropmenu>

Use shortcut to show a keyboard shortcut or command hint on the right side of an item, like ⌘S or ⌘K>P.

View profile Settings Dark mode
<loomi-dropmenu>
<loomi-dropmenu-item icon="user" shortcut="⌘K>P">View profile</loomi-dropmenu-item>
<loomi-dropmenu-item icon="cog-6-tooth" shortcut="⌘S">Settings</loomi-dropmenu-item>
<loomi-dropmenu-item icon="moon">Dark mode</loomi-dropmenu-item>
</loomi-dropmenu>

The shortcut text is a visual hint. Wire the actual keyboard command in your app, then trigger the same action you use for the item click.

Add submenu items with slot="submenu" inside the parent item. The parent keeps the menu open and shows a chevron automatically.

View profile Settings Support Documentation Contact support System status API API keys Webhooks
<loomi-dropmenu>
<loomi-dropmenu-item icon="user" shortcut="⌘K>P">View profile</loomi-dropmenu-item>
<loomi-dropmenu-item icon="cog-6-tooth" shortcut="⌘S">Settings</loomi-dropmenu-item>
<loomi-dropmenu-item icon="question-mark-circle">
Support
<loomi-dropmenu-item slot="submenu">Documentation</loomi-dropmenu-item>
<loomi-dropmenu-item slot="submenu">Contact support</loomi-dropmenu-item>
<loomi-dropmenu-item slot="submenu">System status</loomi-dropmenu-item>
</loomi-dropmenu-item>
<loomi-dropmenu-item icon="cube">
API
<loomi-dropmenu-item slot="submenu">API keys</loomi-dropmenu-item>
<loomi-dropmenu-item slot="submenu">Webhooks</loomi-dropmenu-item>
</loomi-dropmenu-item>
</loomi-dropmenu>

The panel is placed against the viewport, not inside the component’s own box, and is promoted to the top layer with the popover API. Two consequences worth knowing:

  • No ancestor can clip it. A menu opened from the last row of a table with overflow: auto, from inside a card with overflow: hidden, or from a sticky toolbar opens whole. This is what makes <loomi-dropmenu> usable for row-level action menus; nothing has to be teleported to <body> by hand.
  • It flips and shifts to stay on screen. With no room below the trigger the panel opens upward instead, and the arrow moves to the underside to keep pointing at the trigger. Alignment swaps the same way near a left or right edge.

placement is therefore a preference, not a guarantee — a panel that would leave the viewport still moves.

<loomi-dropmenu placement="auto"></loomi-dropmenu>
<loomi-dropmenu placement="left"></loomi-dropmenu>
<loomi-dropmenu placement="right"></loomi-dropmenu>

left and right align the panel’s left or right edge with the trigger, opening downward. To choose the side as well, use the same bottom-*/top-* names the other floating panels take (<loomi-split-button>, <loomi-popover>) — start/end are the left/right edges:

<loomi-dropmenu placement="top-end"></loomi-dropmenu>
placementAlignmentOpens
autoleft edge (left)downward, flips up
leftleft edgedownward, flips up
rightright edgedownward, flips up
bottom-startleft edgedownward, flips up
bottom-endright edgedownward, flips up
top-startleft edgeupward, flips down
top-endright edgeupward, flips down

The panel is exposed as the menu part, and the arrow’s minimum distance from the panel’s corners is --loomi-dropmenu-arrow-inset:

loomi-dropmenu::part(menu) {
--loomi-dropmenu-arrow-inset: 1rem;
min-width: 14rem;
}

Because the panel lives in the top layer, --loomi-dropmenu-z-index only matters as a fallback on browsers without popover support.

A submenu is a floating panel in its own right, on the same terms as the menu: it opens beside the row that owns it, flips to that row’s left when it would run off the right of the screen, slides up when it is taller than the room beneath the row, and is in the top layer — so scrollable menus don’t clip it either. A nested submenu follows whichever side its parent settled on, rather than zig-zagging back across it.

The resolved side is published as data-side="left" | "right" on the submenu, and the submenu is exposed as the submenu part.

Submenus open on hover and on keyboard focus, and close a moment after the pointer leaves both the row and the panel — the delay is what lets you cross the gap between them.

For long lists, cap the menu’s height and let the menu body scroll.

Item 1 Item 2
<loomi-dropmenu scrollable height="150">
<loomi-dropmenu-item>Item 1</loomi-dropmenu-item>
<loomi-dropmenu-item>Item 2</loomi-dropmenu-item>
<!-- … -->
</loomi-dropmenu>

By default, clicking an item closes the menu. Set hide-after-click="false" when the items contain controls such as toggles or checkboxes.

Email notifications SMS notifications
<loomi-dropmenu hide-after-click="false">
<loomi-dropmenu-item><loomi-checkbox>Email notifications</loomi-checkbox></loomi-dropmenu-item>
<loomi-dropmenu-item><loomi-checkbox>SMS notifications</loomi-checkbox></loomi-dropmenu-item>
</loomi-dropmenu>

See <loomi-bell>’s README for a worked example of <loomi-dropmenu> as a notifications panel.

The panel drops in from the trigger and plays the reverse on close — a panel that flipped above its trigger rises in and sinks back down instead, so the motion always reads as coming out of and returning to the trigger. Submenus fade. prefers-reduced-motion: reduce shortens all of it to near-zero.

The menu counts as closed the moment it is dismissed — isOpen is false, listeners are released, focus goes back to the trigger — and only the panel’s own visibility waits for the animation. While it does, the panel keeps its open class, gains a closing one, and stops taking pointer events so a click can’t land on a menu that is leaving.

For the library-wide baseline, see Foundations — Accessibility.

For the shared container and viewport rules, see Foundations — Responsive behavior.

For theme activation, token overrides, and contrast guidance, see Foundations — Dark mode.

AttributeDefaultDescription
trigger(ellipsis)Icon name for the trigger. The -icon suffix is optional.
trigger-onclickOpen interaction: click or mouseover.
placementautoPanel placement (see above). auto | left | right | bottom-start | bottom-end | top-start | top-end
dividedfalseDivider lines between items. (boolean)
scrollablefalseScroll items past height. (boolean)
height200Max menu height (px) when scrollable.
hide-after-clicktrueClose the menu after an item click. (boolean)
icon-rightfalsePlace every item’s icon after its label. (boolean)
AttributeDefaultDescription
icon(blank)Leading icon name.
shortcut(blank)Right-aligned keyboard shortcut hint.
icon-rightfalsePlace the icon after the label. (boolean)
headerfalseNon-clickable section header. (boolean)
dividerfalseRender a divider line. (boolean)
hovertrueEnable hover styling for a normal item. (boolean)
disabledfalseSkip navigation/clicks and dim the row. (boolean)
variantdefaultdefault | destructive (tints the row red).
checkboxfalseRender as a checkbox row; toggles checked on click. (boolean)
radiofalseRender as a radio row; use with group and value. (boolean)
group(blank)Shared name that makes radio items mutually exclusive.
value(blank)Value carried by a radio item.
checkedfalseCurrent state of a checkbox/radio item. (boolean)
SlotDescription
(default)Content placed inside the component.
submenuNested menu items.
triggerCustom content used to open the component.
MethodDescription
show()Open the menu.
hide()Close the menu.
isOpen(getter) true while the menu is open.
focus()Focus the trigger — e.g. to hand focus back after a dialog it opened closes.
blur()Blur the trigger.
EventDescription
changeFired when the value is committed or changed.
Profile View profile Settings Support Documentation Contact support Sign out
<loomi-dropmenu placement="right">
<loomi-dropmenu-item header>Profile</loomi-dropmenu-item>
<loomi-dropmenu-item icon="user" shortcut="⌘K>P">View profile</loomi-dropmenu-item>
<loomi-dropmenu-item icon="cog-6-tooth" shortcut="⌘S">Settings</loomi-dropmenu-item>
<loomi-dropmenu-item icon="question-mark-circle">
Support
<loomi-dropmenu-item slot="submenu">Documentation</loomi-dropmenu-item>
<loomi-dropmenu-item slot="submenu">Contact support</loomi-dropmenu-item>
</loomi-dropmenu-item>
<loomi-dropmenu-item divider></loomi-dropmenu-item>
<loomi-dropmenu-item icon="log-out-01">Sign out</loomi-dropmenu-item>
</loomi-dropmenu>

<loomi-dropmenu-item> and <loomi-dropmenu> are standard custom elements, so the browser can use them in plain HTML, Blade, React, Vue, Angular, Svelte, Astro, and most other frameworks. The important beginner rule is: install the package, import it once before the tag is rendered, then write the Loomi tag in your template.

Run install commands from the app where you want to use this component. That means the folder that contains that app’s package.json. Do not run these install commands from packages/dropmenu unless you are editing LoomiUI itself.

Terminal window
cd /path/to/your-app
npm install @loomidev/dropmenu lit

If you are contributing to LoomiUI itself, first move to the top-level components folder. That is where the main package.json for all packages lives, and pnpm --filter ... commands should be run from there:

Terminal window
cd /path/to/your-copy-of-loomiui/components
pnpm --filter @loomidev/dropmenu build
pnpm --filter @loomidev/dropmenu typecheck

Use the CDN version for prototypes, documentation pages, or a quick reproduction. The import map tells the browser where to find Lit, which Loomi components use internally.

<script type="importmap">
{ "imports": { "lit": "https://esm.sh/lit@3.3.3", "lit/": "https://esm.sh/lit@3.3.3/" } }
</script>
<script type="module" src="https://esm.sh/@loomidev/dropmenu"></script>
<loomi-dropmenu>
<span slot="trigger" style="font-weight:600">Actions</span>
<loomi-dropmenu-item icon="pencil-square">Edit</loomi-dropmenu-item>
<loomi-dropmenu-item icon="trash">Delete</loomi-dropmenu-item>
</loomi-dropmenu>

In Vite, Webpack, Parcel, Rollup, or a framework build pipeline, install the package and import it once in your main app JavaScript file. After that, you can use the Loomi tag anywhere in your app.

import "@loomidev/dropmenu";

Run the install command from your Laravel project root, then import the component in resources/js/app.js. If your project uses Laravel Vite, npm run dev and npm run build should also be run from the Laravel project root.

Terminal window
cd /path/to/your-laravel-app
npm install @loomidev/dropmenu lit
npm run dev
resources/js/app.js
import "@loomidev/dropmenu";
<loomi-dropmenu>
<span slot="trigger" style="font-weight:600">Actions</span>
<loomi-dropmenu-item icon="pencil-square">Edit</loomi-dropmenu-item>
<loomi-dropmenu-item icon="trash">Delete</loomi-dropmenu-item>
</loomi-dropmenu>

React can render Loomi tags directly. If you are on React 18, or if you need to pass arrays, objects, or functions, use a ref and assign those values after the component mounts.

import "@loomidev/dropmenu";
export function LoomiExample() {
return (
<loomi-dropmenu>
<span slot="trigger" style="font-weight:600">Actions</span>
<loomi-dropmenu-item icon="pencil-square">Edit</loomi-dropmenu-item>
<loomi-dropmenu-item icon="trash">Delete</loomi-dropmenu-item>
</loomi-dropmenu>
);
}

If TypeScript does not recognize the Loomi tag in JSX, add it to your app’s JSX type declarations.

Import the package in the component that uses it, or once in your main Vue file. Vue templates can use Loomi tags directly. For arrays, objects, or functions, pass the value as a JavaScript property instead of as plain text.

<script setup>
import "@loomidev/dropmenu";
</script>
<template>
<loomi-dropmenu>
<span slot="trigger" style="font-weight:600">Actions</span>
<loomi-dropmenu-item icon="pencil-square">Edit</loomi-dropmenu-item>
<loomi-dropmenu-item icon="trash">Delete</loomi-dropmenu-item>
</loomi-dropmenu>
</template>

If Vue warns that the tag is an unknown component, configure compilerOptions.isCustomElement for tags that start with loomi- in your Vite or Vue config.

Import the package once and tell Angular to allow custom HTML tags with CUSTOM_ELEMENTS_SCHEMA. For NgModule apps, add the schema to the module instead of the standalone component.

app.component.ts
import { CUSTOM_ELEMENTS_SCHEMA, Component } from "@angular/core";
import "@loomidev/dropmenu";
@Component({
selector: "app-root",
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<loomi-dropmenu>
<span slot="trigger" style="font-weight:600">Actions</span>
<loomi-dropmenu-item icon="pencil-square">Edit</loomi-dropmenu-item>
<loomi-dropmenu-item icon="trash">Delete</loomi-dropmenu-item>
</loomi-dropmenu>
`,
})
export class AppComponent {}

Svelte can import the package inside a component script. Astro can import it in the frontmatter of the page or layout where the tag appears.

<script>
import "@loomidev/dropmenu";
</script>
<loomi-dropmenu>
<span slot="trigger" style="font-weight:600">Actions</span>
<loomi-dropmenu-item icon="pencil-square">Edit</loomi-dropmenu-item>
<loomi-dropmenu-item icon="trash">Delete</loomi-dropmenu-item>
</loomi-dropmenu>
---
import "@loomidev/dropmenu";
---
<loomi-dropmenu>
<span slot="trigger" style="font-weight:600">Actions</span>
<loomi-dropmenu-item icon="pencil-square">Edit</loomi-dropmenu-item>
<loomi-dropmenu-item icon="trash">Delete</loomi-dropmenu-item>
</loomi-dropmenu>

Frameworks such as Next.js, Nuxt, SvelteKit, and Astro sometimes render HTML on the server before browser-only code runs. If your framework complains, move the Loomi import to client-side code. In Next.js, that usually means a component with "use client"; in Nuxt, it often means a .client.ts plugin.

  • @loomidev/core
  • @loomidev/icons