View as Markdown

Dropdown Menu

A menu triggered by a button, providing a list of actions or links.

100%
Loading...
<div class="flex items-start justify-center pt-6" style="min-height: 460px">
    <twig:DropdownMenu id="demo">
        <twig:DropdownMenu:Trigger>
            <twig:Button variant="outline" {{ ...dropdown_menu_trigger_attrs }}>Open</twig:Button>
        </twig:DropdownMenu:Trigger>
        <twig:DropdownMenu:Content class="w-56">
            <twig:DropdownMenu:Label>My Account</twig:DropdownMenu:Label>
            <twig:DropdownMenu:Separator />
            <twig:DropdownMenu:Group>
                <twig:DropdownMenu:Item>
                    <twig:ux:icon name="lucide:user" class="size-4" />
                    Profile
                    <twig:DropdownMenu:Shortcut>⇧⌘P</twig:DropdownMenu:Shortcut>
                </twig:DropdownMenu:Item>
                <twig:DropdownMenu:Item>
                    <twig:ux:icon name="lucide:credit-card" class="size-4" />
                    Billing
                    <twig:DropdownMenu:Shortcut>⌘B</twig:DropdownMenu:Shortcut>
                </twig:DropdownMenu:Item>
                <twig:DropdownMenu:Item>
                    <twig:ux:icon name="lucide:settings" class="size-4" />
                    Settings
                    <twig:DropdownMenu:Shortcut>⌘S</twig:DropdownMenu:Shortcut>
                </twig:DropdownMenu:Item>
            </twig:DropdownMenu:Group>
            <twig:DropdownMenu:Separator />
            <twig:DropdownMenu:Group>
                <twig:DropdownMenu:Item>
                    <twig:ux:icon name="lucide:users" class="size-4" />
                    Team
                </twig:DropdownMenu:Item>
                <twig:DropdownMenu:Sub>
                    <twig:DropdownMenu:SubTrigger>
                        <twig:ux:icon name="lucide:user-plus" class="size-4" />
                        Invite users
                    </twig:DropdownMenu:SubTrigger>
                    <twig:DropdownMenu:SubContent>
                        <twig:DropdownMenu:Item>
                            <twig:ux:icon name="lucide:mail" class="size-4" />
                            Email
                        </twig:DropdownMenu:Item>
                        <twig:DropdownMenu:Item>
                            <twig:ux:icon name="lucide:message-square" class="size-4" />
                            Message
                        </twig:DropdownMenu:Item>
                        <twig:DropdownMenu:Separator />
                        <twig:DropdownMenu:Item>
                            <twig:ux:icon name="lucide:ellipsis" class="size-4" />
                            More...
                        </twig:DropdownMenu:Item>
                    </twig:DropdownMenu:SubContent>
                </twig:DropdownMenu:Sub>
                <twig:DropdownMenu:Item>
                    <twig:ux:icon name="lucide:plus" class="size-4" />
                    New Team
                    <twig:DropdownMenu:Shortcut>⌘T</twig:DropdownMenu:Shortcut>
                </twig:DropdownMenu:Item>
            </twig:DropdownMenu:Group>
            <twig:DropdownMenu:Separator />
            <twig:DropdownMenu:Item disabled>
                <twig:ux:icon name="lucide:cloud" class="size-4" />
                API
            </twig:DropdownMenu:Item>
            <twig:DropdownMenu:Separator />
            <twig:DropdownMenu:Item>
                <twig:ux:icon name="lucide:log-out" class="size-4" />
                Log out
                <twig:DropdownMenu:Shortcut>⇧⌘Q</twig:DropdownMenu:Shortcut>
            </twig:DropdownMenu:Item>
        </twig:DropdownMenu:Content>
    </twig:DropdownMenu>
</div>

Installation

Note

Available since UX Toolkit 3.5.

php bin/console ux:install dropdown-menu --kit shadcn

Install the following Composer dependencies:

composer require twig/extra-bundle twig/html-extra:^3.24.0 symfony/ux-icons symfony/ux-twig-component:^3.5 tales-from-a-dev/twig-tailwind-extra:^1.3.0

Install the following front-end dependencies:

npm install shadcn tw-animate-css
# ...or with Symfony AssetMapper
php bin/console importmap:require tw-animate-css/dist/tw-animate.css shadcn/dist/tailwind.css

Copy the following file(s) into your app:

assets/controllers/dropdown_menu_controller.js
import { Controller } from '@hotwired/stimulus';

export default class extends Controller {
    static targets = ['trigger', 'content', 'item'];

    static values = {
        open: { type: Boolean, default: false },
    };

    connect() {
        this._onOutsideClick = this._onOutsideClick.bind(this);
        this._setState(this.openValue);
        if (this.openValue) {
            this.open({ focus: false });
        }
    }

    disconnect() {
        document.removeEventListener('click', this._onOutsideClick, true);
    }

    toggle(event) {
        event?.preventDefault();
        if (this.element.dataset.state === 'open') {
            this.close();
        } else {
            this.open();
        }
    }

    open({ focus = 'first' } = {}) {
        this._setState(true);
        document.addEventListener('click', this._onOutsideClick, true);

        if (focus === 'last') {
            this._focusLastItem();
        } else if (focus) {
            this._focusFirstItem();
        }
    }

    close({ focus = true } = {}) {
        this._setState(false);
        document.removeEventListener('click', this._onOutsideClick, true);

        if (focus && this.hasTriggerTarget) {
            this.triggerTarget.focus();
        }
    }

    closeFromItem(event) {
        const item = event.currentTarget;
        if (this._isItemDisabled(item)) {
            event.preventDefault();
            return;
        }

        if (item.dataset.closeOnSelect !== 'false') {
            this.close();
        }
    }

    toggleCheckbox(event) {
        const item = event.currentTarget;
        if (this._isItemDisabled(item)) {
            event.preventDefault();
            return;
        }

        const checked = item.getAttribute('aria-checked') !== 'true';
        item.setAttribute('aria-checked', String(checked));

        const indicator = item.querySelector('[data-checkbox-indicator]');
        if (indicator) {
            indicator.hidden = !checked;
        }

        if (item.dataset.closeOnSelect === 'true') {
            this.close();
        }
    }

    selectRadio(event) {
        const item = event.currentTarget;
        if (this._isItemDisabled(item)) {
            event.preventDefault();
            return;
        }

        const group = item.closest('[data-slot="dropdown-menu-radio-group"]');
        if (!group) {
            return;
        }

        group.dataset.value = item.dataset.value;

        for (const radio of group.querySelectorAll('[role="menuitemradio"]')) {
            const selected = radio.dataset.value === item.dataset.value;
            radio.setAttribute('aria-checked', String(selected));

            const indicator = radio.querySelector('[data-radio-indicator]');
            if (indicator) {
                indicator.hidden = !selected;
            }
        }

        if (item.dataset.closeOnSelect === 'true') {
            this.close();
        }
    }

    onTriggerKeydown(event) {
        if (event.key === 'ArrowDown') {
            event.preventDefault();
            this.open({ focus: 'first' });
        } else if (event.key === 'ArrowUp') {
            event.preventDefault();
            this.open({ focus: 'last' });
        }
    }

    onContentKeydown(event) {
        switch (event.key) {
            case 'Escape':
                event.preventDefault();
                this.close();
                break;
            case 'ArrowDown':
                event.preventDefault();
                this._focusNextItem();
                break;
            case 'ArrowUp':
                event.preventDefault();
                this._focusPrevItem();
                break;
            case 'Home':
                event.preventDefault();
                this._focusFirstItem();
                break;
            case 'End':
                event.preventDefault();
                this._focusLastItem();
                break;
            case 'Tab':
                this.close({ focus: false });
                break;
        }
    }

