Skip to content

Buttons

The button family covers every clickable affordance in vueda: primary buttons, button groups, toggles, switches, and the keyboard caps that pair with them. All members share the same control sizing (32 / 28 / 40, governed by Control sizing (VUEDA canon) tokens.css-token:vueda-control-height and its sm / lg companions) and focus treatment (2 px solid Color palette: light tokens.css-token:ring outline at 2 px offset).

This page is the visual contract the default theme guarantees. Use it as the target spec when you re-skin: every cell shown here should still read as the same control after a customization, even if its color, radius, or density shifts. If a cell breaks, the change has crossed from skin into design language.

For the mechanics of overriding any of this, see Customize VUEDA Appearance. In brief: values (color, dimension, duration) belong in CSS tokens; compositions (class arrangements, state recipes) belong in theme keys.

Button: state matrix

Every tone × emphasis × size × state the default theme renders. The matrix below exercises the same Tailwind utilities the real component does, with hover and focus-visible reproduced via the docs harness so every cell is verifiable without pointer or keyboard.

Theme keys: Button, composing _ButtonBase plus the _Button* primitive for the resolved (tone, emphasis) cell (see Tone × emphasis below). To restyle every button-shaped surface in the app (calendar day cells, pagination items, dialog actions), override the matching _Button* meta key rather than Button itself.

Button: tone × emphasis

Button resolves on two axes: tone (neutral · primary · destructive, the color) and emphasis (fill · outline · ghost · link, the structure). A bare <Button> is neutral fill. The primary CTA is explicit with tone="primary", and secondary structure is explicit with emphasis. The grid below drives each cell from the two-axis props directly.

Theme keys: each cell composes _ButtonBase plus its tone primitive: fills are _ButtonDefault / _ButtonSecondary / _ButtonDestructive; outlines _ButtonOutline / _ButtonPrimaryOutline / _ButtonDestructiveOutline; ghosts _ButtonGhost / _ButtonPrimaryGhost / _ButtonDestructiveGhost; links _ButtonNeutralLink / _ButtonLink / _ButtonDestructiveLink.

Choosing tone and emphasis

The two axes answer two independent questions, and the default theme keeps them independent. Decide each separately:

  • emphasis is placement. How loud the control is and what chrome it sits in. It does not depend on what the action does.
  • tone is meaning. What the action is. A delete reads destructive whether it is a page hero or a quiet row glyph; the tone is set once and does not change as the same action moves between contexts.
  • primary is "the one action here." Exactly one control per context earns a fill. Promotion sets emphasis to fill and lifts a neutral action's tone to primary; a promoted destructive action stays a destructive fill.

Pick emphasis by placement

  • fill is the single earned action in a context: the form submit, the confirm in a dialog, the hero action in a page title. One per context.
  • outline is a genuine alternative that still deserves a chip: a secondary form action, a page-title secondary action, a toolbar trigger, pagination, an error-recovery retry.
  • ghost is a dismiss or a dense-strip action: cancel, clear, the actions in a bulk-selection bar, an inline tertiary affordance.
  • link is inline within running prose only. Never in a toolbar or an action strip, where it breaks the control rhythm.

Pick tone by meaning

  • neutral is the resting default. A bare <Button> is a neutral fill, not a CTA.
  • primary is the earned accent. Reach for it only on the one promoted action per context (or set it explicitly for a deliberate CTA). Spreading primary across a cluster spends the accent that signals "the" action.
  • destructive marks an action that deletes data or is otherwise irreversible. Set the tone, not a text-destructive class: a destructive ghost (tone="destructive" emphasis="ghost") is a quiet red row action, a destructive fill is a confirm hero. The tone composes the right _ButtonDestructive* primitive for whatever emphasis the placement chose.

Size by chrome density

Size is a third, independent placement axis: sm in dense chrome (titles, toolbars, dialogs, bulk bars), default (32px) in form footers, lg for auth and full-page CTAs, icon-sm for pagination.

Placement reference

PlacementPrimary actionAlternativeDismissDestructive
Page-title action zoneprimary fill, smneutral outline, smghost smdestructive fill, sm
Form / dialog footerprimary fillneutral outlineghostdestructive fill
Toolbarneutral outline, sm
Bulk-selection barghost smdestructive ghost, sm
Paginationneutral outline, icon-sm
Empty stateprimary fill, smneutral outline, smghost sm
Inline (running prose)primary linkneutral linkdestructive link

For buttons that navigate to a model action, this resolution happens automatically: every action button renders through Button via LinkModelView, which reads the action's intrinsic tone (a delete / destroy action is destructive, everything else neutral), applies the placement emphasis chosen by the surrounding view, and promotes the view's hero action to a fill. Authoring a <Button> by hand should follow the same table so hand-placed and resolved buttons read identically.

ButtonGroup: composition matrix

ButtonGroup adjusts its children's radii and shared edges so adjacent buttons read as a single control. The interesting axis is composition: orientation, child variant, and what kind of children sit inside (buttons, static text, separators). Child-button state shifts (hover, focus, disabled) are already covered by the Button matrix above.

Theme keys: ButtonGroup, ButtonGroupText, ButtonGroupSeparator. The seam treatment is a child-radius and hairline-overlap override applied via the parent's data attributes, so re-skinning the seam means editing these keys, not the token layer.

Toggle: state matrix

Toggle adds a pressed axis on top of Button's tone × emphasis × size × state cube. Each cell shows the size row and state row twice: once unpressed, once pressed, so the press delta is visible against every other state.

Theme key: Toggle. The pressed surface reads from Color palette: light tokens.css-token:accent / Color palette: light tokens.css-token:accent-foreground, the same pair that drives hover on ghost and outline buttons; rebranding the accent shifts all three in lockstep.

ToggleGroup: composition matrix

ToggleGroup's interesting axis is composition (selection mode, spacing, child variant), not state. Per-item states are covered by the Toggle matrix above.

Theme keys: ToggleGroup, ToggleGroupItem. Items compose from the Toggle key, so a restyle of Toggle flows through here automatically.

Switch: state matrix

Switch has no variant or size axis in the current skin, so the matrix collapses to off | on × default | hover | focus-visible | disabled.

Theme key: Switch. Token surface: Color palette: light tokens.css-token:input (off track), Color palette: light tokens.css-token:primary (on track), Color palette: light tokens.css-token:background (thumb), Color palette: light tokens.css-token:ring (focus). Switch is the one place a brand's Color palette: light tokens.css-token:primary reads as a fill rather than as a CTA; verify the on-track contrast against the thumb when retoning primary.

Kbd: content & composition

Kbd is a stateless typographic element, so the interesting axes are content (does a 1-char digit look balanced next to a 3-char word?) and composition (bare cap, grouped shortcut, embedded in another control).

Cap typography reads from Typography tokens.css-token:vueda-text-micro (11px) and the mono stack (Typography tokens.css-token:vueda-font-mono); the rounded chiclet shape follows Semantic radius tokens.css-token:vueda-checkbox-radius so caps match the form-control language rather than the slab-control language. The keycap edge uses the hairline box-shadow contract, the same DPR-tracked edge as buttons and inputs.

Documents matching: server v3.0.0a1.post1client v3.0.0-alpha.2