Skip to content

Dropdown Menu

Dropdown menuLabels, groups, separators and shortcuts; links and buttons; a disabled item.
Thymeleaf source
<sl:dropdown-menu id="account-menu">
<sl:dropdown-menu-trigger variant="outline">Open</sl:dropdown-menu-trigger>
<sl:dropdown-menu-content>
<sl:dropdown-menu-label>My Account</sl:dropdown-menu-label>
<sl:dropdown-menu-group>
<sl:dropdown-menu-item as="a" href="/profile">
Profile <sl:dropdown-menu-shortcut>⇧⌘P</sl:dropdown-menu-shortcut>
</sl:dropdown-menu-item>
<sl:dropdown-menu-item as="a" href="/billing">
Billing <sl:dropdown-menu-shortcut>⌘B</sl:dropdown-menu-shortcut>
</sl:dropdown-menu-item>
<sl:dropdown-menu-item as="a" href="/settings">
Settings <sl:dropdown-menu-shortcut>⌘S</sl:dropdown-menu-shortcut>
</sl:dropdown-menu-item>
</sl:dropdown-menu-group>
<sl:dropdown-menu-separator/>
<sl:dropdown-menu-group>
<sl:dropdown-menu-item as="a" href="/team">Team</sl:dropdown-menu-item>
<sl:dropdown-menu-item>
New Team <sl:dropdown-menu-shortcut>⌘T</sl:dropdown-menu-shortcut>
</sl:dropdown-menu-item>
</sl:dropdown-menu-group>
<sl:dropdown-menu-separator/>
<sl:dropdown-menu-item as="a" href="https://github.com/wimdeblauwe/shadleaf">GitHub</sl:dropdown-menu-item>
<sl:dropdown-menu-item as="a" href="/support">Support</sl:dropdown-menu-item>
<sl:dropdown-menu-item disabled>API</sl:dropdown-menu-item>
<sl:dropdown-menu-separator/>
<sl:dropdown-menu-item type="submit" form="logout-form">
Log out <sl:dropdown-menu-shortcut>⇧⌘Q</sl:dropdown-menu-shortcut>
</sl:dropdown-menu-item>
</sl:dropdown-menu-content>
</sl:dropdown-menu>
<form id="logout-form" method="post" action="/logout"></form>
Rendered HTML
<button class="button dropdown-menu-trigger" type="button" data-variant="outline" id="account-menu-trigger" popovertarget="account-menu" aria-haspopup="menu">Open</button>
<div class="dropdown-menu-content" id="account-menu" role="menu" popover tabindex="-1" x-data="slDropdownMenu" aria-labelledby="account-menu-trigger">
<div class="dropdown-menu-label">My Account</div>
<div class="dropdown-menu-group" role="group">
<a class="dropdown-menu-item" role="menuitem" href="/profile">
Profile
<span class="dropdown-menu-shortcut">⇧⌘P</span>
</a>
<a class="dropdown-menu-item" role="menuitem" href="/billing">
Billing
<span class="dropdown-menu-shortcut">⌘B</span>
</a>
<a class="dropdown-menu-item" role="menuitem" href="/settings">
Settings
<span class="dropdown-menu-shortcut">⌘S</span>
</a>
</div>
<div class="dropdown-menu-separator" role="separator"></div>
<div class="dropdown-menu-group" role="group">
<a class="dropdown-menu-item" role="menuitem" href="/team">Team</a>
<button type="button" class="dropdown-menu-item" role="menuitem">
New Team
<span class="dropdown-menu-shortcut">⌘T</span>
</button>
</div>
<div class="dropdown-menu-separator" role="separator"></div>
<a class="dropdown-menu-item" role="menuitem" href="https://github.com/wimdeblauwe/shadleaf">GitHub</a>
<a class="dropdown-menu-item" role="menuitem" href="/support">Support</a>
<button type="button" class="dropdown-menu-item" role="menuitem" disabled>API</button>
<div class="dropdown-menu-separator" role="separator"></div>
<button type="submit" class="dropdown-menu-item" role="menuitem" form="logout-form">
Log out
<span class="dropdown-menu-shortcut">⇧⌘Q</span>
</button>
</div>
<form id="logout-form" method="post" action="/logout"></form>