    _setState(isOpen) {
        const state = isOpen ? 'open' : 'closed';
        this.element.dataset.state = state;

        if (this.hasTriggerTarget) {
            this.triggerTarget.dataset.state = state;
            this.triggerTarget.setAttribute('aria-expanded', String(isOpen));
        }
        if (this.hasContentTarget) {
            this.contentTarget.dataset.state = state;
        }
    }

    _onOutsideClick(event) {
        if (!this.element.contains(event.target)) {
            this.close({ focus: false });
        }
    }

    _isItemDisabled(item) {
        return (
            !item ||
            item.getAttribute('aria-disabled') === 'true' ||
            item.dataset.disabled === 'true' ||
            item.hasAttribute('disabled')
        );
    }

    _enabledItems() {
        // Exclude items nested in a sub-menu: they are not part of this menu's roving focus.
        return this.itemTargets.filter(
            (item) => !this._isItemDisabled(item) && !item.closest('[data-slot="dropdown-menu-sub-content"]')
        );
    }

    _focusFirstItem() {
        this._enabledItems()[0]?.focus();
    }

    _focusLastItem() {
        const items = this._enabledItems();
        items[items.length - 1]?.focus();
    }

    _focusNextItem() {
        const items = this._enabledItems();
        if (items.length === 0) return;
        const index = items.indexOf(document.activeElement);
        items[index === -1 ? 0 : (index + 1) % items.length].focus();
    }

    _focusPrevItem() {
        const items = this._enabledItems();
        if (items.length === 0) return;
        const index = items.indexOf(document.activeElement);
        items[index === -1 ? items.length - 1 : (index - 1 + items.length) % items.length].focus();
    }
}
assets/controllers/dropdown_menu_sub_controller.js
import { Controller } from '@hotwired/stimulus';

export default class extends Controller {
    static targets = ['trigger', 'content'];

    static values = {
        openDelay: { type: Number, default: 0 },
        closeDelay: { type: Number, default: 0 },
    };

    connect() {
        this.openTimeout = null;
        this.closeTimeout = null;
        this.#setState('closed');
    }

    disconnect() {
        this.#clearTimeouts();
    }

    show() {
        this.#clearTimeouts();
        this.openTimeout = setTimeout(() => {
            this.#setState('open');
            this.openTimeout = null;
        }, this.openDelayValue);
    }

    hide() {
        this.#clearTimeouts();
        this.closeTimeout = setTimeout(() => {
            this.#setState('closed');
            this.closeTimeout = null;
        }, this.closeDelayValue);
    }

    // State lives on the trigger and content directly (not a parent group) so nested
    // sub-menus each react only to their own state.
    #setState(state) {
        this.element.dataset.state = state;
        if (this.hasTriggerTarget) {
            this.triggerTarget.dataset.state = state;
        }
        if (this.hasContentTarget) {
            this.contentTarget.dataset.state = state;
        }
    }

    #clearTimeouts() {
        if (this.openTimeout) {
            clearTimeout(this.openTimeout);
            this.openTimeout = null;
        }
        if (this.closeTimeout) {
            clearTimeout(this.closeTimeout);
            this.closeTimeout = null;
        }
    }
}
templates/components/DropdownMenu.html.twig
{%- props
    ## string Unique identifier used to generate the internal DropdownMenu IDs.
    id,
    ## boolean Whether the menu is open on initial render.
    open = false,
    ## 'top'|'right'|'bottom'|'left' Which side of the trigger the menu appears on.
    side = 'bottom',
    ## 'start'|'center'|'end' Alignment of the menu along the cross axis.
    align = 'start'
-%}
{%- do provide('dropdownMenu.open', open) -%}
{%- do provide('dropdownMenu.side', side) -%}
{%- do provide('dropdownMenu.align', align) -%}
{%- do provide('dropdownMenu.triggerId', 'dropdown-menu-' ~ id ~ '-trigger') -%}
{%- do provide('dropdownMenu.contentId', 'dropdown-menu-' ~ id ~ '-content') -%}
<div
    data-slot="dropdown-menu"
    data-state="{{ open ? 'open' : 'closed' }}"
    data-dropdown-menu-open-value="{{ open ? 'true' : 'false' }}"
    {{ attributes.defaults({
        class: 'group/dropdown-menu relative inline-block'|tailwind_classes,
        'data-controller': 'dropdown-menu',
    }) }}
