Skip to content
Component installs need the registry setup
VegaStack Design

Interaction patterns

System-wide behaviors — how state, selection, live updates, and keyboard affordances read consistently across every component.

Last updated

These are behaviors, not components: patterns that must read the same everywhere so the system feels like one product. Each is already expressed in tokens/components — this page is where the rule lives.

Open triggers stay lit

A trigger whose menu or popover is open keeps its hover treatment, so it never drops back to rest while its surface is showing — the lit trigger is the anchor the eye tracks. Two attributes carry it: Base UI's data-popup-open (NavigationMenuTrigger, menu sub-triggers, Select) and plain aria-expanded, which Button's outline, secondary and ghost variants answer with the same bg-muted step. A Button used as a custom trigger therefore needs nothing extra beyond setting aria-expanded from your open state.

Highlight-on-change

When a value updates live (a record field synced, a row inserted), pulse it with motion-flash — a 2px accent ring that swells and settles. Set --vs-flash-color to match the context (var(--info) for a synced field, the default ring otherwise). Honours prefers-reduced-motion (collapses to no-op). Use it for events the user should notice, not on every re-render.

Selection & cell focus

  • List/menu selection rides data-selected / data-highlighted → the neutral accent fill (never a hue).
  • Grid cell focus in DataGrid keeps the same 2px outline but draws it inside (focus-visible:-outline-offset-2), because the scroll container clips an outward one. Same width, same --ring — it is the one outline, not a second treatment.
  • Everything else keyboard-focusable gets the one 2px :focus-visible outline; text fields use the border-tint instead (a raw input can't tell mouse from keyboard).

The page doesn't bounce under a mouse

On a mouse or trackpad (pointer: fine), base.css sets overscroll-behavior-y: none on html and body (INT-12). The page no longer rubber-bands at its top and bottom edges, and scrolling past the end of an inner scroller (a list, a panel, a table) no longer carries on into the page behind it.

  • Vertical only. Horizontal overscroll is untouched, so a trackpad two-finger swipe still goes back and forward.
  • Fine pointers only. Touch devices keep the native bounce and pull-to-refresh.
  • Nothing to add per component. Don't restate it on a page or a scroller. An inner scroller that must contain its scroll on touch too still sets overscroll-contain / overscroll-y-contain itself, as Combobox's list and Board's lanes do.
/* @vegastack/design-tokens/base.css */
@media (pointer: fine) {
  html,
  body {
    overscroll-behavior-y: none;
  }
}

Keyboard affordances are visible

  • Shortcut hints render in Kbd, and Kbd composes inside buttons and menu rows (Cancel [ESC], Save [↵], Search records ↵) — the shortcut lives next to the action.
  • Command palettes and rich pickers can pin a footer hint bar: navigation hints left (↑ ↓ Navigate), the primary action right. Upstream's command ships no footer part — compose one under CommandList with Kbd.

Settings save model

Settings fields autosave. Text saves after a short debounce (about 800ms) and on blur; pickers, switches and swatches save at once. Each field shows its own quiet status inline (AutoSaveIndicator variant="icon", or AutoSaveInput), and a failed save rolls the value back and raises a toast with Retry. Validate inline (a required name, a length limit) before saving.

Consequential changes confirm first — anything that changes who can see something or removes it (making a space private, archiving, deleting, transferring ownership) opens an AlertDialog that says what will happen ("3 people who aren't members will lose access") and saves only on its confirm button, which repeats the verb.

Explicit Discard / Save is the exception: a multi-field draft that must apply together — a wizard step, a saved view's filter set (the data-grid view bar), a form whose fields only make sense as a whole. There the user owns when the change becomes everyone's, and an ActionBar shows "Unsaved changes" with Discard / Save.

Progressive disclosure

  • Overflow collapses behind a count: TagGroup +N, DataList's "View all values" — the collapsed control expands in place, it never navigates away.
  • In-place drilldown beats cascading flyouts for multi-step pickers (the filter builder): a back chevron swaps the panel content within one popover.

Density has tiers, not one size

Chrome runs tighter than content. Button and its relatives ship four heights — xs 24px, sm 28px, default 32px, lg 36px — with matching icon-xs / icon-sm / icon / icon-lg squares. Reach for size="sm", text-xs font-medium labels and text-muted-foreground secondary text on data-dense surfaces (a toolbar, a filter bar, a joined ToggleGroup); keep the default 32px tier for primary flows. Do not mix two tiers inside one control group.

On this page