A dropdown menu is <sl:dropdown-menu> with a <sl:dropdown-menu-trigger> and a <sl:dropdown-menu-content> holding the items, as in shadcn/ui. <sl:dropdown-menu> renders no element of its own: it gives the parts their ids.

<sl:dropdown-menu id="account-menu">
<sl:dropdown-menu-trigger variant="outline">Open</sl:dropdown-menu-trigger>
<sl:dropdown-menu-content>
<sl:dropdown-menu-label>My Account</sl:dropdown-menu-label>
<sl:dropdown-menu-group>
<sl:dropdown-menu-item as="a" th:href="@{/profile}">
Profile <sl:dropdown-menu-shortcut>⇧⌘P</sl:dropdown-menu-shortcut>
</sl:dropdown-menu-item>
<sl:dropdown-menu-item as="a" th:href="@{/settings}">Settings</sl:dropdown-menu-item>
</sl:dropdown-menu-group>
<sl:dropdown-menu-separator/>
<sl:dropdown-menu-item type="submit" form="logout-form">Log out</sl:dropdown-menu-item>
</sl:dropdown-menu-content>
</sl:dropdown-menu>

The menu is a popover, so the browser does the hard parts: the menu is drawn above everything else (no z-index to manage, no portal), Escape or a click outside closes it, and closing returns focus to the trigger. The trigger is a <sl:button> with popovertarget set to the menu’s id; it takes the button’s variant and size, its icon-start and icon-end slots, and any other attribute passes through. Any other button can open the menu the same way: <sl:button popovertarget="account-menu">.

The menu is labelled by its trigger. A menu opened by a button of your own needs an aria-label on <sl:dropdown-menu-content>.

as makes the trigger a sidebar part instead of a button: shadcn’s <DropdownMenuTrigger asChild> around a SidebarMenuButton or a SidebarMenuAction.

as Renders For
button (default) <sl:button> Any menu
sidebar-menu-button <sl:sidebar-menu-button as="button">, with its variant, size, tooltip and icon-start/icon-end slots A team or version switcher in the sidebar header, a user menu in its footer
sidebar-menu-action <sl:sidebar-menu-action>, with show-on-hover “More” beside a project’s link
<sl:sidebar-menu-item>
<sl:dropdown-menu id="team-switcher">
<sl:dropdown-menu-trigger as="sidebar-menu-button" size="lg" tooltip="Acme Inc.">
<sl:slot name="icon-start"><sl:icon name="gallery-vertical-end"/></sl:slot>
<strong>Acme Inc.</strong>
<small>Enterprise</small>
<sl:slot name="icon-end"><sl:icon name="chevrons-up-down"/></sl:slot>
</sl:dropdown-menu-trigger>
<sl:dropdown-menu-content side="right">...</sl:dropdown-menu-content>
</sl:dropdown-menu>
</sl:sidebar-menu-item>

The trigger is the same element the part renders on its own, with the menu’s popovertarget, aria-haspopup and id, so it looks and lines up like the entries around it. The Sidebar page shows the team switcher, the version switcher and the “More” menu, and where each opens: beside the sidebar on a desktop (side="right"), below its trigger in the phone panel and in the header layout’s row.

An item is a button, or with as="a" a link. Every attribute that is not a prop passes through, so an item does anything a button or link does, and the menu closes once it is chosen, with focus back on the trigger:

Item What it does
<sl:dropdown-menu-item as="a" th:href="@{/orders/{id}(id=${order.id})}"> Navigates.
<sl:dropdown-menu-item hx:post="@{/orders/{id}/duplicate(id=${order.id})}"> Sends an htmx request.
<sl:dropdown-menu-item type="submit" form="logout-form"> Submits a form (a POST logout).
<sl:dropdown-menu-item commandfor="rename-dialog" command="show-modal"> Opens a dialog; closing it returns focus to the menu’s trigger.

variant="destructive" is for an item that deletes something. disabled disables an item: the arrow keys skip it, a disabled button cannot be clicked (htmx included), and a disabled link loses its href. inset indents an item (or a label) to line up with items that start with an icon.