>
    {##- A `DropdownMenu:Trigger` and a `DropdownMenu:Content`. -#}
    {%- block content %}{% endblock -%}
</div>
templates/components/DropdownMenu/CheckboxItem.html.twig
{%- props
    ## boolean Whether the checkbox is initially checked.
    checked = false,
    ## boolean Whether the item is disabled.
    disabled = false,
    ## boolean Whether selecting the item closes the menu.
    closeOnSelect = false
-%}
<button
    type="button"
    role="menuitemcheckbox"
    tabindex="-1"
    data-slot="dropdown-menu-checkbox-item"
    data-dropdown-menu-target="item"
    data-close-on-select="{{ closeOnSelect ? 'true' : 'false' }}"
    data-disabled="{{ disabled ? 'true' : 'false' }}"
    aria-checked="{{ checked ? 'true' : 'false' }}"
    aria-disabled="{{ disabled ? 'true' : 'false' }}"
    {%- if disabled %} disabled{% endif %}
    {{ attributes.defaults({
        class: 'relative flex w-full cursor-default select-none items-center gap-1.5 rounded-md py-1 ps-1.5 pe-8 text-sm outline-hidden transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg:not([class*=size-])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0'|tailwind_classes,
        'data-action': 'click->dropdown-menu#toggleCheckbox',
    }) }}
>
    <span class="pointer-events-none absolute end-2 flex items-center justify-center" data-checkbox-indicator {% if not checked %}hidden{% endif %}>
        <twig:ux:icon name="lucide:check" class="size-4" aria-hidden="true" />
    </span>
    {##- The label text. -#}
    {%- block content %}{% endblock -%}
</button>
templates/components/DropdownMenu/Content.html.twig
{%- set _dropdownMenu_open = inject('dropdownMenu.open', false) -%}
{%- set _dropdownMenu_side = inject('dropdownMenu.side', 'bottom') -%}
{%- set _dropdownMenu_align = inject('dropdownMenu.align', 'start') -%}
{%- set _dropdownMenu_contentId = inject('dropdownMenu.contentId', null) -%}
{%- set _dropdownMenu_triggerId = inject('dropdownMenu.triggerId', null) -%}
{%- set style = html_cva(
    base: 'invisible scale-95 opacity-0 transition-[opacity,scale,visibility] duration-100 ease-out group-data-[state=open]/dropdown-menu:transition-[opacity,scale] group-data-[state=open]/dropdown-menu:visible group-data-[state=open]/dropdown-menu:scale-100 group-data-[state=open]/dropdown-menu:opacity-100 absolute z-50 min-w-32 rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden',
    variants: {
        side: {
            top: 'bottom-full mb-2',
            bottom: 'top-full mt-2',
            left: 'right-full top-1/2 -translate-y-1/2 mr-2 origin-right',
            right: 'left-full top-1/2 -translate-y-1/2 ml-2 origin-left',
        },
    },
    compound_variants: [
        {side: ['top'], align: ['start'], class: 'start-0 ltr:origin-bottom-left rtl:origin-bottom-right'},
        {side: ['top'], align: ['center'], class: 'start-1/2 -translate-x-1/2 rtl:translate-x-1/2 origin-bottom'},
        {side: ['top'], align: ['end'], class: 'end-0 ltr:origin-bottom-right rtl:origin-bottom-left'},
        {side: ['bottom'], align: ['start'], class: 'start-0 ltr:origin-top-left rtl:origin-top-right'},
        {side: ['bottom'], align: ['center'], class: 'start-1/2 -translate-x-1/2 rtl:translate-x-1/2 origin-top'},
        {side: ['bottom'], align: ['end'], class: 'end-0 ltr:origin-top-right rtl:origin-top-left'},
    ],
) -%}
<div
    id="{{ _dropdownMenu_contentId }}"
    role="menu"
    tabindex="-1"
    aria-labelledby="{{ _dropdownMenu_triggerId }}"
    data-slot="dropdown-menu-content"
    data-dropdown-menu-target="content"
    data-state="{{ _dropdownMenu_open ? 'open' : 'closed' }}"
    data-side="{{ _dropdownMenu_side }}"
    data-align="{{ _dropdownMenu_align }}"
    data-action="keydown->dropdown-menu#onContentKeydown"
    {{ attributes.defaults({
        class: style.apply({side: _dropdownMenu_side, align: _dropdownMenu_align})|tailwind_classes,
    }) }}
>
    {##- The menu items, e.g. `DropdownMenu:Item`, `DropdownMenu:Separator`. -#}
    {%- block content %}{% endblock -%}
</div>
templates/components/DropdownMenu/Group.html.twig
<div role="group" data-slot="dropdown-menu-group" {{ attributes }}>
    {##- One or more `DropdownMenu:Item` components. -#}
    {%- block content %}{% endblock -%}
</div>
templates/components/DropdownMenu/Item.html.twig
{%- props
    ## 'button'|'a'|'div' The HTML tag to render.
    as = 'button',
    ## boolean Whether the item is disabled.
    disabled = false,
    ## boolean Whether the item is inset with extra start padding.
    inset = false,
    ## 'default'|'destructive' The visual style variant.
    variant = 'default',
    ## boolean Whether selecting the item closes the menu.
    closeOnSelect = true
-%}
{%- set style = html_cva(
    base: 'group/dropdown-menu-item relative flex w-full cursor-default select-none items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg:not([class*=size-])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0',
    variants: {
        variant: {
            default: 'hover:**:text-accent-foreground focus:**:text-accent-foreground',
            destructive: 'text-destructive hover:bg-destructive/10 hover:text-destructive focus:bg-destructive/10 focus:text-destructive dark:hover:bg-destructive/20 dark:focus:bg-destructive/20 *:[svg]:text-destructive',
        },
        inset: {
            true: 'ps-7',
        },
    },
) -%}
<{{ as }}
    role="menuitem"
    tabindex="-1"
    data-slot="dropdown-menu-item"
    data-variant="{{ variant }}"
    data-dropdown-menu-target="item"
    data-close-on-select="{{ closeOnSelect ? 'true' : 'false' }}"
    data-disabled="{{ disabled ? 'true' : 'false' }}"
    aria-disabled="{{ disabled ? 'true' : 'false' }}"
    {% if as == 'button' %}type="button"{% endif %}
    {%- if as == 'button' and disabled %} disabled{% endif %}
    {{ attributes.defaults({
        class: style.apply({variant: variant, inset: inset ? 'true' : 'false'})|tailwind_classes,
        'data-action': 'click->dropdown-menu#closeFromItem',
    }) }}
>
    {##- The item label. -#}
    {%- block content %}{% endblock -%}
</{{ as }}>
templates/components/DropdownMenu/Label.html.twig
{%- props
    ## boolean Whether the label is inset with extra start padding.
    inset = false
-%}
{%- set style = html_cva(
    base: 'px-1.5 py-1 text-xs font-medium text-muted-foreground',
    variants: {
        inset: {
            true: 'ps-7',
        },
    },
) -%}
<div
    data-slot="dropdown-menu-label"
    {{ attributes.defaults({
        class: style.apply({inset: inset ? 'true' : 'false'})|tailwind_classes,
    }) }}
>
    {##- The label text. -#}
    {%- block content %}{% endblock -%}
</div>
templates/components/DropdownMenu/RadioGroup.html.twig
{%- props
    ## string|null The currently selected value.
    value = null
-%}
<div
    role="group"
    data-slot="dropdown-menu-radio-group"
    data-value="{{ value }}"
    {{ attributes }}
>
    {##- One or more `DropdownMenu:RadioItem` components. -#}
    {%- block content %}{% endblock -%}
</div>
templates/components/DropdownMenu/RadioItem.html.twig
{%- props
    ## string The value submitted when this radio is selected.
    value,
    ## boolean Whether this radio is initially checked.
    checked = false,
    ## boolean Whether the item is disabled.
    disabled = false,
    ## boolean Whether selecting the item closes the menu.
    closeOnSelect = false
-%}
<button
    type="button"
    role="menuitemradio"
    tabindex="-1"
    data-slot="dropdown-menu-radio-item"
    data-dropdown-menu-target="item"
    data-value="{{ value }}"
    data-close-on-select="{{ closeOnSelect ? 'true' : 'false' }}"
    data-disabled="{{ disabled ? 'true' : 'false' }}"
    aria-checked="{{ checked ? 'true' : 'false' }}"
    aria-disabled="{{ disabled ? 'true' : 'false' }}"
    {%- if disabled %} disabled{% endif %}
    {{ attributes.defaults({
        class: 'relative flex w-full cursor-default select-none items-center gap-1.5 rounded-md py-1 ps-1.5 pe-8 text-sm outline-hidden transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg:not([class*=size-])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0'|tailwind_classes,
        'data-action': 'click->dropdown-menu#selectRadio',
    }) }}
>
    <span class="pointer-events-none absolute end-2 flex items-center justify-center" data-radio-indicator {% if not checked %}hidden{% endif %}>
        <twig:ux:icon name="lucide:check" class="size-4" aria-hidden="true" />
    </span>
    {##- The label text. -#}
    {%- block content %}{% endblock -%}
</button>
templates/components/DropdownMenu/Separator.html.twig
<div
    role="separator"
    aria-orientation="horizontal"
    data-slot="dropdown-menu-separator"
    {{ attributes.defaults({
        class: '-mx-1 my-1 h-px bg-border'|tailwind_classes,
    }) }}
></div>
templates/components/DropdownMenu/Shortcut.html.twig
<span
    data-slot="dropdown-menu-shortcut"
    {{ attributes.defaults({
        class: 'ms-auto text-xs tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground'|tailwind_classes,
    }) }}
>
    {##- The shortcut label, e.g. "⌘K". -#}
    {%- block content %}{% endblock -%}
</span>
templates/components/DropdownMenu/Sub.html.twig
{%- props
    ## number Delay in milliseconds before showing the sub-menu.
    openDelay = 0,
    ## number Delay in milliseconds before hiding the sub-menu.
    closeDelay = 0
-%}
<div
    data-slot="dropdown-menu-sub"
    data-dropdown-menu-sub-open-delay-value="{{ openDelay }}"
    data-dropdown-menu-sub-close-delay-value="{{ closeDelay }}"
    {{ attributes.defaults({
        class: 'relative'|tailwind_classes,
        'data-controller': 'dropdown-menu-sub',
        'data-action': 'mouseenter->dropdown-menu-sub#show mouseleave->dropdown-menu-sub#hide',
    }) }}
>
    {##- A `DropdownMenu:SubTrigger` and a `DropdownMenu:SubContent`. -#}
    {%- block content %}{% endblock -%}
</div>
templates/components/DropdownMenu/SubContent.html.twig
<div
    role="menu"
    data-slot="dropdown-menu-sub-content"
    data-dropdown-menu-sub-target="content"
    data-state="closed"
    {{ attributes.defaults({
        class: 'invisible opacity-0 transition-[opacity,translate,visibility] duration-100 ease-out data-[state=open]:visible data-[state=open]:opacity-100 data-[state=open]:translate-x-0 data-[state=closed]:ltr:-translate-x-2 data-[state=closed]:rtl:translate-x-2 absolute start-full top-0 z-50 ms-1 w-max min-w-[96px] rounded-lg bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 outline-none'|tailwind_classes,
    }) }}
>
    {##- The sub-menu items, e.g. `DropdownMenu:Item`. -#}
    {%- block content %}{% endblock -%}
</div>
templates/components/DropdownMenu/SubTrigger.html.twig
{%- props
    ## boolean Whether the trigger is inset with extra start padding.
    inset = false
-%}
{%- set style = html_cva(
    base: 'relative flex w-full cursor-default select-none items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*=size-])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0',
    variants: {
        inset: {
            true: 'ps-7',
        },
    },
) -%}
<button
    type="button"
    role="menuitem"
    tabindex="-1"
    aria-haspopup="menu"
    data-slot="dropdown-menu-sub-trigger"
    data-dropdown-menu-target="item"
    data-dropdown-menu-sub-target="trigger"
    data-state="closed"
    {{ attributes.defaults({
        class: style.apply({inset: inset ? 'true' : 'false'})|tailwind_classes,
        'data-action': 'mouseenter->dropdown-menu-sub#show focus->dropdown-menu-sub#show blur->dropdown-menu-sub#hide',
    }) }}
>
    {##- The sub-menu trigger label. -#}
    {%- block content %}{% endblock -%}
    <twig:ux:icon name="lucide:chevron-right" class="ms-auto size-4 rtl:rotate-180" aria-hidden="true" />
</button>
templates/components/DropdownMenu/Trigger.html.twig
{%- set _dropdownMenu_triggerId = inject('dropdownMenu.triggerId', null) -%}
{%- set _dropdownMenu_contentId = inject('dropdownMenu.contentId', null) -%}
{%- set dropdown_menu_trigger_attrs = {
    id: _dropdownMenu_triggerId,
    'data-slot': 'dropdown-menu-trigger',
    'data-dropdown-menu-target': 'trigger',
    'aria-haspopup': 'menu',
    'aria-controls': _dropdownMenu_contentId,
    'aria-expanded': 'false',
    'data-state': 'closed',
    'data-action': 'click->dropdown-menu#toggle keydown->dropdown-menu#onTriggerKeydown'|html_attr_type('sst'),
} -%}
{##- The trigger label (e.g., a `Button`) that opens the menu on click. -#}
{%- block content %}{% endblock -%}

Usage

<twig:DropdownMenu id="menu" side="bottom" align="start">
    <twig:DropdownMenu:Trigger>
        <twig:Button variant="outline" {{ ...dropdown_menu_trigger_attrs }}>Open</twig:Button>
    </twig:DropdownMenu:Trigger>
    <twig:DropdownMenu:Content class="w-40">
        <twig:DropdownMenu:Item>Profile</twig:DropdownMenu:Item>
        <twig:DropdownMenu:Item>Settings</twig:DropdownMenu:Item>
        <twig:DropdownMenu:Separator />
        <twig:DropdownMenu:Item>Log out</twig:DropdownMenu:Item>
    </twig:DropdownMenu:Content>
</twig:DropdownMenu>

Examples

Basic

A basic dropdown menu with labels and separators.

100%
Loading...
<div class="flex items-start justify-center pt-6" style="min-height: 300px">
    <twig:DropdownMenu id="basic">
        <twig:DropdownMenu:Trigger>
            <twig:Button variant="outline" {{ ...dropdown_menu_trigger_attrs }}>Open</twig:Button>
        </twig:DropdownMenu:Trigger>
        <twig:DropdownMenu:Content>
            <twig:DropdownMenu:Group>
                <twig:DropdownMenu:Label>My Account</twig:DropdownMenu:Label>
                <twig:DropdownMenu:Item>Profile</twig:DropdownMenu:Item>
                <twig:DropdownMenu:Item>Billing</twig:DropdownMenu:Item>
                <twig:DropdownMenu:Item>Settings</twig:DropdownMenu:Item>
            </twig:DropdownMenu:Group>
            <twig:DropdownMenu:Separator />
            <twig:DropdownMenu:Item>GitHub</twig:DropdownMenu:Item>
            <twig:DropdownMenu:Item>Support</twig:DropdownMenu:Item>
            <twig:DropdownMenu:Item disabled>API</twig:DropdownMenu:Item>
        </twig:DropdownMenu:Content>
    </twig:DropdownMenu>
</div>

Submenus

A DropdownMenu:Item can open a nested DropdownMenu:SubContent on hover or focus with DropdownMenu:Sub and DropdownMenu:SubTrigger.

100%
Loading...
<div class="flex w-[400px] items-start pt-6 ps-6" style="min-height: 340px">
    <twig:DropdownMenu id="submenu">
        <twig:DropdownMenu:Trigger>
            <twig:Button variant="outline" {{ ...dropdown_menu_trigger_attrs }}>Open</twig:Button>
        </twig:DropdownMenu:Trigger>
        <twig:DropdownMenu:Content class="w-48">
            <twig:DropdownMenu:Item>New Tab</twig:DropdownMenu:Item>
            <twig:DropdownMenu:Item>New Window</twig:DropdownMenu:Item>
            <twig:DropdownMenu:Separator />
            <twig:DropdownMenu:Sub>
                <twig:DropdownMenu:SubTrigger>Share</twig:DropdownMenu:SubTrigger>
                <twig:DropdownMenu:SubContent>
                    <twig:DropdownMenu:Item>Copy link</twig:DropdownMenu:Item>
                    <twig:DropdownMenu:Item>Email</twig:DropdownMenu:Item>
                    <twig:DropdownMenu:Sub>
                        <twig:DropdownMenu:SubTrigger>More options</twig:DropdownMenu:SubTrigger>
                        <twig:DropdownMenu:SubContent>
                            <twig:DropdownMenu:Item>Messages</twig:DropdownMenu:Item>
                            <twig:DropdownMenu:Item>Notes</twig:DropdownMenu:Item>
                        </twig:DropdownMenu:SubContent>
                    </twig:DropdownMenu:Sub>
                </twig:DropdownMenu:SubContent>
            </twig:DropdownMenu:Sub>
            <twig:DropdownMenu:Separator />
            <twig:DropdownMenu:Item>Print</twig:DropdownMenu:Item>
        </twig:DropdownMenu:Content>
    </twig:DropdownMenu>
</div>

Shortcuts

Add DropdownMenu:Shortcut to show keyboard hints.

100%
Loading...
<div class="flex items-start justify-center pt-6" style="min-height: 240px">
    <twig:DropdownMenu id="shortcuts">
        <twig:DropdownMenu:Trigger>
            <twig:Button variant="outline" {{ ...dropdown_menu_trigger_attrs }}>Open</twig:Button>
        </twig:DropdownMenu:Trigger>
        <twig:DropdownMenu:Content>
            <twig:DropdownMenu:Group>
                <twig:DropdownMenu:Label>My Account</twig:DropdownMenu:Label>
                <twig:DropdownMenu:Item>
                    Profile
                    <twig:DropdownMenu:Shortcut>⇧⌘P</twig:DropdownMenu:Shortcut>
                </twig:DropdownMenu:Item>
                <twig:DropdownMenu:Item>
                    Billing
                    <twig:DropdownMenu:Shortcut>⌘B</twig:DropdownMenu:Shortcut>
                </twig:DropdownMenu:Item>
                <twig:DropdownMenu:Item>
                    Settings
                    <twig:DropdownMenu:Shortcut>⌘S</twig:DropdownMenu:Shortcut>
                </twig:DropdownMenu:Item>
            </twig:DropdownMenu:Group>
            <twig:DropdownMenu:Separator />
            <twig:DropdownMenu:Item>
                Log out
                <twig:DropdownMenu:Shortcut>⇧⌘Q</twig:DropdownMenu:Shortcut>
            </twig:DropdownMenu:Item>
        </twig:DropdownMenu:Content>
    </twig:DropdownMenu>
</div>

Icons

Combine icons with labels for quick scanning.

100%
Loading...
<div class="flex items-start justify-center pt-6" style="min-height: 220px">
    <twig:DropdownMenu id="icons">
        <twig:DropdownMenu:Trigger>
            <twig:Button variant="outline" {{ ...dropdown_menu_trigger_attrs }}>Open</twig:Button>
        </twig:DropdownMenu:Trigger>
        <twig:DropdownMenu:Content>
            <twig:DropdownMenu:Item>
                <twig:ux:icon name="lucide:user" class="size-4" />
                Profile
            </twig:DropdownMenu:Item>
            <twig:DropdownMenu:Item>
                <twig:ux:icon name="lucide:credit-card" class="size-4" />
                Billing
            </twig:DropdownMenu:Item>
            <twig:DropdownMenu:Item>
                <twig:ux:icon name="lucide:settings" class="size-4" />
                Settings
            </twig:DropdownMenu:Item>
            <twig:DropdownMenu:Separator />
            <twig:DropdownMenu:Item variant="destructive">
                <twig:ux:icon name="lucide:log-out" class="size-4" />
                Log out
            </twig:DropdownMenu:Item>
        </twig:DropdownMenu:Content>
    </twig:DropdownMenu>
</div>

Checkboxes

100%
Loading...
<div class="flex items-start justify-center pt-6" style="min-height: 280px">
    <twig:DropdownMenu id="checkboxes">
        <twig:DropdownMenu:Trigger>
            <twig:Button variant="outline" {{ ...dropdown_menu_trigger_attrs }}>Open</twig:Button>
        </twig:DropdownMenu:Trigger>
        <twig:DropdownMenu:Content class="w-56">
            <twig:DropdownMenu:Label>Appearance</twig:DropdownMenu:Label>
            <twig:DropdownMenu:Separator />
            <twig:DropdownMenu:CheckboxItem checked>Status Bar</twig:DropdownMenu:CheckboxItem>
            <twig:DropdownMenu:CheckboxItem checked>Activity Bar</twig:DropdownMenu:CheckboxItem>
            <twig:DropdownMenu:CheckboxItem>Panel</twig:DropdownMenu:CheckboxItem>
            <twig:DropdownMenu:CheckboxItem disabled>Full Screen</twig:DropdownMenu:CheckboxItem>
        </twig:DropdownMenu:Content>
    </twig:DropdownMenu>
</div>

Checkboxes Icons

Add icons to checkbox items.

100%
Loading...
<div class="flex items-start justify-center pt-6" style="min-height: 220px">
    <twig:DropdownMenu id="checkboxes_icons">
        <twig:DropdownMenu:Trigger>
            <twig:Button variant="outline" {{ ...dropdown_menu_trigger_attrs }}>Notifications</twig:Button>
        </twig:DropdownMenu:Trigger>
        <twig:DropdownMenu:Content class="w-48">
            <twig:DropdownMenu:Group>
                <twig:DropdownMenu:Label>Notification Preferences</twig:DropdownMenu:Label>
                <twig:DropdownMenu:CheckboxItem checked>
                    <twig:ux:icon name="lucide:mail" class="size-4" />
                    Email notifications
                </twig:DropdownMenu:CheckboxItem>
                <twig:DropdownMenu:CheckboxItem>
                    <twig:ux:icon name="lucide:message-square" class="size-4" />
                    SMS notifications
                </twig:DropdownMenu:CheckboxItem>
                <twig:DropdownMenu:CheckboxItem checked>
                    <twig:ux:icon name="lucide:bell" class="size-4" />
                    Push notifications
                </twig:DropdownMenu:CheckboxItem>
            </twig:DropdownMenu:Group>
        </twig:DropdownMenu:Content>
    </twig:DropdownMenu>
</div>

Radio Group

100%
Loading...
<div class="flex items-start justify-center pt-6" style="min-height: 240px">
    <twig:DropdownMenu id="radio">
        <twig:DropdownMenu:Trigger>
            <twig:Button variant="outline" {{ ...dropdown_menu_trigger_attrs }}>Open</twig:Button>
        </twig:DropdownMenu:Trigger>
        <twig:DropdownMenu:Content class="w-56">
            <twig:DropdownMenu:Label>Panel Position</twig:DropdownMenu:Label>
            <twig:DropdownMenu:Separator />
            <twig:DropdownMenu:RadioGroup value="bottom">
                <twig:DropdownMenu:RadioItem value="top">Top</twig:DropdownMenu:RadioItem>
                <twig:DropdownMenu:RadioItem value="bottom" checked>Bottom</twig:DropdownMenu:RadioItem>
                <twig:DropdownMenu:RadioItem value="right">Right</twig:DropdownMenu:RadioItem>
            </twig:DropdownMenu:RadioGroup>
        </twig:DropdownMenu:Content>
    </twig:DropdownMenu>
</div>

Radio Icons

Show radio options with icons.

100%
Loading...
<div class="flex items-start justify-center pt-6" style="min-height: 220px">
    <twig:DropdownMenu id="radio_icons">
        <twig:DropdownMenu:Trigger>
            <twig:Button variant="outline" {{ ...dropdown_menu_trigger_attrs }}>Payment Method</twig:Button>
        </twig:DropdownMenu:Trigger>
        <twig:DropdownMenu:Content class="min-w-56">
            <twig:DropdownMenu:Group>
                <twig:DropdownMenu:Label>Select Payment Method</twig:DropdownMenu:Label>
                <twig:DropdownMenu:RadioGroup value="card">
                    <twig:DropdownMenu:RadioItem value="card" checked>
                        <twig:ux:icon name="lucide:credit-card" class="size-4" />
                        Credit Card
                    </twig:DropdownMenu:RadioItem>
                    <twig:DropdownMenu:RadioItem value="paypal">
                        <twig:ux:icon name="lucide:wallet" class="size-4" />
                        PayPal
                    </twig:DropdownMenu:RadioItem>
                    <twig:DropdownMenu:RadioItem value="bank">
                        <twig:ux:icon name="lucide:building-2" class="size-4" />
                        Bank Transfer
                    </twig:DropdownMenu:RadioItem>
                </twig:DropdownMenu:RadioGroup>
            </twig:DropdownMenu:Group>
        </twig:DropdownMenu:Content>
    </twig:DropdownMenu>
</div>

Destructive

Use variant="destructive" for irreversible actions.

100%
Loading...
<div class="flex items-start justify-center pt-6" style="min-height: 220px">
    <twig:DropdownMenu id="destructive">
        <twig:DropdownMenu:Trigger>
            <twig:Button variant="outline" {{ ...dropdown_menu_trigger_attrs }}>Actions</twig:Button>
        </twig:DropdownMenu:Trigger>
        <twig:DropdownMenu:Content>
            <twig:DropdownMenu:Group>
                <twig:DropdownMenu:Item>
                    <twig:ux:icon name="lucide:pencil" class="size-4" />
                    Edit
                </twig:DropdownMenu:Item>
                <twig:DropdownMenu:Item>
                    <twig:ux:icon name="lucide:share" class="size-4" />
                    Share
                </twig:DropdownMenu:Item>
            </twig:DropdownMenu:Group>
            <twig:DropdownMenu:Separator />
            <twig:DropdownMenu:Group>
                <twig:DropdownMenu:Item variant="destructive">
                    <twig:ux:icon name="lucide:trash" class="size-4" />
                    Delete
                </twig:DropdownMenu:Item>
            </twig:DropdownMenu:Group>
        </twig:DropdownMenu:Content>
    </twig:DropdownMenu>
</div>

Avatar

An account switcher dropdown triggered by an avatar.

100%
Loading...
<div class="flex items-start justify-center pt-6" style="min-height: 240px">
    <twig:DropdownMenu id="avatar" align="end">
        <twig:DropdownMenu:Trigger>
            <twig:Button variant="ghost" size="icon" class="rounded-full" {{ ...dropdown_menu_trigger_attrs }}>
                <twig:Avatar>
                    <twig:Avatar:Image src="https://github.com/shadcn.png" alt="shadcn" />
                    <twig:Avatar:Fallback>LR</twig:Avatar:Fallback>
                </twig:Avatar>
            </twig:Button>
        </twig:DropdownMenu:Trigger>
        <twig:DropdownMenu:Content>
            <twig:DropdownMenu:Group>
                <twig:DropdownMenu:Item>
                    <twig:ux:icon name="lucide:badge-check" class="size-4" />
                    Account
                </twig:DropdownMenu:Item>
                <twig:DropdownMenu:Item>
                    <twig:ux:icon name="lucide:credit-card" class="size-4" />
                    Billing
                </twig:DropdownMenu:Item>
                <twig:DropdownMenu:Item>
                    <twig:ux:icon name="lucide:bell" class="size-4" />
                    Notifications
                </twig:DropdownMenu:Item>
            </twig:DropdownMenu:Group>
            <twig:DropdownMenu:Separator />
            <twig:DropdownMenu:Item>
                <twig:ux:icon name="lucide:log-out" class="size-4" />
                Sign Out
            </twig:DropdownMenu:Item>
        </twig:DropdownMenu:Content>
    </twig:DropdownMenu>
</div>

Complex

A richer example combining groups, icons, and submenus.

100%
Loading...
<div class="flex items-start justify-center pt-6" style="min-height: 640px">
    <twig:DropdownMenu id="complex">
        <twig:DropdownMenu:Trigger>
            <twig:Button variant="outline" {{ ...dropdown_menu_trigger_attrs }}>Complex Menu</twig:Button>
        </twig:DropdownMenu:Trigger>
        <twig:DropdownMenu:Content class="w-44">
            <twig:DropdownMenu:Group>
                <twig:DropdownMenu:Label>File</twig:DropdownMenu:Label>
                <twig:DropdownMenu:Item>
                    <twig:ux:icon name="lucide:file" class="size-4" />
                    New File
                    <twig:DropdownMenu:Shortcut>⌘N</twig:DropdownMenu:Shortcut>
                </twig:DropdownMenu:Item>
                <twig:DropdownMenu:Item>
                    <twig:ux:icon name="lucide:folder" class="size-4" />
                    New Folder
                    <twig:DropdownMenu:Shortcut>⇧⌘N</twig:DropdownMenu:Shortcut>
                </twig:DropdownMenu:Item>
                <twig:DropdownMenu:Sub>
                    <twig:DropdownMenu:SubTrigger>
                        <twig:ux:icon name="lucide:folder-open" class="size-4" />
                        Open Recent
                    </twig:DropdownMenu:SubTrigger>
                    <twig:DropdownMenu:SubContent>
                        <twig:DropdownMenu:Group>
                            <twig:DropdownMenu:Label>Recent Projects</twig:DropdownMenu:Label>
                            <twig:DropdownMenu:Item>
                                <twig:ux:icon name="lucide:file-code" class="size-4" />
                                Project Alpha
                            </twig:DropdownMenu:Item>
                            <twig:DropdownMenu:Item>
                                <twig:ux:icon name="lucide:file-code" class="size-4" />
                                Project Beta
                            </twig:DropdownMenu:Item>
                            <twig:DropdownMenu:Sub>
                                <twig:DropdownMenu:SubTrigger>
                                    <twig:ux:icon name="lucide:ellipsis" class="size-4" />
                                    More Projects
                                </twig:DropdownMenu:SubTrigger>
                                <twig:DropdownMenu:SubContent>
                                    <twig:DropdownMenu:Item>
                                        <twig:ux:icon name="lucide:file-code" class="size-4" />
                                        Project Gamma
                                    </twig:DropdownMenu:Item>
                                    <twig:DropdownMenu:Item>
                                        <twig:ux:icon name="lucide:file-code" class="size-4" />
                                        Project Delta
                                    </twig:DropdownMenu:Item>
                                </twig:DropdownMenu:SubContent>
                            </twig:DropdownMenu:Sub>
                        </twig:DropdownMenu:Group>
                        <twig:DropdownMenu:Separator />
                        <twig:DropdownMenu:Group>
                            <twig:DropdownMenu:Item>
                                <twig:ux:icon name="lucide:folder-search" class="size-4" />
                                Browse...
                            </twig:DropdownMenu:Item>
                        </twig:DropdownMenu:Group>
                    </twig:DropdownMenu:SubContent>
                </twig:DropdownMenu:Sub>
                <twig:DropdownMenu:Separator />
                <twig:DropdownMenu:Item>
                    <twig:ux:icon name="lucide:save" class="size-4" />
                    Save
                    <twig:DropdownMenu:Shortcut>⌘S</twig:DropdownMenu:Shortcut>
                </twig:DropdownMenu:Item>
                <twig:DropdownMenu:Item>
                    <twig:ux:icon name="lucide:download" class="size-4" />
                    Export
                    <twig:DropdownMenu:Shortcut>⇧⌘E</twig:DropdownMenu:Shortcut>
                </twig:DropdownMenu:Item>
            </twig:DropdownMenu:Group>
            <twig:DropdownMenu:Separator />
            <twig:DropdownMenu:Group>
                <twig:DropdownMenu:Label>View</twig:DropdownMenu:Label>
                <twig:DropdownMenu:CheckboxItem checked>
                    <twig:ux:icon name="lucide:eye" class="size-4" />
                    Show Sidebar
                </twig:DropdownMenu:CheckboxItem>
                <twig:DropdownMenu:CheckboxItem>
                    <twig:ux:icon name="lucide:panels-top-left" class="size-4" />
                    Show Status Bar
                </twig:DropdownMenu:CheckboxItem>
                <twig:DropdownMenu:Sub>
                    <twig:DropdownMenu:SubTrigger>
                        <twig:ux:icon name="lucide:palette" class="size-4" />
                        Theme
                    </twig:DropdownMenu:SubTrigger>
                    <twig:DropdownMenu:SubContent>
                        <twig:DropdownMenu:Group>
                            <twig:DropdownMenu:Label>Appearance</twig:DropdownMenu:Label>
                            <twig:DropdownMenu:RadioGroup value="light">
                                <twig:DropdownMenu:RadioItem value="light" checked>
                                    <twig:ux:icon name="lucide:sun" class="size-4" />
                                    Light
                                </twig:DropdownMenu:RadioItem>
                                <twig:DropdownMenu:RadioItem value="dark">
                                    <twig:ux:icon name="lucide:moon" class="size-4" />
                                    Dark
                                </twig:DropdownMenu:RadioItem>
                                <twig:DropdownMenu:RadioItem value="system">
                                    <twig:ux:icon name="lucide:monitor" class="size-4" />
                                    System
                                </twig:DropdownMenu:RadioItem>
                            </twig:DropdownMenu:RadioGroup>
                        </twig:DropdownMenu:Group>
                    </twig:DropdownMenu:SubContent>
                </twig:DropdownMenu:Sub>
            </twig:DropdownMenu:Group>
            <twig:DropdownMenu:Separator />
            <twig:DropdownMenu:Group>
                <twig:DropdownMenu:Label>Account</twig:DropdownMenu:Label>
                <twig:DropdownMenu:Item>
                    <twig:ux:icon name="lucide:user" class="size-4" />
                    Profile
                    <twig:DropdownMenu:Shortcut>⇧⌘P</twig:DropdownMenu:Shortcut>
                </twig:DropdownMenu:Item>
                <twig:DropdownMenu:Item>
                    <twig:ux:icon name="lucide:credit-card" class="size-4" />
                    Billing
                </twig:DropdownMenu:Item>
                <twig:DropdownMenu:Sub>
                    <twig:DropdownMenu:SubTrigger>
                        <twig:ux:icon name="lucide:settings" class="size-4" />
                        Settings
                    </twig:DropdownMenu:SubTrigger>
                    <twig:DropdownMenu:SubContent>
                        <twig:DropdownMenu:Group>
                            <twig:DropdownMenu:Label>Preferences</twig:DropdownMenu:Label>
                            <twig:DropdownMenu:Item>
                                <twig:ux:icon name="lucide:keyboard" class="size-4" />
                                Keyboard Shortcuts
                            </twig:DropdownMenu:Item>
                            <twig:DropdownMenu:Item>
                                <twig:ux:icon name="lucide:languages" class="size-4" />
                                Language
                            </twig:DropdownMenu:Item>
                            <twig:DropdownMenu:Sub>
                                <twig:DropdownMenu:SubTrigger>
                                    <twig:ux:icon name="lucide:bell" class="size-4" />
                                    Notifications
                                </twig:DropdownMenu:SubTrigger>
                                <twig:DropdownMenu:SubContent>
                                    <twig:DropdownMenu:Group>
                                        <twig:DropdownMenu:Label>Notification Types</twig:DropdownMenu:Label>
                                        <twig:DropdownMenu:CheckboxItem checked>
                                            <twig:ux:icon name="lucide:bell" class="size-4" />
                                            Push Notifications
                                        </twig:DropdownMenu:CheckboxItem>
                                        <twig:DropdownMenu:CheckboxItem checked>
                                            <twig:ux:icon name="lucide:mail" class="size-4" />
                                            Email Notifications
                                        </twig:DropdownMenu:CheckboxItem>
                                    </twig:DropdownMenu:Group>
                                </twig:DropdownMenu:SubContent>
                            </twig:DropdownMenu:Sub>
                        </twig:DropdownMenu:Group>
                        <twig:DropdownMenu:Separator />
                        <twig:DropdownMenu:Group>
                            <twig:DropdownMenu:Item>
                                <twig:ux:icon name="lucide:shield" class="size-4" />
                                Privacy & Security
                            </twig:DropdownMenu:Item>
                        </twig:DropdownMenu:Group>
                    </twig:DropdownMenu:SubContent>
                </twig:DropdownMenu:Sub>
            </twig:DropdownMenu:Group>
            <twig:DropdownMenu:Separator />
            <twig:DropdownMenu:Group>
                <twig:DropdownMenu:Item>
                    <twig:ux:icon name="lucide:circle-help" class="size-4" />
                    Help & Support
                </twig:DropdownMenu:Item>
                <twig:DropdownMenu:Item>
                    <twig:ux:icon name="lucide:file-text" class="size-4" />
                    Documentation
                </twig:DropdownMenu:Item>
            </twig:DropdownMenu:Group>
            <twig:DropdownMenu:Separator />
            <twig:DropdownMenu:Group>
                <twig:DropdownMenu:Item variant="destructive">
                    <twig:ux:icon name="lucide:log-out" class="size-4" />
                    Sign Out
                    <twig:DropdownMenu:Shortcut>⇧⌘Q</twig:DropdownMenu:Shortcut>
                </twig:DropdownMenu:Item>
            </twig:DropdownMenu:Group>
        </twig:DropdownMenu:Content>
    </twig:DropdownMenu>
</div>

Alignment

Use the align prop on DropdownMenu to align the menu to the start, center or end of the trigger.

100%
Loading...
<div class="flex items-start justify-center pt-6" style="min-height: 260px">
    <div class="flex flex-wrap justify-center gap-8">
        <twig:DropdownMenu id="align_start" align="start">
            <twig:DropdownMenu:Trigger>
                <twig:Button variant="outline" {{ ...dropdown_menu_trigger_attrs }}>Start</twig:Button>
            </twig:DropdownMenu:Trigger>
            <twig:DropdownMenu:Content class="w-40">
                <twig:DropdownMenu:Item><twig:ux:icon name="lucide:file" class="size-4" />New File</twig:DropdownMenu:Item>
                <twig:DropdownMenu:Item><twig:ux:icon name="lucide:folder" class="size-4" />New Folder</twig:DropdownMenu:Item>
            </twig:DropdownMenu:Content>
        </twig:DropdownMenu>
        <twig:DropdownMenu id="align_center" align="center">
            <twig:DropdownMenu:Trigger>
                <twig:Button variant="outline" {{ ...dropdown_menu_trigger_attrs }}>Center</twig:Button>
            </twig:DropdownMenu:Trigger>
            <twig:DropdownMenu:Content class="w-40">
                <twig:DropdownMenu:Item><twig:ux:icon name="lucide:file" class="size-4" />New File</twig:DropdownMenu:Item>
                <twig:DropdownMenu:Item><twig:ux:icon name="lucide:folder" class="size-4" />New Folder</twig:DropdownMenu:Item>
            </twig:DropdownMenu:Content>
        </twig:DropdownMenu>
        <twig:DropdownMenu id="align_end" align="end">
            <twig:DropdownMenu:Trigger>
                <twig:Button variant="outline" {{ ...dropdown_menu_trigger_attrs }}>End</twig:Button>
            </twig:DropdownMenu:Trigger>
            <twig:DropdownMenu:Content class="w-40">
                <twig:DropdownMenu:Item><twig:ux:icon name="lucide:file" class="size-4" />New File</twig:DropdownMenu:Item>
                <twig:DropdownMenu:Item><twig:ux:icon name="lucide:folder" class="size-4" />New Folder</twig:DropdownMenu:Item>
            </twig:DropdownMenu:Content>
        </twig:DropdownMenu>
    </div>
</div>

With Dialog

An item can open a Dialog: close the menu and open the dialog from the same click.

100%
Loading...
<div class="flex items-start justify-center pt-6" style="min-height: 360px">
    <twig:Dialog id="dropdown_dialog">
        <twig:DropdownMenu id="dialog">
            <twig:DropdownMenu:Trigger>
                <twig:Button variant="outline" {{ ...dropdown_menu_trigger_attrs }}>Open</twig:Button>
            </twig:DropdownMenu:Trigger>
            <twig:DropdownMenu:Content class="w-56">
                <twig:DropdownMenu:Label>My Account</twig:DropdownMenu:Label>
                <twig:DropdownMenu:Separator />
                <twig:DropdownMenu:Item>
                    <twig:ux:icon name="lucide:user" class="size-4" />
                    Profile
                </twig:DropdownMenu:Item>
                <twig:DropdownMenu:Separator />
                <twig:DropdownMenu:Item data-action="click->dropdown-menu#close click->dialog#open">
                    <twig:ux:icon name="lucide:user-plus" class="size-4" />
                    Invite users
                </twig:DropdownMenu:Item>
            </twig:DropdownMenu:Content>
        </twig:DropdownMenu>
        <twig:Dialog:Content class="sm:max-w-[425px]">
            <twig:Dialog:Header>
                <twig:Dialog:Title>Invite team members</twig:Dialog:Title>
                <twig:Dialog:Description>Invite your team members to collaborate.</twig:Dialog:Description>
            </twig:Dialog:Header>
            <div class="grid gap-4">
                <div class="grid gap-3">
                    <twig:Label for="invite-email">Email address</twig:Label>
                    <twig:Input id="invite-email" type="email" placeholder="user@example.com" />
                </div>
            </div>
            <twig:Dialog:Footer>
                <twig:Dialog:Close>
                    <twig:Button variant="outline" {{ ...dialog_close_attrs }}>Cancel</twig:Button>
                </twig:Dialog:Close>
                <twig:Button type="submit">Send invite</twig:Button>
            </twig:Dialog:Footer>
        </twig:Dialog:Content>
    </twig:Dialog>
</div>

RTL

To enable RTL support, set the dir="rtl" attribute on the root element.

100%
Loading...
<div class="flex flex-col items-center gap-6 py-6" style="min-height: 420px">
    <div dir="rtl">
        <twig:DropdownMenu id="rtl_ar">
            <twig:DropdownMenu:Trigger>
                <twig:Button variant="outline" {{ ...dropdown_menu_trigger_attrs }}>افتح القائمة</twig:Button>
            </twig:DropdownMenu:Trigger>
            <twig:DropdownMenu:Content class="w-48">
                <twig:DropdownMenu:Item>الملف الشخصي</twig:DropdownMenu:Item>
                <twig:DropdownMenu:Sub>
                    <twig:DropdownMenu:SubTrigger>مشاركة</twig:DropdownMenu:SubTrigger>
                    <twig:DropdownMenu:SubContent>
                        <twig:DropdownMenu:Item>بريد إلكتروني</twig:DropdownMenu:Item>
                        <twig:DropdownMenu:Item>رسالة</twig:DropdownMenu:Item>
                    </twig:DropdownMenu:SubContent>
                </twig:DropdownMenu:Sub>
                <twig:DropdownMenu:Separator />
                <twig:DropdownMenu:Item>تسجيل الخروج</twig:DropdownMenu:Item>
            </twig:DropdownMenu:Content>
        </twig:DropdownMenu>
    </div>
    <div dir="rtl">
        <twig:DropdownMenu id="rtl_he">
            <twig:DropdownMenu:Trigger>
                <twig:Button variant="outline" {{ ...dropdown_menu_trigger_attrs }}>פתח תפריט</twig:Button>
            </twig:DropdownMenu:Trigger>
            <twig:DropdownMenu:Content class="w-48">
                <twig:DropdownMenu:Item>פרופיל</twig:DropdownMenu:Item>
                <twig:DropdownMenu:Sub>
                    <twig:DropdownMenu:SubTrigger>שיתוף</twig:DropdownMenu:SubTrigger>
                    <twig:DropdownMenu:SubContent>
                        <twig:DropdownMenu:Item>אימייל</twig:DropdownMenu:Item>
                        <twig:DropdownMenu:Item>הודעה</twig:DropdownMenu:Item>
                    </twig:DropdownMenu:SubContent>
                </twig:DropdownMenu:Sub>
                <twig:DropdownMenu:Separator />
                <twig:DropdownMenu:Item>התנתקות</twig:DropdownMenu:Item>
            </twig:DropdownMenu:Content>
        </twig:DropdownMenu>
    </div>
</div>

Accessibility

  • DropdownMenu:Content renders role="menu" and its entries role="menuitem", role="menuitemcheckbox" or role="menuitemradio", with aria-checked kept in sync by the controller.
  • DropdownMenu:Trigger exposes aria-haspopup="menu", an aria-expanded reflecting the open state and an aria-controls pointing at the menu.
  • ArrowDown and ArrowUp move between items and wrap around, Home and End jump to the first and last, and Escape closes the menu and returns focus to the trigger. Disabled items are skipped.
  • DropdownMenu:Separator renders role="separator", and DropdownMenu:Group renders role="group". Label a group with aria-labelledby pointing at its DropdownMenu:Label.
  • A disabled item renders aria-disabled="true" rather than being removed, so its presence stays discoverable.
  • DropdownMenu:Shortcut shows the key combination visually. It is announced as part of the item text, so keep it short.

API Reference

<twig:DropdownMenu>

Prop Type Default
id 
string -
open 
boolean false
side 
'top'|'right'|'bottom'|'left' 'bottom'
align 
'start'|'center'|'end' 'start'
Block Description
content A DropdownMenu:Trigger and a DropdownMenu:Content.

<twig:DropdownMenu:CheckboxItem>

Prop Type Default
checked 
boolean false
disabled 
boolean false
closeOnSelect 
boolean false
Block Description
content The label text.

<twig:DropdownMenu:Content>

Block Description
content The menu items, e.g. DropdownMenu:Item, DropdownMenu:Separator.

<twig:DropdownMenu:Group>

Block Description
content One or more DropdownMenu:Item components.

<twig:DropdownMenu:Item>

Prop Type Default
as 
'button'|'a'|'div' 'button'
disabled 
boolean false
inset 
boolean false
variant 
'default'|'destructive' 'default'
closeOnSelect 
boolean true
Block Description
content The item label.

<twig:DropdownMenu:Label>

Prop Type Default
inset 
boolean false
Block Description
content The label text.

<twig:DropdownMenu:RadioGroup>

Prop Type Default
value 
string|null null
Block Description
content One or more DropdownMenu:RadioItem components.

<twig:DropdownMenu:RadioItem>

Prop Type Default
value 
string -
checked 
boolean false
disabled 
boolean false
closeOnSelect 
boolean false
Block Description
content The label text.

<twig:DropdownMenu:Shortcut>

Block Description
content The shortcut label, e.g. "⌘K".

<twig:DropdownMenu:Sub>

Prop Type Default
openDelay 
number 0
closeDelay 
number 0
Block Description
content A DropdownMenu:SubTrigger and a DropdownMenu:SubContent.

<twig:DropdownMenu:SubContent>

Block Description
content The sub-menu items, e.g. DropdownMenu:Item.

<twig:DropdownMenu:SubTrigger>

Prop Type Default
inset 
boolean false
Block Description
content The sub-menu trigger label.

<twig:DropdownMenu:Trigger>

Block Description
content The trigger label (e.g., a Button) that opens the menu on click.