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
destructivewhether 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. primaryis "the one action here." Exactly one control per context earns afill. Promotion setsemphasistofilland lifts a neutral action's tone toprimary; a promoted destructive action stays adestructivefill.
Pick emphasis by placement
fillis 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.outlineis 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.ghostis a dismiss or a dense-strip action: cancel, clear, the actions in a bulk-selection bar, an inline tertiary affordance.linkis inline within running prose only. Never in a toolbar or an action strip, where it breaks the control rhythm.
Pick tone by meaning
neutralis the resting default. A bare<Button>is a neutral fill, not a CTA.primaryis the earned accent. Reach for it only on the one promoted action per context (or set it explicitly for a deliberate CTA). Spreadingprimaryacross a cluster spends the accent that signals "the" action.destructivemarks an action that deletes data or is otherwise irreversible. Set the tone, not atext-destructiveclass: 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
| Placement | Primary action | Alternative | Dismiss | Destructive |
|---|---|---|---|---|
| Page-title action zone | primary fill, sm | neutral outline, sm | ghost sm | destructive fill, sm |
| Form / dialog footer | primary fill | neutral outline | ghost | destructive fill |
| Toolbar | neutral outline, sm | |||
| Bulk-selection bar | ghost sm | destructive ghost, sm | ||
| Pagination | neutral outline, icon-sm | |||
| Empty state | primary fill, sm | neutral outline, sm | ghost sm | |
| Inline (running prose) | primary link | neutral link | destructive 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.