Icons and a destructive itemAn icon at the start of an item; variant="destructive" for one that deletes.
Thymeleaf source
<sl:dropdown-menu id="file-menu">
<sl:dropdown-menu-trigger variant="outline">File</sl:dropdown-menu-trigger>
<sl:dropdown-menu-content>
<sl:dropdown-menu-item><sl:icon name="file"/>New file</sl:dropdown-menu-item>
<sl:dropdown-menu-item><sl:icon name="folder"/>New folder</sl:dropdown-menu-item>
<sl:dropdown-menu-item><sl:icon name="share"/>Share</sl:dropdown-menu-item>
<sl:dropdown-menu-separator/>
<sl:dropdown-menu-item variant="destructive"><sl:icon name="trash-2"/>Delete</sl:dropdown-menu-item>
</sl:dropdown-menu-content>
</sl:dropdown-menu>
Rendered HTML
<button class="button dropdown-menu-trigger" type="button" data-variant="outline" id="file-menu-trigger" popovertarget="file-menu" aria-haspopup="menu">File</button>
<div class="dropdown-menu-content" id="file-menu" role="menu" popover tabindex="-1" x-data="slDropdownMenu" aria-labelledby="file-menu-trigger">
<button type="button" class="dropdown-menu-item" role="menuitem">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"></path>
<path d="M14 2v5a1 1 0 0 0 1 1h5"></path>
</svg>
New file
</button>
<button type="button" class="dropdown-menu-item" role="menuitem">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"></path>
</svg>
New folder
</button>
<button type="button" class="dropdown-menu-item" role="menuitem">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M12 2v13"></path>
<path d="m16 6-4-4-4 4"></path>
<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"></path>
</svg>
Share
</button>
<div class="dropdown-menu-separator" role="separator"></div>
<button type="button" class="dropdown-menu-item" role="menuitem" data-variant="destructive">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M10 11v6"></path>
<path d="M14 11v6"></path>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"></path>
<path d="M3 6h18"></path>
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
Delete
</button>
</div>

<sl:dropdown-menu-shortcut> shows a keyboard shortcut at the end of an item. It only shows it: your application handles the keys.

A checkbox item shows an on/off state with a check mark (role="menuitemcheckbox"), a radio item one choice out of a few (role="menuitemradio", inside <sl:dropdown-menu-radio-group>). The state comes from the server: checked marks it, and choosing an item does what its attributes say, usually a link to the same page with another parameter. The page that comes back shows the new state.

<sl:dropdown-menu-radio-group aria-label="Sort by">
<sl:dropdown-menu-radio-item as="a" th:href="@{/orders(sort='name')}" th:checked="${sort == 'name'}">
Name
</sl:dropdown-menu-radio-item>
<sl:dropdown-menu-radio-item as="a" th:href="@{/orders(sort='date')}" th:checked="${sort == 'date'}">
Date
</sl:dropdown-menu-radio-item>
</sl:dropdown-menu-radio-group>

Give such an item its text as content ([[${option.label}]]), not with th:text: that attribute passes through to the item and would replace its check mark too.

With an hx-post instead of a link, return the menu again (or the part of the page that holds it), so the check mark follows the server. Nothing changes the check mark in the browser: it always shows what the server knows.

