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: true for separator lines.

Quick Start

Simple Dropdown

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.

Tutorial

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

Hover Trigger
How the hover path holds together. The menu is rendered on 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.
  1. Hover Hover Me, then glide down onto the items - slowly, or at an angle. It stays open.
  2. Stop in the gap and wait. Still open.
  3. Move away properly and it closes after hoverCloseDelay (300ms).
  4. 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

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

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

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" and aria-expanded to 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 closeOnClick for 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

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>