Buttons
Materialize is a modern responsive CSS framework based on Material Design by Google.
Explore all ten button families from Material 3's All buttons: buttons, toggles, icon buttons, icon toggles, split buttons, standard groups, connected groups, FABs, extended FABs, and FAB menus.
Button styles are included in Materialize CSS. Import initMaterialButtons from kmaterialize for toggles, groups, and menus.
Try a button. Demo actions and selection changes appear here.
Filled
A prominent action, such as saving your work.
<!-- Use native buttons for actions; add an icon only when it helps. -->
<button type="button" class="btn btn-expressive filled" data-action="Save">Save</button>
<button type="button" class="btn btn-expressive filled" data-action="Save with icon">
<i class="material-icons" aria-hidden="true">save</i>
Save
</button>
<button
type="button"
class="btn btn-expressive filled btn-square"
data-action="Continue"
>
<i class="material-icons" aria-hidden="true">arrow_forward</i>
Continue
</button>Tonal
A supporting action with a tinted surface.
<!-- Use native buttons for actions; add an icon only when it helps. -->
<button type="button" class="btn btn-expressive tonal" data-action="Save">Save</button>
<button type="button" class="btn btn-expressive tonal" data-action="Save with icon">
<i class="material-icons" aria-hidden="true">save</i>
Save
</button>
<button
type="button"
class="btn btn-expressive tonal btn-square"
data-action="Continue"
>
<i class="material-icons" aria-hidden="true">arrow_forward</i>
Continue
</button>Outlined
An alternative action with a visible boundary.
<!-- Use native buttons for actions; add an icon only when it helps. -->
<button type="button" class="btn btn-expressive outlined" data-action="Save">
Save
</button>
<button type="button" class="btn btn-expressive outlined" data-action="Save with icon">
<i class="material-icons" aria-hidden="true">save</i>
Save
</button>
<button
type="button"
class="btn btn-expressive outlined btn-square"
data-action="Continue"
>
<i class="material-icons" aria-hidden="true">arrow_forward</i>
Continue
</button>Elevated
A raised action that separates from a patterned surface.
<!-- Use native buttons for actions; add an icon only when it helps. -->
<button type="button" class="btn btn-expressive elevated" data-action="Save">
Save
</button>
<button type="button" class="btn btn-expressive elevated" data-action="Save with icon">
<i class="material-icons" aria-hidden="true">save</i>
Save
</button>
<button
type="button"
class="btn btn-expressive elevated btn-square"
data-action="Continue"
>
<i class="material-icons" aria-hidden="true">arrow_forward</i>
Continue
</button>Text
A quiet action with no persistent container.
<!-- Use native buttons for actions; add an icon only when it helps. -->
<button type="button" class="btn btn-expressive text" data-action="Save">Save</button>
<button type="button" class="btn btn-expressive text" data-action="Save with icon">
<i class="material-icons" aria-hidden="true">save</i>
Save
</button>
<button type="button" class="btn btn-expressive text btn-square" data-action="Continue">
<i class="material-icons" aria-hidden="true">arrow_forward</i>
Continue
</button>Toggle buttons
A toggle remembers an on/off choice. aria-pressed drives the selected color and shape.
<!-- initMaterialButtons() updates aria-pressed when each button is activated. -->
<button
type="button"
class="btn btn-expressive tonal btn-toggle"
aria-pressed="false"
data-action="Favorite"
>
<i class="material-icons" aria-hidden="true">favorite_border</i>
Favorite
</button>
<button
type="button"
class="btn btn-expressive filled btn-toggle"
aria-pressed="true"
data-action="Following"
>
<i class="material-icons" aria-hidden="true">check</i>
Following
</button>
<button
type="button"
class="btn btn-expressive outlined btn-toggle"
aria-pressed="false"
data-action="Notifications"
>
<i class="material-icons" aria-hidden="true">notifications</i>
Notifications
</button>Icon buttons
Compact actions with an accessible name. Choose standard, filled, tonal, or outlined styling.
<!-- Icon-only buttons always need an accessible name. -->
<button
type="button"
class="btn btn-expressive btn-icon-standard btn-icon"
aria-label="Settings"
title="Settings"
data-action="Settings"
>
<i class="material-icons" aria-hidden="true">settings</i>
</button>
<button
type="button"
class="btn btn-expressive filled btn-icon"
aria-label="Add item"
title="Add item"
data-action="Add item"
>
<i class="material-icons" aria-hidden="true">add</i>
</button>
<button
type="button"
class="btn btn-expressive tonal btn-icon"
aria-label="Share"
title="Share"
data-action="Share"
>
<i class="material-icons" aria-hidden="true">share</i>
</button>
<button
type="button"
class="btn btn-expressive outlined btn-icon"
aria-label="Print"
title="Print"
data-action="Print"
>
<i class="material-icons" aria-hidden="true">print</i>
</button>Toggle icon buttons
Combine btn-icon and btn-toggle for selectable icon actions.
<!-- Keep the accessible name stable; aria-pressed announces the current state. -->
<button
type="button"
class="btn btn-expressive btn-icon-standard btn-icon btn-toggle"
aria-label="Favorite"
title="Favorite"
aria-pressed="false"
data-action="Favorite"
>
<i class="material-icons" aria-hidden="true">favorite</i>
</button>
<button
type="button"
class="btn btn-expressive filled btn-icon btn-toggle"
aria-label="Bookmark"
title="Bookmark"
aria-pressed="true"
data-action="Bookmark"
>
<i class="material-icons" aria-hidden="true">bookmark</i>
</button>
<button
type="button"
class="btn btn-expressive tonal btn-icon btn-toggle"
aria-label="Mute"
title="Mute"
aria-pressed="false"
data-action="Mute"
>
<i class="material-icons" aria-hidden="true">volume_off</i>
</button>
<button
type="button"
class="btn btn-expressive outlined btn-icon btn-toggle"
aria-label="Pin"
title="Pin"
aria-pressed="false"
data-action="Pin"
>
<i class="material-icons" aria-hidden="true">push_pin</i>
</button>Split buttons
The main action and its related menu are separate buttons. Open the menu with a click or arrow key; Escape closes it.
<!-- Use standard dropdown markup and a unique data-target for each menu. -->
<div class="btn-split">
<!-- Primary action. -->
<button type="button" class="btn btn-expressive filled" data-action="Send">
<i class="material-icons" aria-hidden="true">send</i>
Send
</button>
<!-- Dropdown trigger: data-target matches the menu ID. -->
<button
type="button"
class="btn btn-expressive filled btn-icon btn-menu-trigger dropdown-trigger no-autoinit"
aria-label="More send options"
aria-haspopup="menu"
aria-expanded="false"
aria-controls="send-options"
data-target="send-options"
>
<i class="material-icons" aria-hidden="true">expand_more</i>
</button>
<ul id="send-options" class="dropdown-content">
<li><a href="#!" data-action="Schedule send">Schedule send</a></li>
<li><a href="#!" data-action="Save draft">Save draft</a></li>
</ul>
</div>import { Dropdown } from 'kmaterialize';
// Initialize once. The no-autoinit class opts out of the site's general setup.
Dropdown.init(document.querySelectorAll('.btn-split .dropdown-trigger'), {
alignment: 'right',
constrainWidth: false, // Allow menu labels to determine its width.
coverTrigger: false, // Place the menu beside the trigger, without covering it.
closeOnClick: true,
});Standard button groups
Group related actions with space between them. Hovering gives an action a little more room; reduced-motion settings disable the transition.
<!-- Each button remains a separate action. Use role="group" with a descriptive label. -->
<div class="btn-group btn-group-standard" role="group" aria-label="Playback">
<button type="button" class="btn btn-expressive tonal" data-action="Previous track">
<i class="material-icons" aria-hidden="true">skip_previous</i>
Back
</button>
<button
type="button"
class="btn btn-expressive filled btn-toggle"
aria-pressed="false"
data-action="Playback paused"
>
<i class="material-icons" aria-hidden="true">pause</i>
Pause
</button>
<button type="button" class="btn btn-expressive tonal" data-action="Next track">
<i class="material-icons" aria-hidden="true">skip_next</i>
Next
</button>
</div>Connected button groups
Use a connected outline for related choices. These examples show single and multiple selection.
<!-- Single selection: choosing one option clears the others. -->
<div
class="btn-group btn-group-connected"
role="group"
aria-label="Travel mode"
data-selection="single"
>
<button
type="button"
class="btn btn-expressive tonal btn-toggle"
aria-pressed="true"
data-action="Walk"
>
<i class="material-icons" aria-hidden="true">directions_walk</i>
Walk
</button>
<button
type="button"
class="btn btn-expressive tonal btn-toggle"
aria-pressed="false"
data-action="Bike"
>
<i class="material-icons" aria-hidden="true">directions_bike</i>
Bike
</button>
<button
type="button"
class="btn btn-expressive tonal btn-toggle"
aria-pressed="false"
data-action="Drive"
>
<i class="material-icons" aria-hidden="true">directions_car</i>
Drive
</button>
</div>
<!-- Multiple selection: each setting can be toggled independently. -->
<div
class="btn-group btn-group-connected"
role="group"
aria-label="Text formatting"
data-selection="multiple"
>
<button
type="button"
class="btn btn-expressive outlined btn-icon btn-toggle"
aria-label="Bold"
aria-pressed="true"
data-action="Bold"
>
<i class="material-icons" aria-hidden="true">format_bold</i>
</button>
<button
type="button"
class="btn btn-expressive outlined btn-icon btn-toggle"
aria-label="Italic"
aria-pressed="false"
data-action="Italic"
>
<i class="material-icons" aria-hidden="true">format_italic</i>
</button>
<button
type="button"
class="btn btn-expressive outlined btn-icon btn-toggle"
aria-label="Underline"
aria-pressed="false"
data-action="Underline"
>
<i class="material-icons" aria-hidden="true">format_underlined</i>
</button>
</div>Floating action buttons (FABs)
Use a floating action button for a primary action. These examples add container colors and compact, regular, and large sizes.
<!-- The btn-fab class is an opt-in addition to the existing floating button styles. -->
<button
type="button"
class="btn btn-expressive btn-fab fab-small"
aria-label="Add note"
title="Add note"
data-action="Add note"
>
<i class="material-icons" aria-hidden="true">add</i>
</button>
<button
type="button"
class="btn btn-expressive btn-fab"
aria-label="Create note"
title="Create note"
data-action="Create note"
>
<i class="material-icons" aria-hidden="true">edit</i>
</button>
<button
type="button"
class="btn btn-expressive btn-fab fab-secondary"
aria-label="Take photo"
title="Take photo"
data-action="Take photo"
>
<i class="material-icons" aria-hidden="true">photo_camera</i>
</button>
<button
type="button"
class="btn btn-expressive btn-fab fab-large fab-tertiary"
aria-label="Create project"
title="Create project"
data-action="Create project"
>
<i class="material-icons" aria-hidden="true">add</i>
</button>For the existing fixed-position launcher, see Floating Action Button.
Extended FABs
Add a visible label when an icon alone would be unclear.
<!-- Use btn-fab-extended together with btn-fab. -->
<button
type="button"
class="btn btn-expressive btn-fab btn-fab-extended"
data-action="Compose"
>
<i class="material-icons" aria-hidden="true">edit</i>
Compose
</button>
<button
type="button"
class="btn btn-expressive btn-fab btn-fab-extended fab-secondary"
data-action="New project"
>
<i class="material-icons" aria-hidden="true">add</i>
New project
</button>FAB menus
A launcher reveals a small set of related actions. Materialize handles positioning, keyboard navigation, and closing after selection or an outside click.
<!-- Render the popup in document.body with the dropdown container option. -->
<div class="btn-fab-menu">
<!-- Menu appears above the launcher. -->
<ul id="create-options" class="dropdown-content">
<li>
<a href="#!" data-action="New document">
<i class="material-icons" aria-hidden="true">description</i>
Document
</a>
</li>
<li>
<a href="#!" data-action="New folder">
<i class="material-icons" aria-hidden="true">folder</i>
Folder
</a>
</li>
<li>
<a href="#!" data-action="Upload file">
<i class="material-icons" aria-hidden="true">upload_file</i>
Upload
</a>
</li>
</ul>
<button
type="button"
class="btn btn-expressive btn-fab btn-menu-trigger dropdown-trigger no-autoinit"
aria-label="Create"
aria-haspopup="menu"
aria-expanded="false"
aria-controls="create-options"
data-target="create-options"
>
<i class="material-icons" aria-hidden="true">add</i>
</button>
</div>import { Dropdown } from 'kmaterialize';
// Initialize once. The no-autoinit class opts out of the site's general setup.
Dropdown.init(document.querySelectorAll('.btn-fab-menu .dropdown-trigger'), {
container: document.body, // Render outside the FAB container.
alignment: 'right',
constrainWidth: false, // Allow menu labels to determine its width.
coverTrigger: false, // Place the menu beside the trigger, without covering it.
closeOnClick: true,
});Sizes, shapes, and icon widths
Use btn-xs, btn-sm, btn-md, btn-lg, or btn-xl. Rounded is the default; btn-square gives a softer rectangular shape.
<!-- Size modifiers are opt-in and do not change the legacy btn-small / btn-large helpers. -->
<button type="button" class="btn btn-expressive tonal btn-xs" data-action="XS button">
XS
</button>
<button type="button" class="btn btn-expressive tonal btn-sm" data-action="SM button">
SM
</button>
<button type="button" class="btn btn-expressive tonal btn-md" data-action="MD button">
MD
</button>
<button type="button" class="btn btn-expressive tonal btn-lg" data-action="LG button">
LG
</button>
<button type="button" class="btn btn-expressive tonal btn-xl" data-action="XL button">
XL
</button>
<button
type="button"
class="btn btn-expressive outlined btn-icon btn-md btn-icon-narrow"
aria-label="Narrow back button"
data-action="Back"
>
<i class="material-icons" aria-hidden="true">arrow_back</i>
</button>
<button
type="button"
class="btn btn-expressive filled btn-icon btn-md btn-icon-wide"
aria-label="Wide forward button"
data-action="Forward"
>
<i class="material-icons" aria-hidden="true">arrow_forward</i>
</button>Disabled buttons
Use the native disabled attribute so unavailable actions cannot receive clicks or keyboard activation.
<!-- Native disabled state applies to all variants. -->
<button
type="button"
class="btn btn-expressive filled"
disabled
data-action="Should not run"
>
Unavailable
</button>
<button
type="button"
class="btn btn-expressive tonal"
disabled
data-action="Should not run"
>
Unavailable
</button>
<button
type="button"
class="btn btn-expressive outlined"
disabled
data-action="Should not run"
>
Unavailable
</button>
<button
type="button"
class="btn btn-expressive elevated"
disabled
data-action="Should not run"
>
Unavailable
</button>
<button
type="button"
class="btn btn-expressive text"
disabled
data-action="Should not run"
>
Unavailable
</button>
<button
type="button"
class="btn btn-expressive tonal btn-icon btn-toggle"
aria-label="Favorite unavailable"
aria-pressed="true"
disabled
>
<i class="material-icons" aria-hidden="true">favorite</i>
</button>Hierarchy and placement
Give the key action more visual emphasis and keep related alternatives nearby. These arrangements mirror the combinations on the reference page.
<!-- Primary action with a quieter alternative. -->
<div class="btn-group" role="group" aria-label="Save changes">
<button type="button" class="btn btn-expressive text" data-action="Cancel">
Cancel
</button>
<button type="button" class="btn btn-expressive filled" data-action="Save">
Save
</button>
</div>
<!-- Outline and fill distinguish two important actions. -->
<div class="btn-group" role="group" aria-label="Review changes">
<button type="button" class="btn btn-expressive outlined" data-action="Review">
Review
</button>
<button type="button" class="btn btn-expressive filled" data-action="Confirm">
Confirm
</button>
</div>
<!-- Two supporting actions. -->
<div class="btn-group" role="group" aria-label="More information">
<button type="button" class="btn btn-expressive text" data-action="Learn more">
Learn more
</button>
<button type="button" class="btn btn-expressive outlined" data-action="View all">
View all
</button>
</div>
<!-- One clear primary action is enough for a simple step. -->
<button type="button" class="btn btn-expressive filled" data-action="Continue">
Continue
</button>Initialization & classes
Native buttons already support Enter and Space. Initialize once per container for toggles and menus. Split buttons and FAB menus use the standard Materialize Dropdown for positioning and keyboard navigation. The initializer below already calls Dropdown.init; the standalone snippets above are alternatives. New selection controls use aria-pressed; menu triggers use aria-expanded.
// Import once, after the site's Materialize stylesheet.
import { initMaterialButtons } from 'kmaterialize';
const root = document.querySelector<HTMLElement>('#button-examples')!;
const destroy = initMaterialButtons(root);
// Application actions stay outside the reusable UI behavior.
root.addEventListener('buttonaction', (event) => {
const { action, pressed } = (event as CustomEvent).detail;
console.log(action, pressed);
});
// When removing the view:
// destroy();| Class | Purpose |
|---|---|
btn-expressive | Rounded layout, inline icons, focus and pressed states. |
btn-xs / btn-sm / btn-md / btn-lg / btn-xl | Explicit 32 / 40 / 56 / 96 / 136px heights. |
btn-square | Soft rectangular shape. |
btn-toggle | Selection driven by aria-pressed. |
btn-icon / btn-icon-standard | Icon-only geometry and unfilled styling. |
btn-icon-narrow / btn-icon-wide | Narrow and wide icon containers. |
btn-group / btn-group-standard | Spaced related actions. |
btn-group-connected | Connected single- or multi-select choices. |
btn-split / btn-menu-trigger | Primary action paired with a standard dropdown. |
btn-fab / btn-fab-extended | Container-color FABs, with or without labels. |
fab-small / fab-large / fab-secondary / fab-tertiary | FAB sizes and color alternatives. |
btn-fab-menu | FAB launcher for a standard dropdown. |
SCSS — reusable button classes
// Add after Materialize. Opt-in classes preserve existing .btn styling elsewhere.
.btn.btn-expressive {
--button-height: 40px;
--button-padding: 24px;
--button-icon: 20px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
height: var(--button-height);
min-height: var(--button-height);
padding: 0 var(--button-padding);
border-radius: 999px;
font-size: 14px;
line-height: 1.2;
text-transform: none;
white-space: nowrap;
transition:
border-radius 180ms ease,
background-color 180ms ease,
flex-grow 180ms ease;
> .material-icons {
position: static;
transform: none;
font-size: var(--button-icon);
line-height: 1;
flex-shrink: 0;
}
&:focus-visible {
outline: 3px solid var(--md-sys-color-on-surface);
outline-offset: 3px;
}
&:active:not(:disabled) {
border-radius: 12px;
}
&.btn-xs {
--button-height: 32px;
--button-padding: 12px;
}
&.btn-sm {
--button-height: 40px;
}
&.btn-md {
--button-height: 56px;
--button-icon: 24px;
font-size: 16px;
}
&.btn-lg {
--button-height: 96px;
--button-padding: 40px;
--button-icon: 32px;
font-size: 24px;
}
&.btn-xl {
--button-height: 136px;
--button-padding: 48px;
--button-icon: 40px;
font-size: 32px;
}
&.btn-square {
border-radius: calc(var(--button-height) * 0.28);
}
}
// Toggle state belongs in aria-pressed, so visual and accessible states agree.
.btn.btn-toggle[aria-pressed='true'] {
background: var(--md-sys-color-primary);
color: var(--md-sys-color-on-primary);
border-color: transparent;
border-radius: 16px;
}
.btn.btn-icon {
width: var(--button-height);
min-width: var(--button-height);
padding: 0;
}
.btn.btn-icon-standard {
background: transparent;
color: var(--md-sys-color-on-surface-variant);
box-shadow: none;
}
.btn.btn-icon-standard:hover:not(:disabled) {
background: var(--md-sys-color-surface-variant);
}
.btn.btn-icon.btn-toggle[aria-pressed='true'] {
background: var(--md-sys-color-primary);
color: var(--md-sys-color-on-primary);
}
.btn.btn-icon.btn-icon-narrow {
width: calc(var(--button-height) * 0.8);
min-width: calc(var(--button-height) * 0.8);
}
.btn.btn-icon.btn-icon-wide {
width: calc(var(--button-height) * 1.5);
}
// Standard groups keep gaps; connected groups share the outer silhouette.
.btn-group {
display: inline-flex;
align-items: center;
gap: 8px;
max-width: 100%;
}
.btn-group > .btn {
flex: 1 1 0;
min-width: 0;
}
.btn-group-standard {
width: 100%;
max-width: 480px;
}
.btn-group-standard > .btn:hover:not(:disabled) {
flex-grow: 1.2;
}
.btn-group-connected {
gap: 2px;
}
.btn-group-connected > .btn.btn-expressive {
border-radius: 4px;
}
.btn-group-connected > .btn.btn-expressive:first-child {
border-start-start-radius: 999px;
border-end-start-radius: 999px;
}
.btn-group-connected > .btn.btn-expressive:last-child {
border-start-end-radius: 999px;
border-end-end-radius: 999px;
}
// Keep the connected silhouette unchanged during selection and pointer presses.
.btn-group-connected > .btn.btn-expressive:active:not(:disabled) {
border-radius: 4px;
&:first-child {
border-start-start-radius: 999px;
border-end-start-radius: 999px;
}
&:last-child {
border-start-end-radius: 999px;
border-end-end-radius: 999px;
}
}
// Split controls expose a separate primary action and menu action.
.btn-split {
display: inline-flex;
gap: 2px;
position: relative;
vertical-align: middle;
}
.btn-split > .btn:first-child {
border-start-end-radius: 4px;
border-end-end-radius: 4px;
}
.btn-split > .btn-menu-trigger {
border-start-start-radius: 4px;
border-end-start-radius: 4px;
}
.btn-menu-trigger > .material-icons {
transition: transform 180ms ease;
}
.btn-menu-trigger[aria-expanded='true'] > .material-icons {
transform: rotate(180deg);
}
// FABs use container colors and elevation; extended FABs also carry a label.
.btn.btn-fab {
--button-height: 56px;
--button-icon: 24px;
width: var(--button-height);
padding: 0;
border-radius: 16px;
background: var(--md-sys-color-primary-container);
color: var(--md-sys-color-on-primary-container);
box-shadow: 0 3px 8px color-mix(in srgb, var(--md-sys-color-shadow) 20%, transparent);
&.fab-secondary {
background: var(--md-sys-color-secondary-container);
color: var(--md-sys-color-on-secondary-container);
}
&.fab-tertiary {
background: var(--md-sys-color-tertiary-container);
color: var(--md-sys-color-on-tertiary-container);
}
&.fab-small {
--button-height: 40px;
border-radius: 12px;
}
&.fab-large {
--button-height: 96px;
--button-icon: 36px;
border-radius: 28px;
}
&.btn-fab-extended {
width: auto;
padding: 0 20px;
}
}
.btn-fab-menu {
position: relative;
display: inline-flex;
}
.btn-fab-menu .btn-menu-trigger[aria-expanded='true'] > .material-icons {
transform: rotate(45deg);
}
// Explicitly retain native disabled behavior across all custom variants.
.btn.btn-expressive:disabled,
.btn.btn-expressive.btn-toggle:disabled {
background: color-mix(in srgb, var(--md-sys-color-on-surface) 12%, transparent);
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
box-shadow: none;
cursor: default;
}
@media (pointer: coarse) {
.btn.btn-expressive::after {
content: '';
position: absolute;
inset: -4px 0;
min-height: 48px;
}
.btn-group {
gap: 8px;
}
}
@media (prefers-reduced-motion: reduce) {
.btn.btn-expressive,
.btn-menu-trigger > .material-icons {
transition: none;
}
}TypeScript — toggles, groups, and menus
import { Dropdown } from 'kmaterialize';
/** Initialize Materialize dropdowns and the additional button selection states. */
export function initMaterialButtons(root: HTMLElement): () => void {
const events = new AbortController();
const triggers = Array.from(root.querySelectorAll<HTMLElement>('.dropdown-trigger'));
// Remember ownership before Dropdown moves a FAB menu into document.body.
const menus = new Set(
triggers.map((trigger) => document.getElementById(trigger.dataset.target!)!),
);
const owns = (element: Element) =>
root.contains(element) || Array.from(menus).some((menu) => menu.contains(element));
const dropdowns = triggers.map((trigger) =>
Dropdown.init(trigger, {
alignment: 'right',
constrainWidth: false,
coverTrigger: false,
closeOnClick: true,
// FAB popups escape their demo container; split menus keep their default parent.
...(trigger.closest('.btn-fab-menu') ? { container: document.body } : {}),
}),
);
// Dropdown activates its focused item on Enter. Suppress the native follow-up
// click when focus returns to a button trigger, which would reopen the menu.
document.addEventListener(
'keydown',
(event) => {
if (
event.key === 'Enter' &&
owns(event.target as Element) &&
(event.target as Element).closest('.dropdown-content')
) {
event.preventDefault();
}
},
{ capture: true, signal: events.signal },
);
document.addEventListener(
'click',
(event) => {
const action = (event.target as Element).closest<HTMLElement>(
'[data-action], .btn-toggle',
);
if (!action || !owns(action) || action.matches(':disabled')) return;
// Demo menu actions are links, following the standard dropdown markup.
if (action.matches('a[data-action]')) event.preventDefault();
if (action.classList.contains('btn-toggle')) {
const group = action.closest<HTMLElement>('[data-selection]');
const selected = action.getAttribute('aria-pressed') === 'true';
if (group?.dataset.selection === 'single') {
group.querySelectorAll('.btn-toggle').forEach((item) => {
item.setAttribute('aria-pressed', String(item === action));
});
} else {
action.setAttribute('aria-pressed', String(!selected));
}
}
// Keep application behavior separate from selection and dropdown behavior.
if (action.dataset.action) {
root.dispatchEvent(
new CustomEvent('buttonaction', {
bubbles: true,
detail: {
action: action.dataset.action,
pressed: action.getAttribute('aria-pressed'),
},
}),
);
}
},
{ signal: events.signal },
);
return () => {
dropdowns.forEach((dropdown) => dropdown.destroy());
events.abort();
};
}