Flyout
Positioned surface attached to a trigger, commonly used for action lists and small panels.
Flyout and context menu share one action-surface runtime:
- Flyout = a visible trigger opens a surface.
- Context menu = right click or a keyboard context action opens that same surface.
- Sheet = mobile presentation mode of that same surface.
Use data-flyout-mobile on the flyout to control mobile behavior:
autois the default. It keeps the surface anchored on desktop and switches to a bottom sheet on coarse pointers below the breakpoint.sheetalways uses the sheet presentation.anchoredalways uses floating positioning.nonedisables the mobile transformation.
data-flyout-breakpoint only matters for auto. Prefer the built-in tokens (sm, md, lg) and use raw pixel values only as an escape hatch, for example data-flyout-breakpoint="640". Use data-flyout-placement for the preferred anchored placement. It accepts the placement strings supported by the floating runtime, such as bottom-start, bottom-end, top-start, right, or left. Use data-flyout-distance for the trigger gap in pixels. The default is 4. Use data-flyout-auto-close when the default click dismissal behavior is not right. Escape still follows the shared surface lifecycle.
trueis the default. Action items close on activation and outside clicks close the flyout.insidecloses on action item activation only.outsidecloses on outside click only.falsedisables automatic click closing.
Flyout covers two distinct patterns:
Action list
- A list of actions the user can take: sign out, copy, delete.
- Wrap the trigger and flyout in
.flyout-triggerwhen the flyout should have a local absolute-position fallback before JavaScript positions it. Add.stretchwhen the trigger must span its container, such as the last row of a full-width sidebar nav list. - Use
<menu class="flyout">with<li>items. - Items are regular
<button>or<a>elements. - Use
.smor.lgfor density changes. - Direct action items get roving arrow-key focus.
- Add
role="menu"/role="menuitem"only when you intentionally need the ARIA menu pattern.
Nav panel
- A panel of links to other pages: product categories, docs sections.
- Items are regular
<a href>links, notrole="menuitem". - No arrow-key navigation, no
role="menu". - Just a toggle with outside-click and Escape dismissal.
- Use grid utilities, such as
.grid-3, for wider multi-column flyouts.
<div class="flyout-trigger">
<button class="btn outline"
type="button"
aria-expanded="false"
aria-controls="account-actions"
id="account-flyout-trigger">
Account
<i class="ti ti-chevron-down" aria-hidden="true"></i>
</button>
<menu class="flyout sm"
id="account-actions"
aria-labelledby="account-flyout-trigger"
hidden>
<li><button type="button">Profile</button></li>
<li><button type="button">Settings</button></li>
<li><hr /></li>
<li><button class="danger" type="button">Sign out</button></li>
</menu>
</div>
<nav aria-label="Main navigation">
<ul class="list-reset cluster">
<li class="flyout-trigger">
<button class="btn ghost"
type="button"
aria-expanded="false"
aria-controls="products-panel">
Products
<i class="ti ti-chevron-down" aria-hidden="true"></i>
</button>
<div class="flyout"
id="products-panel"
aria-label="Products"
data-flyout-mobile="auto"
hidden>
<section aria-labelledby="products-design">
<h3 id="products-design">Design</h3>
<ul>
<li><a href="/figma">Figma integration</a></li>
<li><a href="/tokens">Design tokens</a></li>
</ul>
</section>
<section aria-labelledby="products-dev">
<h3 id="products-dev">Development</h3>
<ul>
<li><a href="/components">Components</a></li>
<li><a href="/api">API</a></li>
</ul>
</section>
<footer>
<a href="/pricing" class="btn primary">See pricing</a>
</footer>
</div>
</li>
<li><a href="/about" class="btn ghost">About</a></li>
<li><a href="/contact" class="btn ghost">Contact</a></li>
</ul>
</nav>
Mega menu
Use class="flyout grid-3" when a nav panel needs multiple link groups. Keep links regular anchors and let the panel collapse to a one-column sheet on mobile with data-flyout-mobile="auto".
<nav aria-label="Product navigation">
<ul class="list-reset cluster">
<li class="flyout-trigger">
<button class="btn ghost"
type="button"
aria-expanded="false"
aria-controls="product-mega-menu">
Platform
<i class="ti ti-chevron-down" aria-hidden="true"></i>
</button>
<div class="flyout grid-3"
id="product-mega-menu"
aria-label="Platform"
data-flyout-mobile="auto"
style="--flyout-inline-size: 42rem; --flyout-max-inline-size: 42rem"
hidden>
<section aria-labelledby="mega-design">
<h3 id="mega-design">Design</h3>
<ul>
<li><a href="/figma">Figma integration</a></li>
<li><a href="/tokens">Design tokens</a></li>
<li><a href="/handoff">Developer handoff</a></li>
</ul>
</section>
<section aria-labelledby="mega-develop">
<h3 id="mega-develop">Development</h3>
<ul>
<li><a href="/components">Components</a></li>
<li><a href="/api">API</a></li>
<li><a href="/changelog">Changelog</a></li>
</ul>
</section>
<section aria-labelledby="mega-operate">
<h3 id="mega-operate">Operate</h3>
<ul>
<li><a href="/analytics">Analytics</a></li>
<li><a href="/security">Security</a></li>
<li><a href="/support">Support</a></li>
</ul>
</section>
<footer class="cluster"
style="grid-column: 1 / -1; --cluster-justify: flex-end; border-block-start: var(--border-width) solid var(--border)">
<a href="/pricing" class="btn primary">See pricing</a>
</footer>
</div>
</li>
</ul>
</nav>
Context menu
Put data-context-menu on the smallest unit the actions operate on: a file row, card, or canvas item. Several units may reference the same <menu class="flyout">. A normal flyout trigger inside that unit opens the same menu through the same context-aware path. Before opening, the context element dispatches the cancelable actual:context-menu event. Its detail contains the shared menu, the owning context, the exact origin, and the opening trigger (pointer, touch, keyboard, or button). Use it to tailor the static menu to the selected item, or cancel the event to keep it closed. contextFor(menu) from actual-css/js/context-menu returns that detail while handling a menu action. Add data-context-menu-scope only when the flyout should stay inside a specific region. Empty or self constrains to the target itself, parent constrains to the parent, and a selector constrains to the closest matching ancestor or first matching element. The scope also constrains the available height, so omit it when the menu is allowed to escape the card or list item. Long press is opt-in with data-context-menu-long-press. Empty uses the default delay; a number sets the delay in milliseconds.
Right click a detail below, press the context-menu key, or use More.
<div class="card stack"
id="file-card"
data-context-menu="file-actions"
data-context-menu-long-press
tabindex="0"
style="min-block-size: 12rem;">
<div class="cluster justify-content-space-between items-center">
<strong>File.pdf</strong>
<button class="btn ghost"
type="button"
aria-expanded="false"
aria-controls="file-actions"
id="file-actions-trigger">
More
</button>
</div>
<p class="text-sm text-muted">Right click a detail below, press the context-menu key, or use More.</p>
<div class="cluster">
<span class="badge" data-context-item="File name">File.pdf</span>
<span class="badge" data-context-item="Owner">Design team</span>
</div>
<output id="context-result" class="text-sm text-muted" aria-live="polite">No context selected.</output>
<menu class="flyout"
id="file-actions"
aria-labelledby="file-actions-trigger"
hidden>
<li><button type="button">Open</button></li>
<li><button type="button">Rename</button></li>
<li><hr /></li>
<li><button class="danger" type="button">Delete</button></li>
</menu>
</div>
<script>
const card = document.getElementById("file-card");
const result = document.getElementById("context-result");
card.addEventListener("actual:context-menu", (event) => {
const item = event.detail.origin.closest?.("[data-context-item]");
const label = item?.dataset.contextItem ?? "the card";
result.textContent = `Menu opened for ${label} (${event.detail.trigger}).`;
});
</script>