Internal Tokens
Component-local tokens allowed when they reduce duplication or make component code clearer.
Internal tokens are allowed when they reduce duplication or make component code clearer.
Variant Plumbing
The shared visual variants are:
.solid: full background, paired foreground. This is the default for buttons and badges..soft: lightly tinted background, colored text. Good for alerts and secondary badges..outline: transparent background, colored border and text.
Buttons also support two button-only variants:
.ghost: transparent button with hover affordance..link: button semantics with link presentation.
Do not force .ghost or .link onto components where the interaction model does not fit. Shared API should follow shared use.
CSS
.btn {
--btn-bg: var(--ui-bg, var(--btn-default-bg, var(--intent, var(--neutral))));
--btn-fg: var(--ui-fg, var(--btn-default-fg, var(--intent-fg, var(--neutral-fg))));
--btn-border: var(--ui-border, var(--btn-default-border, transparent));
border: var(--border-width) solid var(--btn-border-color, var(--btn-border));
border-block-end-color: var(
--btn-border-block-end-color,
var(--btn-border-color, var(--btn-border))
);
border-block-end-width: var(--btn-border-block-end-width, var(--border-width));
}
Button-local defaults are intentionally inherited extension points for themes that change button grammar, such as turning the default solid button into a surface button with an intent-colored lower edge. Keep them local until the same need appears across multiple components. Text controls (.input/.textarea/.select, forms/control.css) expose the same lower-edge extension point: --control-border-block-end-color and --control-border-block-end-width are unset by default and fall back to the regular border, so a theme can give inputs an underline-style lower edge the same way edge.css does for buttons, without fighting the hover/focus/disabled state rules that still own --control-border itself. Alerts (components/alert.css) expose the same idea on the leading edge instead of the lower one: --alert-border-inline-start-color and --alert-border-inline-start-width are unset by default and fall back to the regular --ui-border, so a theme can turn an alert into a callout with a colored flag on the inline-start side, matching the classic notice pattern, without a per-variant override. A theme is not limited to the tokens above; it can restate any component-local token under a state selector to change the interaction recipe itself, not just the palette. edge.css does this for focus: the base input recipe (forms/control.css) flips --control-border to --focus on top of the shared outline + ring, which triples up with the lower-edge accent above. Edge drops the border-color flip under :focus-visible (thickening the accent edge instead) and also thins --focus-outline and tightens --focus-outline-offset so the outline hugs the control rather than floating a second ring around it. Buttons don't consume --focus-outline — button.css computes its own outline inline so the color can follow --btn-focus-color (intent-aware, unlike the plain --focus the shared token is hardcoded to), so it exposes a matching local token, --btn-focus-outline-width, defaulting to the same calc(var(--border-width) * 2) the old hardcoded value used. edge.css overrides it the same way it overrides --focus-outline for inputs. The outline overrides are gated in @media not (forced-colors: active). Under forced-colors, theme.css repoints --focus-outline to a Highlight-colored system outline via a :root, [data-theme="light"], [data-theme="dark"] rule — :root always matches the root element regardless of its data-theme attribute, so that fallback reaches every theme, edge included. But it is a lower-specificity, inherited value: an unguarded same-property override declared directly on .input would out-specificity it in every color mode, including forced-colors, because direct beats inherited regardless of the media condition either rule was written under. Any theme that restyles --focus-outline, --btn-focus-outline-width, or --focus-ring-shadow needs the same guard. Rules:
- Prefix internal component tokens with the component name (
--btn-,--alert-,--card-*). - Prefix shared variant plumbing with
--ui-*. - Keep component-specific variants in component code when they do not generalize.
- Do not require users to override internal tokens for ordinary theming.
- Promote an internal token to public only when there is a repeated, reasonable customization need.