zudo-text

検索したい単語を入力

いつでも検索バーを開ける

Buttons & Context Menus

Which button component to reach for, the visible-edge rule that keeps a grid of icons from reading as decoration, and the shared ContextMenu primitive every right-click surface builds on — durable public reference distilled from the Editing Keys Polish epic (#6674).

Note

This page distills the "Buttons" and "Context menus" sections of.claude/skills/l-design-system/SKILL.md into a durable public reference. That file is the quick-lookup source of truth for agents working in the codebase; this page is the same rules for anyone reading the docs site.

Buttons

Four components cover every button in the app, picked by what the surface around them already reads as:

ComponentFileUse for
Buttonpackages/ui-components/src/button.tsxDialog/form actions. Variants primary | secondary | ghost | danger | danger-ghost, sizes sm | md, an active prop.
IconButtonpackages/ui-components/src/icon-button.tsxA single icon action. toolbar variant is transparent — for a row that is itself already visibly a toolbar. square variant is bordered — for a standalone icon action that is NOT inside an obvious toolbar row.
DialogCloseButtonpackages/ui-components/src/dialog-close-button.tsxThe dialog-header close (X) only.
Keycap (appearance="keycap" on PaletteKeyButton)tauri-app/renderer/components/edit-key-bar/palette-key-button.tsxTouch key pads / grids of same-weight actions — the mobile More-tools panel. Backed by bg-keycap and the --spacing-keycap-edge bottom edge.

The edge rule

A button must have a visible edge or fill at rest unless it sits inside a container that already reads as a toolbar row. A grid of bare glyphs is not a set of buttons — that gap (the mobile More tools panel, before the keycap treatment landed) is exactly the defect this rule exists to prevent.

Concretely: a lone IconButton sitting on a plain background needs variant="square" (or the keycap treatment on touch); it never sits there with variant="toolbar" and no surrounding chrome that already signals "this row is a toolbar".

Filled accent is reserved

Filled accent (bg-accent text-on-accent — Button's primary variant) is reserved for isolated primary controls (the launcher, draft-bar pills) — never for list selection. See the design-token skill's "Active State" section for the selection tokens (bg-active/text-active-fg); filled accent is a distinct, louder pattern, not an interchangeable one.

States and sizing

Every button state comes from the same tokens regardless of which component renders it:

StateTokens
Hoverbg-hover text-hover-fg
Pressedbg-active text-active-fg
Disabledopacity-40
FocusfocusRingInsetCls (@takazudo/ui-components)

Sizes come from the button size tokens: --spacing-icon-btn-square (32px square IconButton), --spacing-icon-btn-mini (22px table-helper buttons), --spacing-close-btn (28px DialogCloseButton), and --spacing-touch-min (44px floor on mobile). Buttons keep the radius scale (--radius-sm/md/lg) — only dialog/panel surfaces are square.

New toolbar buttons must be hideable

Register them in TOOLBAR_SURFACES (packages/app-defaults/src/toolbar-surfaces.ts) and guard them with useToolbarContextMenu — see root CLAUDE.md § "Feature Implementation Checklist".

Context menus

Every new interactive surface ships with a designed context menu. Right-click must never fall through to the native WebKit menu (Reload / Inspect Element) on app chrome. The desktop fallback described below only hides the native menu as a last resort — it is a safety net, not a substitute for designing one.

Use the shared ContextMenu + useContextMenu (packages/ui-components/src/context-menu.tsx, use-context-menu.ts) for any menu that needs groups, checked rows, or a submenu. Never reach for ToolbarKebabMenu (packages/ui-components/src/toolbar-kebab-menu.tsx) for that case — it is a flat single-select list with no separators, labels, or checkbox rows.

Opening the menu

useContextMenu exposes two openers that share one item-builder function:

  • openFromEvent(event, context?) — right-click (and the ContextMenu key / Shift+F10, which browsers dispatch as the same contextmenu event).

  • openAtPoint(x, y, context?, trigger?) — a … button. Read the trigger's getBoundingClientRect() and pass its left/bottom; pass the trigger element itself as trigger so focus returns to it on close. See tauri-app/renderer/components/settings/device-management/device-context-menu.tsx for the full pattern, including click-to-toggle via menuProps.open / close() and ignoreRef.

Grouping

A { type: "label" } heading plus { type: "separator" } between groups. Actions of a different kind never share a group without a separator — for example a "Configure palette…" settings action is its own group, separate from placement actions. A current choice among alternatives (which placement is active, say) is a { type: "checkbox" } checked row via onToggle/checked — never a set of aria-pressed buttons standing in for a menu.

One ContextMenu per surface

The component is a single-instance registry (module-level — opening a second root menu evicts the first), so a list of rows (dashboard widgets, kanban cards, …) shares ONE ContextMenu instance rather than mounting one per row — see tauri-app/renderer/components/dashboard/dashboard-context-menu.tsx. A surface that already owns a right-click menu and also wants the toolbar customize entries merges visibility.menuItems from useToolbarContextMenu into its own item list instead of rendering that hook's menu (a second ContextMenu would evict the first).

Menu rows use menuItemCls (the shared Tier A class constant); the panel z-index is --z-context-menu.

Keep-native list

Deliberate, must stay native — do not add a custom menu here:

  • Every <input> / <textarea> / <select> / [contenteditable].

  • Any non-collapsed text selection, anywhere.

  • The editor when AppSettings.editor.nativeContextMenu is on.

  • Touch devices on the editor and preview panes — the native long-press callout is their only copy affordance.

  • The slide-deck preview.

  • The terminal idle area.

This is the epic #6674 keep-native inventory, enforced app-wide by the desktop fallback in tauri-app/renderer/lib/suppress-native-context-menu.ts (shouldSuppressNativeContextMenu / installNativeContextMenuSuppressor): one document-level bubble contextmenu listener that defers to event.defaultPrevented (so it never races a surface's own menu) and exempts the list above plus dev builds (so Inspect Element keeps working). A [data-native-context-menu] wrapper is the generic per-element opt-out for a surface that wants the native menu without owning a contextmenu handler.