Checkbox itemsSettings that are on or off. The server renders the state; choosing an item sends a request (here a link) and the page that comes back shows the new state.
Thymeleaf source
<sl:dropdown-menu id="view-menu">
<sl:dropdown-menu-trigger variant="outline">View</sl:dropdown-menu-trigger>
<sl:dropdown-menu-content>
<sl:dropdown-menu-label>Appearance</sl:dropdown-menu-label>
<sl:dropdown-menu-checkbox-item as="a" href="?status-bar=false" checked>Status Bar</sl:dropdown-menu-checkbox-item>
<sl:dropdown-menu-checkbox-item as="a" href="?activity-bar=true" disabled>Activity Bar</sl:dropdown-menu-checkbox-item>
<sl:dropdown-menu-checkbox-item as="a" href="?panel=true">Panel</sl:dropdown-menu-checkbox-item>
</sl:dropdown-menu-content>
</sl:dropdown-menu>
Rendered HTML
<button class="button dropdown-menu-trigger" type="button" data-variant="outline" id="view-menu-trigger" popovertarget="view-menu" aria-haspopup="menu">View</button>
<div class="dropdown-menu-content" id="view-menu" role="menu" popover tabindex="-1" x-data="slDropdownMenu" aria-labelledby="view-menu-trigger">
<div class="dropdown-menu-label">Appearance</div>
<a class="dropdown-menu-checkbox-item" role="menuitemcheckbox" aria-checked="true" href="?status-bar=false">
Status Bar
<span class="dropdown-menu-item-indicator">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</span>
</a>
<a class="dropdown-menu-checkbox-item" role="menuitemcheckbox" aria-checked="false" aria-disabled="true">
Activity Bar
<span class="dropdown-menu-item-indicator">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</span>
</a>
<a class="dropdown-menu-checkbox-item" role="menuitemcheckbox" aria-checked="false" href="?panel=true">
Panel
<span class="dropdown-menu-item-indicator">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</span>
</a>
</div>
Radio itemsOne choice out of a few, such as where a panel goes or how a list is sorted.
Thymeleaf source
<sl:dropdown-menu id="position-menu">
<sl:dropdown-menu-trigger variant="outline">Panel position</sl:dropdown-menu-trigger>
<sl:dropdown-menu-content>
<sl:dropdown-menu-label>Panel Position</sl:dropdown-menu-label>
<sl:dropdown-menu-separator/>
<sl:dropdown-menu-radio-group aria-label="Panel position">
<sl:dropdown-menu-radio-item as="a" href="?position=top">Top</sl:dropdown-menu-radio-item>
<sl:dropdown-menu-radio-item as="a" href="?position=bottom" checked>Bottom</sl:dropdown-menu-radio-item>
<sl:dropdown-menu-radio-item as="a" href="?position=right">Right</sl:dropdown-menu-radio-item>
</sl:dropdown-menu-radio-group>
</sl:dropdown-menu-content>
</sl:dropdown-menu>
Rendered HTML
<button class="button dropdown-menu-trigger" type="button" data-variant="outline" id="position-menu-trigger" popovertarget="position-menu" aria-haspopup="menu">Panel position</button>
<div class="dropdown-menu-content" id="position-menu" role="menu" popover tabindex="-1" x-data="slDropdownMenu" aria-labelledby="position-menu-trigger">
<div class="dropdown-menu-label">Panel Position</div>
<div class="dropdown-menu-separator" role="separator"></div>
<div class="dropdown-menu-radio-group" role="group" aria-label="Panel position">
<a class="dropdown-menu-radio-item" role="menuitemradio" aria-checked="false" href="?position=top">
Top
<span class="dropdown-menu-item-indicator">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</span>
</a>
<a class="dropdown-menu-radio-item" role="menuitemradio" aria-checked="true" href="?position=bottom">
Bottom
<span class="dropdown-menu-item-indicator">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</span>
</a>
<a class="dropdown-menu-radio-item" role="menuitemradio" aria-checked="false" href="?position=right">
Right
<span class="dropdown-menu-item-indicator">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</span>
</a>
</div>
</div>
Key On the trigger In the menu
Enter, Space Opens the menu on its first item Chooses the item (Space works on links too)
Arrow down Opens the menu on its first item Next item
Arrow up Opens the menu on its last item Previous item
Home, End First, last item
A letter The next item whose text starts with it; typing quickly matches a prefix
Escape Closes the menu; focus returns to the trigger (also inside another popover, such as the sidebar’s phone panel, where the browser itself does not return it)
Tab Closes the menu and moves on

Disabled items, labels and separators are skipped. The item under the pointer takes the focus, so pointer and keyboard highlight items the same way; only keyboard focus draws the focus ring.

The menu opens below its trigger, lined up with its start. side="top|right|bottom|left" and align="start|center|end" on <sl:dropdown-menu-content> change that, and the menu flips to the other side when there is no room. right and left are the end and the start of a line, so they swap on a right-to-left page. Inside the sidebar’s phone panel and its header row a right or left menu opens below its trigger instead (see Sidebar).

It is placed with CSS anchor positioning, which every current browser supports (Chrome and Edge 133, Safari 26, Firefox 147). In older ones the menu’s small Alpine component places it from the trigger’s position instead.

A menu per row of a table takes its id from an expression; the parts derive theirs from it.

