Dropdown
Display contextual menus and option lists with smart positioning and keyboard navigation
Overview
Dropdowns display contextual menus and option lists. They're perfect for navigation menus, action lists, and settings panels. Domma's dropdown supports positioning, triggers, and dynamic content.
Key Features
- Multiple Triggers - Click or hover activation
- Smart Positioning - Auto-adjusts to viewport edges
- Keyboard Navigation - Arrow keys and Enter/Escape support
- Close on Outside Click - Automatic dismiss behaviour
Basic Usage
// Attach to a button element
const dropdown = Domma.elements.dropdown('#my-button', {
items: [
{ text: 'Edit', value: 'edit' },
{ text: 'Delete', value: 'delete' }
],
trigger: 'click',
position: 'bottom-start',
onSelect: (item) => console.log(item.value)
});
Note: The dropdown component creates menus programmatically from the items array. It doesn't use static HTML menu structures.
Getting Started
Add a dropdown menu to your page in three simple steps
Step 1: Add Trigger Button
Create a button that will trigger the dropdown:
<button id="my-dropdown" class="btn btn-primary">Actions</button>
Step 2: Include Domma
Add Domma CSS and JavaScript to your page:
<!-- CSS -->
<link rel="stylesheet" href="dist/domma.css">
<link rel="stylesheet" href="dist/elements.css">
<!-- JavaScript -->
<script src="dist/domma.min.js"></script>
Step 3: Initialize the Dropdown
Call Domma.elements.dropdown() with menu items:
Domma.elements.dropdown('#my-dropdown', {
items: [
{ text: 'Edit', value: 'edit' },
{ text: 'Duplicate', value: 'duplicate' },
{ divider: true }, // Add separator line
{ text: 'Delete', value: 'delete', class: 'text-danger' }
],
trigger: 'click', // Show on click
position: 'bottom-start', // Position below button
onSelect: (item) => {
console.log('Selected:', item.value);
}
});
Note: The dropdown auto-generates the menu from the items array (no HTML menu required). It handles positioning, keyboard navigation, and automatically closes when clicking outside. Use
divider: truefor separator lines.
Quick Start
Simple Dropdown
<button type="button" class="btn btn-primary" id="quick-dropdown">Actions</button>/**
* Simple Dropdown - from public/showcase/elements/dropdown.
*/
export function mount(root) {
E.dropdown(root.querySelector('#quick-dropdown'), {
items: [
{text: 'Edit', value: 'edit'},
{text: 'Duplicate', value: 'duplicate'},
{divider: true},
{text: 'Delete', value: 'delete', class: 'text-danger'}
],
onSelect: (item) => console.log('Selected:', item)
});
}<button class="btn btn-primary" id="my-dropdown">Actions</button>
<script>
Domma.elements.dropdown('#my-dropdown', {
items: [
{ text: 'Edit', value: 'edit' },
{ text: 'Duplicate', value: 'duplicate' },
{ divider: true },
{ text: 'Delete', value: 'delete', class: 'text-danger' }
],
onSelect: (item) => console.log('Selected:', item)
});
</script>
Tutorial
Build a user profile dropdown with event handling.
Select an option from the dropdown
✓ Action selected:
<button type="button" class="btn btn-secondary" id="tutorial-dropdown">
<span>👤</span> User Menu
</button>
<div id="tutorial-result" class="mt-4 p-3 bg-surface rounded">
<p class="text-muted text-sm" data-if="!action.value">Select an option from the dropdown</p>
<p class="text-primary text-sm" data-if="action.value">✓ Action selected: <strong data-bind-text="action.value"></strong></p>
</div>/**
* Tutorial - from public/showcase/elements/dropdown.
*/
export function mount(root) {
// Tutorial - the chosen action is an observable that #tutorial-result binds to
const action = M.observable('');
const handle = M.applyBindings({action}, root.querySelector('#tutorial-result'));
const tutorialDropdown = E.dropdown(root.querySelector('#tutorial-dropdown'), {
items: [
{header: 'Signed in as user@example.com'},
{divider: true},
{text: 'Profile', value: 'profile'},
{text: 'Settings', value: 'settings'},
{text: 'Billing', value: 'billing'},
{divider: true},
{text: 'Sign Out', value: 'logout'}
],
onOpen: () => console.log('User menu opened'),
onClose: () => console.log('User menu closed'),
onSelect: (item) => { action.value = item.value; }
});
return () => {
handle.dispose();
tutorialDropdown?.destroy?.();
};
}const dropdown = Domma.elements.dropdown('#user-dropdown', {
items: [
{ header: 'Signed in as user@example.com' },
{ divider: true },
{ text: 'Profile', value: 'profile' },
{ text: 'Settings', value: 'settings' },
{ text: 'Billing', value: 'billing' },
{ divider: true },
{ text: 'Sign Out', value: 'logout' }
],
onOpen: () => console.log('Dropdown opened'),
onClose: () => console.log('Dropdown closed'),
onSelect: (item) => {
console.log(`Action selected: ${item.value}`);
}
});
Examples
Hover Trigger
document.body, so it never touches its trigger: there is always an
offset of empty page between them. While the menu is open, Domma tracks the pointer
against three regions - the trigger, the menu, and the corridor spanning the gap - so any route
between the two counts as staying, whatever angle you take and however long you dawdle.
- Hover Hover Me, then glide down onto the items - slowly, or at an angle. It stays open.
- Stop in the gap and wait. Still open.
- Move away properly and it closes after
hoverCloseDelay(300ms). - Click Me uses a click trigger. Opening it closes the hover menu - only one dropdown is ever open at a time.
<div class="dropdown-flex-center-wrap">
<button type="button" class="btn btn-outline" id="hover-dropdown">Hover Me</button>
<button type="button" class="btn btn-primary" id="hover-click-dropdown">Click Me (same menu)</button>
</div>
<div class="alert alert-info mt-4" role="note">
<strong>How the hover path holds together.</strong> The menu is rendered on
<code>document.body</code>, so it never touches its trigger: there is always an
<code>offset</code> of empty page between them. While the menu is open, Domma tracks the pointer
against three regions - the trigger, the menu, and the corridor spanning the gap - so any route
between the two counts as staying, whatever angle you take and however long you dawdle.
<ol class="mt-2 mb-0">
<li>Hover <em>Hover Me</em>, then glide down onto the items - slowly, or at an angle. It stays open.</li>
<li>Stop in the gap and wait. Still open.</li>
<li>Move away properly and it closes after <code>hoverCloseDelay</code> (300ms).</li>
<li><em>Click Me</em> uses a click trigger. Opening it closes the hover menu - only one dropdown is
ever open at a time.</li>
</ol>
</div>/**
* Hover Trigger - from public/showcase/elements/dropdown.
*/
export function mount(root) {
// Hover Trigger
E.dropdown(root.querySelector('#hover-dropdown'), {
trigger: 'hover',
items: [
{text: 'Profile', value: 'profile'},
{text: 'Dashboard', value: 'dashboard'},
{text: 'Projects', value: 'projects'},
{text: 'Team', value: 'team'},
{text: 'Settings', value: 'settings'},
{text: 'Sign Out', value: 'signout'}
]
});
// Same menu, click trigger - confirms click stays consistent alongside hover.
E.dropdown(root.querySelector('#hover-click-dropdown'), {
trigger: 'click',
items: [
{text: 'Profile', value: 'profile'},
{text: 'Dashboard', value: 'dashboard'},
{text: 'Projects', value: 'projects'},
{text: 'Team', value: 'team'},
{text: 'Settings', value: 'settings'},
{text: 'Sign Out', value: 'signout'}
]
});
}Positioning
<div class="dropdown-flex-center-wrap">
<button type="button" class="btn btn-outline" id="pos-bottom">Bottom</button>
<button type="button" class="btn btn-outline" id="pos-top">Top</button>
<button type="button" class="btn btn-outline" id="pos-left">Left</button>
<button type="button" class="btn btn-outline" id="pos-right">Right</button>
</div>/**
* Positioning - from public/showcase/elements/dropdown.
*/
export function mount(root) {
// Positioning
E.dropdown(root.querySelector('#pos-bottom'), {
position: 'bottom-start',
items: [
{text: 'Bottom Start', value: 'b1'},
{text: 'Default Position', value: 'b2'}
]
});
E.dropdown(root.querySelector('#pos-top'), {
position: 'top-start',
items: [
{text: 'Top Start', value: 't1'},
{text: 'Upward Position', value: 't2'}
]
});
E.dropdown(root.querySelector('#pos-left'), {
position: 'left-start',
items: [
{text: 'Left Start', value: 'l1'},
{text: 'Leftward', value: 'l2'}
]
});
E.dropdown(root.querySelector('#pos-right'), {
position: 'right-start',
items: [
{text: 'Right Start', value: 'r1'},
{text: 'Rightward', value: 'r2'}
]
});
}With Dividers and Headers
<button type="button" class="btn btn-primary" id="structured-dropdown">Menu</button>/**
* With Dividers and Headers - from public/showcase/elements/dropdown.
*/
export function mount(root) {
// Structured with headers and dividers
E.dropdown(root.querySelector('#structured-dropdown'), {
items: [
{header: 'Navigation'},
{text: 'Home', value: 'home'},
{text: 'Dashboard', value: 'dashboard'},
{divider: true},
{header: 'Actions'},
{text: 'New Project', value: 'new'},
{text: 'Import', value: 'import'},
{divider: true},
{text: 'Delete All', value: 'delete-all', class: 'text-danger'}
]
});
}Programmatic Control
<button type="button" class="btn btn-secondary" id="programmatic-dropdown">Controlled Dropdown</button>
<div class="flex gap-2 justify-center mt-3">
<button type="button" id="prog-open" class="btn btn-outline btn-sm">Open</button>
<button type="button" id="prog-close" class="btn btn-outline btn-sm">Close</button>
<button type="button" id="prog-toggle" class="btn btn-outline btn-sm">Toggle</button>
</div>/**
* Programmatic Control - from public/showcase/elements/dropdown.
*/
export function mount(root) {
// Programmatic Control
const programmaticDropdown = E.dropdown(root.querySelector('#programmatic-dropdown'), {
items: [
{text: 'Item 1', value: '1'},
{text: 'Item 2', value: '2'},
{text: 'Item 3', value: '3'}
]
});
$(root).find('#prog-open').on('click', () => programmaticDropdown.open());
$(root).find('#prog-close').on('click', () => programmaticDropdown.close());
$(root).find('#prog-toggle').on('click', () => programmaticDropdown.toggle());
}Best Practices
Accessibility
- Keyboard Navigation - Use arrow keys to navigate items, Enter to select, Escape to close
- ARIA Attributes - Add
aria-haspopup="true"andaria-expandedto toggle button - Focus Management - Focus moves to first item when opened
- Screen Readers - Use semantic ```` elements for triggers
UX Guidelines
- Clear Trigger - Make dropdown button visually distinct with chevron icon
- Logical Grouping - Use dividers and headers to organise menu items
- Close on Action - Enable
closeOnClickfor action menus - Hover Delay - Add slight delay for hover triggers to prevent accidental opens
Performance
- Event Delegation - Attach single listener to menu instead of individual items
- Destroy When Done - Call
destroy()on dynamic dropdowns
API Reference
| Option | Type | Default | Description |
|---|---|---|---|
items |
Array | [] |
Menu items array. Each item: { text, value, class } or { header } or { divider: true } |
trigger |
String | 'click' |
Trigger type: 'click' or 'hover' |
position |
String | 'bottom-start' |
Side plus alignment: bottom, top, left or right, each with -start or -end |
flip |
Boolean | true |
Flip to the opposite side, and clamp to the viewport, rather than open off screen |
closeOnSelect |
Boolean | true |
Close when an item is chosen |
closeOnClickOutside |
Boolean | true |
Close when clicking outside the trigger and menu |
closeOnEscape |
Boolean | true |
Close on Esc and return focus to the trigger |
closeOthers |
Boolean | true |
Opening this dropdown closes any other open one |
hoverOpenDelay |
Number | 0 |
ms to wait before a hover opens the menu - raise it to stop a menu opening when the pointer merely sweeps across the trigger |
hoverCloseDelay |
Number | 300 |
ms of grace before a hover menu closes once the pointer has left the trigger, the menu and the space between them |
animation |
Boolean | true |
Enable animations |
animationDuration |
Number | 150 |
ms the open/close transition runs for |
offset |
Array | [0, 4] |
[x, y] gap between trigger and menu, in px |
onOpen |
Function | null |
Open callback |
onClose |
Function | null |
Close callback |
onSelect |
Function | null |
Item selection callback - receives selected item object |
Methods
| Method | Description |
|---|---|
open() |
Open the dropdown |
close() |
Close the dropdown |
toggle() |
Toggle dropdown state |
isOpen() |
Check if dropdown is open |
setItems(items) |
Replace the item list, re-rendering if the menu is open |
addItem(item) |
Append one item |
removeItem(index) |
Remove the item at index |
getSelected() |
The value of the last selected item |
destroy() |
Remove dropdown and clean up |
CSS Classes
| Class | Description |
|---|---|
.dropdown |
Container element |
.dropdown-toggle |
Trigger button |
.dropdown-menu |
Menu container |
.dropdown-item |
Menu item link |
.dropdown-divider |
Visual separator |
.dropdown-header |
Section header text |
Config Engine
$.setup({
'#user-menu': {
component: 'dropdown',
options: {
trigger: 'click',
position: 'bottom-end',
closeOnClick: true,
onOpen: () => console.log('Menu opened')
}
},
'#hover-menu': {
component: 'dropdown',
options: {
trigger: 'hover',
position: 'bottom-start'
}
}
});
CSS Customisation
Override these CSS variables to customise Dropdown appearance and match your design system.
| Variable | Default | Controls |
|---|---|---|
--dm-dropdown-bg |
var(--dm-surface) |
Dropdown background |
--dm-dropdown-border |
var(--dm-border) |
Dropdown border colour |
--dm-dropdown-item-hover |
var(--dm-hover-bg) |
Item hover background |
--dm-shadow-md |
0 4px 6px... |
Dropdown shadow |
Example Override
:root {
/* Highlight hover with primary */
--dm-dropdown-item-hover: var(--dm-primary-light);
}
Full CSS Customisation Cheat-Sheet →
Related Elements
<div class="grid grid-cols-1 gap-4 grid-auto-fit-250"><a href="/showcase/elements/tooltip" class="card card-hover">
<div class="card-body">
<h3 class="card-title">Tooltip</h3>
<p class="card-text">For simple contextual information</p>
</div>
</a>
<a href="/showcase/elements/navbar" class="card card-hover">
<div class="card-body">
<h3 class="card-title">Navbar</h3>
<p class="card-text">Navigation with built-in dropdowns</p>
</div>
</a>
</div>