Navigation
Menubar
A row of application menus such as File, Edit and View, for desktop-style tools with many commands.
- Built on
Popover APICSS anchor positioning@starting-styleRoving tabindex- Client JavaScript
- Yes, for state
- Works without React
- React only (needs JavaScript)
- Source, gzipped
- 5.3 kB
- Dependencies
- React
Code
"use client";
import { useState } from "react";
import { Menubar, MenubarCheckboxItem, MenubarItem, MenubarMenu, MenubarSeparator } from "@/components/equip/menubar";
export default function Demo() {
const [minimap, setMinimap] = useState(true);
const [wrap, setWrap] = useState(false);
return (
<Menubar aria-label="Editor">
<MenubarMenu label="File">
<MenubarItem shortcut="⌘N">New file</MenubarItem>
<MenubarItem shortcut="⌘O">Open…</MenubarItem>
<MenubarItem shortcut="⌘S">Save</MenubarItem>
<MenubarItem disabled>Save all</MenubarItem>
<MenubarSeparator />
<MenubarItem shortcut="⌘W">Close tab</MenubarItem>
</MenubarMenu>
<MenubarMenu label="Edit">
<MenubarItem shortcut="⌘Z">Undo</MenubarItem>
<MenubarItem shortcut="⇧⌘Z">Redo</MenubarItem>
<MenubarSeparator />
<MenubarItem shortcut="⌘F">Find</MenubarItem>
<MenubarItem shortcut="⌥⌘F">Replace</MenubarItem>
</MenubarMenu>
<MenubarMenu label="View">
<MenubarCheckboxItem checked={minimap} onCheckedChange={setMinimap}>
Minimap
</MenubarCheckboxItem>
<MenubarCheckboxItem checked={wrap} onCheckedChange={setWrap} shortcut="⌥Z">
Word wrap
</MenubarCheckboxItem>
<MenubarSeparator />
<MenubarItem shortcut="⌘+">Zoom in</MenubarItem>
<MenubarItem shortcut="⌘−">Zoom out</MenubarItem>
</MenubarMenu>
</Menubar>
);
}Examples
Acting on a choice
onSelect runs, the menu closes and focus returns to its top-level item, ready for the next command.
Last command: Nothing yet
Code
"use client";
import { useState } from "react";
import { Menubar, MenubarItem, MenubarMenu, MenubarSeparator } from "@/components/equip/menubar";
export default function Example() {
const [last, setLast] = useState("Nothing yet");
return (
<div style={{ display: "grid", gap: 8, justifyItems: "start" }}>
<Menubar aria-label="Spreadsheet">
<MenubarMenu label="Insert">
<MenubarItem onSelect={() => setLast("Inserted row above")}>Row above</MenubarItem>
<MenubarItem onSelect={() => setLast("Inserted row below")}>Row below</MenubarItem>
<MenubarItem onSelect={() => setLast("Inserted column")}>Column</MenubarItem>
</MenubarMenu>
<MenubarMenu label="Format">
<MenubarItem shortcut="⌘B" onSelect={() => setLast("Bold")}>Bold</MenubarItem>
<MenubarItem shortcut="⌘I" onSelect={() => setLast("Italic")}>Italic</MenubarItem>
<MenubarSeparator />
<MenubarItem onSelect={() => setLast("Formatting cleared")}>Clear formatting</MenubarItem>
</MenubarMenu>
<MenubarMenu label="Data">
<MenubarItem onSelect={() => setLast("Sorted A to Z")}>Sort A to Z</MenubarItem>
<MenubarItem onSelect={() => setLast("Filter created")}>Create filter</MenubarItem>
<MenubarItem disabled>Pivot table</MenubarItem>
</MenubarMenu>
</Menubar>
<span style={{ fontSize: 12, color: "var(--eq-ink-2)" }}>Last command: {last}</span>
</div>
);
}Checkbox items
Checkbox items stay open when toggled, so several panels can be switched in one visit.
Code
"use client";
import { useState } from "react";
import { Menubar, MenubarCheckboxItem, MenubarItem, MenubarMenu } from "@/components/equip/menubar";
export default function Example() {
const [panels, setPanels] = useState({ sidebar: true, terminal: false, problems: true });
const set = (key: keyof typeof panels) => (checked: boolean) => setPanels((p) => ({ ...p, [key]: checked }));
return (
<Menubar aria-label="Workbench">
<MenubarMenu label="View">
<MenubarCheckboxItem checked={panels.sidebar} onCheckedChange={set("sidebar")} shortcut="⌘B">
Sidebar
</MenubarCheckboxItem>
<MenubarCheckboxItem checked={panels.terminal} onCheckedChange={set("terminal")} shortcut="⌃`">
Terminal
</MenubarCheckboxItem>
<MenubarCheckboxItem checked={panels.problems} onCheckedChange={set("problems")} shortcut="⇧⌘M">
Problems
</MenubarCheckboxItem>
</MenubarMenu>
<MenubarMenu label="Help">
<MenubarItem>Keyboard shortcuts</MenubarItem>
<MenubarItem>Release notes</MenubarItem>
</MenubarMenu>
</Menubar>
);
}Props
| Prop | Type | Default | What it does |
|---|---|---|---|
...rest | ComponentProps<"div"> | none | Every native div attribute. Pass aria-label to name the bar. |
MenubarMenu label | ReactNode | none | Text of the top-level item, e.g. "File". |
MenubarMenu children | ReactNode | none | MenubarItem, MenubarCheckboxItem and MenubarSeparator elements. |
MenubarMenu ...rest | ComponentProps<"button"> | none | Every native button attribute, applied to the top-level item. |
MenubarItem onSelect | () => void | none | Runs when the item is chosen. The menu then closes. |
MenubarItem shortcut | string | none | A shortcut hint at the end of the row. Display only; it does not bind the key. |
MenubarItem disabled | boolean | none | Sets aria-disabled. The item stays focusable but does nothing. |
MenubarCheckboxItem checked | boolean | none | Whether the item is ticked. |
MenubarCheckboxItem onCheckedChange | (checked: boolean) => void | none | Called with the new state. The menu stays open. |
MenubarCheckboxItem shortcut | string | none | A shortcut hint at the end of the row. Display only. |
Anatomy
| root | .eq-menubar | The element with role="menubar". |
|---|---|---|
| menu | .eq-menubar-menu | Wraps one trigger and its list; role="none". |
| trigger | .eq-menubar-trigger | A top-level menuitem that opens its list. |
| trigger label | .eq-menubar-trigger-label | |
| list | .eq-menubar-list | The popover with role="menu". |
| item | .eq-menubar-item | |
| check | .eq-menubar-check | The tick gutter, shown only in menus with checkbox items. |
| item label | .eq-menubar-item-label | Type-ahead matches against this text. |
| shortcut | .eq-menubar-shortcut | |
| separator | .eq-menubar-separator |
Keyboard
| ArrowRightArrowLeft | Moves between top-level items, wrapping. With a menu open, opens the neighbouring menu instead. |
|---|---|
| ArrowDownEnterSpace | On a top-level item, opens its menu on the first item. |
| ArrowUp | On a top-level item, opens its menu on the last item. In a menu, moves to the previous item. |
| ArrowDown | In a menu, moves to the next item, wrapping. |
| HomeEnd | Moves to the first or last top-level item, or the first or last item in a menu. |
| A-Z | Moves to the next item whose label starts with that letter. |
| EnterSpace | In a menu, chooses the item, or toggles a checkbox item. |
| Escape | Closes the menu and returns focus to its top-level item. |
| Tab | Closes the menu and moves focus on out of the menubar. |
Notes
- The bar is one tab stop with a roving tabindex; the last top-level item used keeps it.
- Hovering another top-level item switches menus only while one is already open, and only for a mouse, as desktop menubars do.
- Pressing outside the menubar closes the open menu without moving focus.
- Checkbox items use role="menuitemcheckbox" with aria-checked and keep the menu open, so several can be toggled in one visit.
- Disabled items stay focusable so they can be discovered, as the menu pattern recommends, but cannot be chosen.
- Submenus are not supported; keep each menu one level deep.
- CSS anchor positioning is used inside @supports when the engine has anchor-name, position-area and position-try-fallbacks. Elsewhere each menu is placed with fixed coordinates from its trigger when it opens and does not follow scrolling. The exit fade needs transition of overlay, which only Chromium has; elsewhere menus close at once.
Source
Two files, and they are the whole part. Open either to read it, copy it straight from its bar, or add the part to your loadout and install it with everything else you picked.
components/equip/menubar.tsx449 lines
"use client";
import {
createContext,
useContext,
useEffect,
useId,
useLayoutEffect,
useRef,
useState,
type ComponentProps,
type KeyboardEvent,
type PointerEvent,
type ReactNode,
} from "react";
import "./menubar.css";
type Focus = "first" | "last" | "none";
type MenubarContext = {
open: string | null;
tabStop: string | null;
openMenu: (id: string, focus: Focus) => void;
close: (returnFocus: boolean) => void;
step: (id: string, delta: number) => void;
};
const Context = createContext<MenubarContext>({
open: null,
tabStop: null,
openMenu: () => {},
close: () => {},
step: () => {},
});
const CloseMenu = createContext<() => void>(() => {});
/* Must match the @supports condition in menubar.css exactly. */
const ANCHOR_SUPPORT =
"(anchor-name: --a) and (position-area: block-end) and (position-try-fallbacks: flip-block)";
function itemsIn(list: Element | null | undefined) {
return Array.from(list?.querySelectorAll<HTMLElement>('[role="menuitem"], [role="menuitemcheckbox"]') ?? []);
}
function startsWith(node: Element, selector: string, letter: string) {
return (node.querySelector(selector)?.textContent ?? node.textContent ?? "").trim().toLowerCase().startsWith(letter);
}
export type MenubarProps = ComponentProps<"div">;
export function Menubar({ className, children, onKeyDown, onBlur, ...rest }: MenubarProps) {
const ref = useRef<HTMLDivElement>(null);
const [open, setOpen] = useState<string | null>(null);
const [tabStop, setTabStop] = useState<string | null>(null);
const focusOnOpen = useRef<Focus>("first");
function triggers() {
return Array.from(ref.current?.querySelectorAll<HTMLButtonElement>(".eq-menubar-trigger") ?? []);
}
function trigger(id: string) {
return triggers().find((node) => node.dataset.menu === id);
}
function focusItem(id: string, which: Focus) {
if (which === "none") return;
const list = ref.current?.querySelector(`.eq-menubar-list[data-menu="${CSS.escape(id)}"]`);
const items = itemsIn(list);
items[which === "last" ? items.length - 1 : 0]?.focus();
}
function openMenu(id: string, focus: Focus) {
setTabStop(id);
if (open === id) {
focusItem(id, focus);
return;
}
focusOnOpen.current = focus;
setOpen(id);
}
function close(returnFocus: boolean) {
const id = open;
setOpen(null);
if (returnFocus && id) trigger(id)?.focus();
}
function step(id: string, delta: number) {
const all = triggers();
const index = all.findIndex((node) => node.dataset.menu === id);
const next = all[(index + delta + all.length) % all.length];
const nextId = next?.dataset.menu;
if (!next || !nextId) return;
setTabStop(nextId);
if (open !== null) {
// Focus the trigger first so focus never drops to the page while the menus swap.
next.focus();
focusOnOpen.current = "first";
setOpen(nextId);
} else {
next.focus();
}
}
// Child menus show their popover in a layout effect, which runs before this one.
useEffect(() => {
if (open) focusItem(open, focusOnOpen.current);
}, [open]);
useEffect(() => {
if (tabStop === null) setTabStop(triggers()[0]?.dataset.menu ?? null);
}, [tabStop]);
useEffect(() => {
if (open === null) return;
function onPointerDown(event: globalThis.PointerEvent) {
if (event.target instanceof Node && ref.current?.contains(event.target)) return;
setOpen(null);
}
document.addEventListener("pointerdown", onPointerDown, true);
return () => document.removeEventListener("pointerdown", onPointerDown, true);
}, [open]);
function handleKeyDown(event: KeyboardEvent<HTMLDivElement>) {
onKeyDown?.(event);
if (event.defaultPrevented) return;
const target = event.target;
if (!(target instanceof HTMLElement) || !target.classList.contains("eq-menubar-trigger")) return;
const id = target.dataset.menu;
if (!id) return;
const all = triggers();
switch (event.key) {
case "ArrowRight":
step(id, 1);
break;
case "ArrowLeft":
step(id, -1);
break;
case "ArrowDown":
openMenu(id, "first");
break;
case "ArrowUp":
openMenu(id, "last");
break;
case "Home":
step(all[0]?.dataset.menu ?? id, 0);
break;
case "End":
step(all[all.length - 1]?.dataset.menu ?? id, 0);
break;
case "Escape":
if (open === null) return;
close(true);
break;
default: {
if (event.key.length !== 1 || event.key === " ") return;
if (event.ctrlKey || event.metaKey || event.altKey) return;
const letter = event.key.toLowerCase();
const index = all.indexOf(target as HTMLButtonElement);
for (let n = 1; n <= all.length; n++) {
const candidate = all[(index + n) % all.length];
if (startsWith(candidate, ".eq-menubar-trigger-label", letter)) {
step(candidate.dataset.menu ?? id, 0);
break;
}
}
}
}
event.preventDefault();
}
return (
<div
{...rest}
ref={ref}
role="menubar"
className={["eq-menubar", className].filter(Boolean).join(" ")}
onKeyDown={handleKeyDown}
onBlur={(event) => {
onBlur?.(event);
const next = event.relatedTarget;
if (open !== null && next instanceof Node && !ref.current?.contains(next)) setOpen(null);
}}
>
<Context.Provider value={{ open, tabStop, openMenu, close, step }}>{children}</Context.Provider>
</div>
);
}
export type MenubarMenuProps = Omit<ComponentProps<"button">, "children"> & {
/** Text of the top-level item, e.g. "File". */
label: ReactNode;
/** MenubarItem, MenubarCheckboxItem and MenubarSeparator elements. */
children: ReactNode;
};
/** Fallback for engines without anchor positioning: fixed coordinates from the trigger's box. */
function place(trigger: Element, surface: HTMLElement) {
const box = trigger.getBoundingClientRect();
const viewWidth = document.documentElement.clientWidth;
const viewHeight = document.documentElement.clientHeight;
const gap = parseFloat(getComputedStyle(surface).marginTop) || 0;
const above = box.bottom + surface.offsetHeight + gap > viewHeight && box.top - surface.offsetHeight - gap >= 0;
const end = box.left + surface.offsetWidth > viewWidth && box.right - surface.offsetWidth >= 0;
const style = surface.style;
style.top = above ? "auto" : `${box.bottom}px`;
style.bottom = above ? `${viewHeight - box.top}px` : "auto";
style.left = end ? "auto" : `${box.left}px`;
style.right = end ? `${viewWidth - box.right}px` : "auto";
}
export function MenubarMenu({
label,
id,
className,
style,
children,
onClick,
onPointerEnter,
...rest
}: MenubarMenuProps) {
const { open, tabStop, openMenu, close, step } = useContext(Context);
const auto = useId();
const menu = `m${auto.replace(/[^\w-]/g, "")}`;
const triggerId = id ?? `${auto}trigger`;
const listId = `${auto}menu`;
const anchorName = `--eq-menubar-${menu}`;
const triggerRef = useRef<HTMLButtonElement>(null);
const listRef = useRef<HTMLDivElement>(null);
const isOpen = open === menu;
useLayoutEffect(() => {
const list = listRef.current;
const button = triggerRef.current;
if (!list || !button) return;
const showing = list.matches(":popover-open");
if (isOpen && !showing) {
list.showPopover();
if (!CSS.supports(ANCHOR_SUPPORT)) place(button, list);
} else if (!isOpen && showing) {
list.hidePopover();
}
}, [isOpen]);
function handleListKeyDown(event: KeyboardEvent<HTMLDivElement>) {
const all = itemsIn(listRef.current);
const current = all.findIndex((item) => item === document.activeElement);
let next: number | undefined;
switch (event.key) {
case "ArrowDown":
next = current < 0 ? 0 : (current + 1) % all.length;
break;
case "ArrowUp":
next = current < 0 ? all.length - 1 : (current - 1 + all.length) % all.length;
break;
case "Home":
next = 0;
break;
case "End":
next = all.length - 1;
break;
case "ArrowRight":
case "ArrowLeft":
event.preventDefault();
step(menu, event.key === "ArrowRight" ? 1 : -1);
return;
case "Escape":
event.preventDefault();
close(true);
return;
case "Tab":
// Back to the trigger first, so the browser's Tab moves on out of the menubar.
close(true);
return;
default: {
if (event.key.length !== 1 || event.key === " ") return;
if (event.ctrlKey || event.metaKey || event.altKey) return;
const letter = event.key.toLowerCase();
for (let n = 1; n <= all.length; n++) {
const index = (current + n + all.length) % all.length;
if (startsWith(all[index], ".eq-menubar-item-label", letter)) {
next = index;
break;
}
}
}
}
if (next === undefined || all.length === 0) return;
event.preventDefault();
all[next]?.focus();
}
return (
<div role="none" className="eq-menubar-menu">
<button
type="button"
{...rest}
ref={triggerRef}
id={triggerId}
role="menuitem"
className={["eq-menubar-trigger", className].filter(Boolean).join(" ")}
style={{ anchorName, ...style }}
data-menu={menu}
tabIndex={tabStop === menu || tabStop === null ? 0 : -1}
aria-haspopup="menu"
aria-expanded={isOpen}
aria-controls={listId}
onClick={(event) => {
onClick?.(event);
if (event.defaultPrevented) return;
if (isOpen) close(false);
else openMenu(menu, "first");
}}
onPointerEnter={(event: PointerEvent<HTMLButtonElement>) => {
onPointerEnter?.(event);
// Moving across the bar switches menus, but only once one is open, and only for a mouse.
if (event.pointerType !== "mouse" || open === null || isOpen) return;
event.currentTarget.focus({ preventScroll: true });
openMenu(menu, "none");
}}
>
<span className="eq-menubar-trigger-label">{label}</span>
</button>
<div
ref={listRef}
id={listId}
popover="manual"
role="menu"
aria-labelledby={triggerId}
className="eq-menubar-list"
data-menu={menu}
style={{ positionAnchor: anchorName }}
onKeyDown={handleListKeyDown}
>
<CloseMenu.Provider value={() => close(true)}>{children}</CloseMenu.Provider>
</div>
</div>
);
}
export type MenubarItemProps = Omit<ComponentProps<"button">, "onSelect"> & {
/** Runs when the item is chosen by click, Enter or Space. The menu then closes. */
onSelect?: () => void;
/** A shortcut hint shown at the end of the row, e.g. "⌘S". Display only. */
shortcut?: string;
};
function focusOnPointer(event: PointerEvent<HTMLButtonElement>) {
if (document.activeElement !== event.currentTarget) event.currentTarget.focus({ preventScroll: true });
}
export function MenubarItem({
onSelect,
shortcut,
disabled,
className,
children,
onClick,
onPointerMove,
...rest
}: MenubarItemProps) {
const close = useContext(CloseMenu);
return (
<button
type="button"
{...rest}
role="menuitem"
tabIndex={-1}
aria-disabled={disabled || undefined}
className={["eq-menubar-item", className].filter(Boolean).join(" ")}
onPointerMove={(event) => {
onPointerMove?.(event);
focusOnPointer(event);
}}
onClick={(event) => {
if (disabled) return;
onClick?.(event);
if (event.defaultPrevented) return;
onSelect?.();
close();
}}
>
<span className="eq-menubar-check" aria-hidden="true" />
<span className="eq-menubar-item-label">{children}</span>
{shortcut && <kbd className="eq-menubar-shortcut">{shortcut}</kbd>}
</button>
);
}
export type MenubarCheckboxItemProps = Omit<ComponentProps<"button">, "onSelect"> & {
checked: boolean;
onCheckedChange: (checked: boolean) => void;
shortcut?: string;
};
export function MenubarCheckboxItem({
checked,
onCheckedChange,
shortcut,
disabled,
className,
children,
onClick,
onPointerMove,
...rest
}: MenubarCheckboxItemProps) {
return (
<button
type="button"
{...rest}
role="menuitemcheckbox"
tabIndex={-1}
aria-checked={checked}
aria-disabled={disabled || undefined}
className={["eq-menubar-item", className].filter(Boolean).join(" ")}
onPointerMove={(event) => {
onPointerMove?.(event);
focusOnPointer(event);
}}
onClick={(event) => {
if (disabled) return;
onClick?.(event);
if (!event.defaultPrevented) onCheckedChange(!checked);
}}
>
<span className="eq-menubar-check" aria-hidden="true">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M3.5 8.5l3 3 6-7" pathLength={1} />
</svg>
</span>
<span className="eq-menubar-item-label">{children}</span>
{shortcut && <kbd className="eq-menubar-shortcut">{shortcut}</kbd>}
</button>
);
}
export function MenubarSeparator({ className, ...rest }: ComponentProps<"div">) {
return (
<div
role="separator"
{...rest}
className={["eq-menubar-separator", className].filter(Boolean).join(" ")}
/>
);
}components/equip/menubar.css230 lines
@layer equip {
.eq-menubar {
display: inline-flex;
align-items: center;
gap: 2px;
padding: 3px;
border: 1px solid var(--eq-line);
border-radius: var(--eq-radius-m);
color: var(--eq-ink);
font: var(--eq-weight) var(--eq-text-m) / 1.3 var(--eq-font);
}
.eq-menubar-menu {
display: contents;
}
.eq-menubar-trigger {
display: inline-flex;
align-items: center;
height: var(--eq-h-s);
padding-inline: var(--eq-space-2);
border: 0;
border-radius: var(--eq-radius-s);
background: transparent;
color: var(--eq-ink);
font: var(--eq-weight-strong) var(--eq-text-s) / 1 var(--eq-font);
white-space: nowrap;
cursor: pointer;
user-select: none;
transition:
background-color var(--eq-dur) var(--eq-ease),
scale var(--eq-dur) var(--eq-ease);
}
@media (hover: hover) {
.eq-menubar-trigger:hover {
background: var(--eq-tint);
}
}
.eq-menubar-trigger[aria-expanded="true"] {
background: var(--eq-tint-strong);
}
.eq-menubar-trigger:active {
scale: var(--eq-press);
}
.eq-menubar-trigger:focus-visible {
outline: 2px solid var(--eq-focus);
outline-offset: 2px;
}
.eq-menubar-list {
box-sizing: border-box;
/* Undo the UA's centring: the menu hangs from its trigger. */
position: fixed;
inset: auto;
margin: var(--eq-space-1) 0;
min-width: 208px;
max-width: calc(100vw - 2 * var(--eq-space-4));
max-height: calc(100dvh - 2 * var(--eq-space-4));
overflow: auto;
/* Rows carry their own padding, so the surface keeps only room for the focus ring. */
padding: var(--eq-space-1);
border: 1px solid var(--eq-line-strong);
border-radius: var(--eq-radius-l);
background: var(--eq-raised);
color: var(--eq-ink);
box-shadow: var(--eq-shadow);
-webkit-backdrop-filter: var(--eq-blur);
backdrop-filter: var(--eq-blur);
opacity: 0;
scale: 0.96;
translate: 0 -4px;
transform-origin: top left;
transition:
opacity var(--eq-dur) var(--eq-ease),
scale var(--eq-dur) var(--eq-ease),
translate var(--eq-dur) var(--eq-ease);
}
.eq-menubar-list:popover-open {
display: flex;
flex-direction: column;
opacity: 1;
scale: none;
translate: none;
transition:
opacity var(--eq-dur) var(--eq-ease),
scale var(--eq-dur-move) var(--eq-spring),
translate var(--eq-dur-move) var(--eq-spring);
}
@starting-style {
.eq-menubar-list:popover-open {
opacity: 0;
scale: 0.96;
translate: 0 -4px;
}
}
.eq-menubar-item {
display: flex;
align-items: center;
gap: var(--eq-space-2);
flex: none;
width: 100%;
min-height: var(--eq-h-s);
padding: 0 var(--eq-space-2);
border: 0;
border-radius: var(--eq-radius-s);
background: transparent;
color: var(--eq-ink);
font: var(--eq-weight) var(--eq-text-m) / 1.3 var(--eq-font);
text-align: start;
white-space: nowrap;
cursor: pointer;
transition: scale var(--eq-dur) var(--eq-ease);
}
.eq-menubar-item:active:not([aria-disabled="true"]) {
scale: var(--eq-press);
}
.eq-menubar-item:focus {
outline: none;
background: var(--eq-tint);
}
.eq-menubar-item:focus-visible {
outline: 2px solid var(--eq-focus);
outline-offset: -2px;
}
.eq-menubar-item[aria-disabled="true"] {
opacity: 0.45;
cursor: not-allowed;
}
.eq-menubar-item[aria-disabled="true"]:focus {
background: transparent;
}
/* A gutter for check marks, only in menus that have checkbox items. */
.eq-menubar-check {
flex: none;
display: grid;
place-items: center;
width: 16px;
height: 16px;
}
.eq-menubar-list:not(:has([role="menuitemcheckbox"])) .eq-menubar-check {
display: none;
}
.eq-menubar-check > svg {
width: 16px;
height: 16px;
opacity: 0;
transition: opacity var(--eq-dur) var(--eq-ease);
}
/* The tick draws along its stroke (pathLength="1"); unticking fades and rewinds quickly. */
.eq-menubar-check path {
stroke-dasharray: 2 3;
stroke-dashoffset: 2;
transition: stroke-dashoffset var(--eq-dur) var(--eq-ease);
}
.eq-menubar-item[aria-checked="true"] .eq-menubar-check > svg {
opacity: 1;
}
.eq-menubar-item[aria-checked="true"] .eq-menubar-check path {
stroke-dashoffset: 1;
transition: stroke-dashoffset var(--eq-dur-move) var(--eq-spring);
}
.eq-menubar-item-label {
flex: 1;
}
.eq-menubar-shortcut {
margin-inline-start: auto;
padding-inline-start: var(--eq-space-4);
color: var(--eq-ink-2);
font-family: var(--eq-font-mono);
font-variation-settings: var(--eq-font-mono-settings);
font-size: var(--eq-text-xs);
}
.eq-menubar-separator {
flex: none;
height: 1px;
margin: var(--eq-space-1) calc(-1 * var(--eq-space-1));
background: var(--eq-line);
}
/* Keeping the surface in the top layer while it fades out needs overlay, which only some
engines can transition. Elsewhere it closes at once. */
@supports (overlay: auto) {
.eq-menubar-list {
transition:
opacity var(--eq-dur) var(--eq-ease),
scale var(--eq-dur) var(--eq-ease),
translate var(--eq-dur) var(--eq-ease),
display var(--eq-dur) allow-discrete,
overlay var(--eq-dur) allow-discrete;
}
.eq-menubar-list:popover-open {
transition:
opacity var(--eq-dur) var(--eq-ease),
scale var(--eq-dur-move) var(--eq-spring),
translate var(--eq-dur-move) var(--eq-spring),
display var(--eq-dur) allow-discrete,
overlay var(--eq-dur) allow-discrete;
}
}
/* Anchor positioning where the engine has it. menubar.tsx uses the same condition. */
@supports (anchor-name: --a) and (position-area: block-end) and (position-try-fallbacks: flip-block) {
.eq-menubar-list {
position-area: block-end span-inline-end;
position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
}
}
}