Row actionsAn icon button opens the actions of a table row; align="end" lines the menu up with its end.
Thymeleaf source
<sl:dropdown-menu id="order-1042-actions">
<sl:dropdown-menu-trigger variant="ghost" size="icon-sm" aria-label="Actions for order 1042">
<sl:icon name="ellipsis"/>
</sl:dropdown-menu-trigger>
<sl:dropdown-menu-content align="end">
<sl:dropdown-menu-item as="a" href="/orders/1042">View order</sl:dropdown-menu-item>
<sl:dropdown-menu-item hx-post="/orders/1042/duplicate">Duplicate</sl:dropdown-menu-item>
<sl:dropdown-menu-separator/>
<sl:dropdown-menu-item variant="destructive" hx-delete="/orders/1042">Delete</sl:dropdown-menu-item>
</sl:dropdown-menu-content>
</sl:dropdown-menu>
Rendered HTML
<button class="button dropdown-menu-trigger" type="button" data-variant="ghost" data-size="icon-sm" id="order-1042-actions-trigger" popovertarget="order-1042-actions" aria-haspopup="menu" aria-label="Actions for order 1042">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<circle cx="12" cy="12" r="1"></circle>
<circle cx="19" cy="12" r="1"></circle>
<circle cx="5" cy="12" r="1"></circle>
</svg>
</button>
<div class="dropdown-menu-content" id="order-1042-actions" role="menu" popover tabindex="-1" x-data="slDropdownMenu" aria-labelledby="order-1042-actions-trigger" data-align="end">
<a class="dropdown-menu-item" role="menuitem" href="/orders/1042">View order</a>
<button type="button" class="dropdown-menu-item" role="menuitem" hx-post="/orders/1042/duplicate">Duplicate</button>
<div class="dropdown-menu-separator" role="separator"></div>
<button type="button" class="dropdown-menu-item" role="menuitem" data-variant="destructive" hx-delete="/orders/1042">Delete</button>
</div>
<tr th:each="order : ${orders}" th:id="|order-${order.id}|">
<td th:text="${order.number}">1042</td>
<td>
<sl:dropdown-menu th:id="|order-${order.id}-actions|">
<sl:dropdown-menu-trigger variant="ghost" size="icon-sm" th:aria-label="|Actions for order ${order.number}|">
<sl:icon name="ellipsis"/>
</sl:dropdown-menu-trigger>
<sl:dropdown-menu-content align="end">
<sl:dropdown-menu-item hx:get="@{/orders/{id}/edit(id=${order.id})}" hx-target="#modal-root">
Edit
</sl:dropdown-menu-item>
<sl:dropdown-menu-separator/>
<sl:dropdown-menu-item variant="destructive" hx:get="@{/orders/{id}/delete(id=${order.id})}"
hx-target="#modal-root">Delete</sl:dropdown-menu-item>
</sl:dropdown-menu-content>
</sl:dropdown-menu>
</td>
</tr>

The trigger’s id is the menu’s id plus -trigger (order-1042-actions-trigger). When a response replaces the row, htmx puts the focus back on the element with the same id in the new row, so the user does not lose their place. An item that fetches a dialog closes the menu first, so the dialog returns the focus to the trigger when it closes.

  • A swap that replaces an open menu leaves nothing behind: the new menu starts closed and works like the old one.
  • Back never brings a menu back open: whether a popover is open is not part of the markup htmx saves in its history cache, and the restored menu starts again.
  • An hx-* item sends its request and the menu closes; an hx-target can be anything, the menu’s own row included.

These shadcn/Radix parts and props have no counterpart, on purpose:

  • Sub-menus (DropdownMenuSub, -SubTrigger, -SubContent). They need a lot more JavaScript (hover intent, a pointer grace area towards the sub-menu, arrow right and left) and are hard to use on a touch screen and with a screen reader. A server-rendered menu is short: flatten it with labels and groups, or open a dialog.
  • DropdownMenuPortal: the top layer does what the portal is for.
  • State in the browser (open/onOpenChange, checked/onCheckedChange, value/onValueChange, onSelect): the menu is opened by its trigger, an item is chosen by what its attributes say, and checked states come from the server.
  • modal: the menu never makes the rest of the page inert; a click outside closes it.
  • loop: arrow keys stop at the first and last item, Radix’s default.
AttributeValuesDefaultDescription
idstringrequiredThe menu's id, which the trigger points at with popovertarget. Any other button can open it that way too.

Every prop can also be set from an expression: th:id="${...}". Any other attribute passes through to the rendered element, and class is appended.

AttributeValuesDefaultDescription
asbutton sidebar-menu-button sidebar-menu-actionbuttonThe part to render: a sl:button, a sl:sidebar-menu-button (a team switcher or user menu in a sl:sidebar-menu-item), or a sl:sidebar-menu-action (a "More" beside a menu button, which needs an aria-label).
variantdefault outline ghost destructive secondary linkdefaultVisual style, as on sl:button, or default and outline as on sl:sidebar-menu-button. Not for a sidebar menu action.
sizexs sm default lg icon icon-xs icon-sm icon-lgdefaultSize, as on sl:button (the icon sizes need an aria-label), or sm, default and lg as on sl:sidebar-menu-button. Not for a sidebar menu action.
tooltipstring—With as="sidebar-menu-button": the label shown beside it while the sidebar is collapsed to icons, as on sl:sidebar-menu-button. Never shown while the menu is open.
show-on-hoverboolean—With as="sidebar-menu-action": from 768&nbsp;px, show it only while the pointer is over its item, the focus is in it, or the menu is open.

Every prop can also be set from an expression: th:as="${...}". Any other attribute passes through to the rendered element, and class is appended.

AttributeValuesDefaultDescription
sidetop right bottom leftbottomThe side of the trigger it opens on. right and left are the end and the start of a line: on a right-to-left page a right menu opens on the left.
alignstart center endstartHow it lines up with the trigger along that side: start lines up the start edges.

Every prop can also be set from an expression: th:side="${...}". Any other attribute passes through to the rendered element, and class is appended.

AttributeValuesDefaultDescription
asbutton abuttonElement to render: a button, or an anchor (a) for navigation.
variantdefault destructivedefaultdestructive for an item that deletes something.
typebutton submit resetbuttonSubmit behaviour of a button item. Defaults to button; submit submits the form it is in (or names with form).
insetboolean—Indent the item to line up with items that start with an icon.
disabledboolean—Disable the item. The arrow keys skip it; a disabled link loses its href.

Every prop can also be set from an expression: th:as="${...}". Any other attribute passes through to the rendered element, and class is appended.

AttributeValuesDefaultDescription
checkedboolean—Whether the setting is on (aria-checked).
asbutton abuttonElement to render: a button, or an anchor (a).
typebutton submit resetbuttonSubmit behaviour of a button item.
insetboolean—Indent the item to line up with items that start with an icon.
disabledboolean—Disable the item. The arrow keys skip it.

Every prop can also be set from an expression: th:checked="${...}". Any other attribute passes through to the rendered element, and class is appended.

sl:dropdown-menu-radio-group declares no props; every attribute passes through.

AttributeValuesDefaultDescription
checkedboolean—Whether this is the chosen item (aria-checked).
asbutton abuttonElement to render: a button, or an anchor (a).
typebutton submit resetbuttonSubmit behaviour of a button item.
insetboolean—Indent the item to line up with items that start with an icon.
disabledboolean—Disable the item. The arrow keys skip it.

Every prop can also be set from an expression: th:checked="${...}". Any other attribute passes through to the rendered element, and class is appended.

AttributeValuesDefaultDescription
insetboolean—Indent the label to line up with items that start with an icon.

Every prop can also be set from an expression: th:inset="${...}". Any other attribute passes through to the rendered element, and class is appended.

sl:dropdown-menu-group declares no props; every attribute passes through.

sl:dropdown-menu-separator declares no props; every attribute passes through.

sl:dropdown-menu-shortcut declares no props; every attribute passes through.

A small Alpine component, slDropdownMenu, adds the menu’s keyboard and behaviour: <sl:dropdown-menu-content> renders as <div class="dropdown-menu-content" role="menu" popover x-data="slDropdownMenu">.

  • The basics work without Alpine. The trigger opens and closes the menu, Escape and a click outside close it, and items work with the mouse and with Tab. Alpine is needed for the arrow keys and typeahead, for closing the menu after an item is chosen, for aria-expanded on the trigger, and for placing the menu in browsers without CSS anchor positioning. Alpine is included by default; see Alpine.
  • It needs no Alpine plugin. If you load Alpine yourself (shadleaf.assets.alpine=external), plain Alpine is enough.
  • Keep your own x-data off <sl:dropdown-menu-content>. An element has one x-data, and attributes on it end up on the menu, so yours would replace slDropdownMenu. Put it on an element inside the menu.