Skip to main content

Component Variants and States

Premise

Every component exists in multiple variants (primary, secondary, ghost…) and multiple states (normal, hover, active, focus, disabled). The token system treats variants and states as independent, predictable dimensions — not as ad hoc combinations.

This document shows how to map each variant × state combination to the correct token, and how to apply that pattern to any component in the DS.


The Model: Variant × State × Property

The interface token structure follows a three-axis pattern:

semantic.color.interface.function.{variant}.{state}.{property}
AxisAvailable values
Variantprimary, secondary, link, active, disabled
Statenormal, action, active (disabled only has normal)
Propertybackground, txtOn, border, txt (since 3.6.0)

These three axes form the functional token grid. For any combination of variant and state, the corresponding token already exists in the build.


States — CSS Mapping

Token stateCSS trigger
normalDefault state — no pseudo-class
action:hover, :focus-visible (during visual interaction feedback)
active:active (pressed), [aria-selected="true"], [aria-pressed="true"]

The disabled state is a special case: it only has normal and never responds to :hover or :active.


Function Variants — Buttons and Controls

Token table by variant

VariantWhen to use
primaryMain CTA — the most important action on the screen
secondarySupporting action — complements the primary
linkText action — low prominence, navigational
activeCurrently selected control (toggle, active tab)
disabledControl not available for interaction

Example: Button with all states

/* ── Primary Button ── */
.btn-primary {
background: var(--semantic-color-interface-function-primary-normal-background);
color: var(--semantic-color-interface-function-primary-normal-txt-on);
border: 1px solid var(--semantic-color-interface-function-primary-normal-border);
}

.btn-primary:hover,
.btn-primary:focus-visible {
background: var(--semantic-color-interface-function-primary-action-background);
color: var(--semantic-color-interface-function-primary-action-txt-on);
border: 1px solid var(--semantic-color-interface-function-primary-action-border);
}

.btn-primary:active,
.btn-primary[aria-pressed="true"] {
background: var(--semantic-color-interface-function-primary-active-background);
color: var(--semantic-color-interface-function-primary-active-txt-on);
border: 1px solid var(--semantic-color-interface-function-primary-active-border);
}

/* ── Disabled Button ── */
.btn-primary:disabled,
.btn-primary[aria-disabled="true"] {
background: var(--semantic-color-interface-function-disabled-normal-background);
color: var(--semantic-color-interface-function-disabled-normal-txt-on);
border: 1px solid var(--semantic-color-interface-function-disabled-normal-border);
cursor: not-allowed;
}

Example: Secondary Button

.btn-secondary {
background: var(--semantic-color-interface-function-secondary-normal-background);
color: var(--semantic-color-interface-function-secondary-normal-txt-on);
border: 1px solid var(--semantic-color-interface-function-secondary-normal-border);
}

.btn-secondary:hover {
background: var(--semantic-color-interface-function-secondary-action-background);
}

.btn-secondary:active {
background: var(--semantic-color-interface-function-secondary-active-background);
}

Feedback Variants — Alerts, Banners, Toasts

Feedback components follow the {type}.{variant}.{state} pattern:

semantic.color.interface.feedback.{type}.{variant}.{state}.{property}
SegmentValues
{type}info, success, warning, danger
{variant}default (soft, for backgrounds), secondary (saturated, for borders and icons)
{state}normal, action, active

Example: Alert with all types

/* Generic alert */
.alert {
padding: var(--semantic-dimension-spacing-small);
border-radius: var(--semantic-border-radii-small);
border-left: 4px solid;
}

/* Info */
.alert--info {
background: var(--semantic-color-interface-feedback-info-default-normal-background);
color: var(--semantic-color-interface-feedback-info-default-normal-txt-on);
border-color: var(--semantic-color-interface-feedback-info-secondary-normal-border);
}

/* Success */
.alert--success {
background: var(--semantic-color-interface-feedback-success-default-normal-background);
color: var(--semantic-color-interface-feedback-success-default-normal-txt-on);
border-color: var(--semantic-color-interface-feedback-success-secondary-normal-border);
}

/* Warning */
.alert--warning {
background: var(--semantic-color-interface-feedback-warning-default-normal-background);
color: var(--semantic-color-interface-feedback-warning-default-normal-txt-on);
border-color: var(--semantic-color-interface-feedback-warning-secondary-normal-border);
}

/* Danger */
.alert--danger {
background: var(--semantic-color-interface-feedback-danger-default-normal-background);
color: var(--semantic-color-interface-feedback-danger-default-normal-txt-on);
border-color: var(--semantic-color-interface-feedback-danger-secondary-normal-border);
}

Rule: always use default for the background and secondary for border and icon — this combination guarantees visual hierarchy and adequate contrast.


Input Variants — Validation States

Inputs have a validation state that overlaps with the interactive state:

/* Input — normal state */
.input {
background: var(--semantic-color-brand-ambient-contrast-deep-positive-background);
color: var(--semantic-color-text-body);
border: 1px solid var(--semantic-color-brand-ambient-neutral-mid-border);
border-radius: var(--semantic-border-radii-extra-small);
padding: var(--semantic-dimension-spacing-extra-small) var(--semantic-dimension-spacing-small);
}

/* Input — focus */
.input:focus-visible {
border-color: var(--semantic-color-interface-function-primary-action-border);
outline: none;
}

/* Input — error */
.input--error {
border-color: var(--semantic-color-interface-feedback-danger-secondary-normal-border);
}

/* Error message below the input */
.input__error-msg {
color: var(--semantic-color-text-danger_default);
font-size: var(--semantic-typography-font-sizes-extra-small);
}

/* Input — success */
.input--success {
border-color: var(--semantic-color-interface-feedback-success-secondary-normal-border);
}

/* Input — disabled */
.input:disabled {
background: var(--semantic-color-interface-function-disabled-normal-background);
color: var(--semantic-color-interface-function-disabled-normal-txt-on);
border-color: var(--semantic-color-interface-function-disabled-normal-border);
cursor: not-allowed;
}

Intensity Variants — Brand Components

For components that express brand identity (badges, chips, brand tags), use the intensity levels from brand.branding:

semantic.color.brand.branding.{role}.{intensity}.{property}
IntensityVisualTypical use
lowestVery subtleChip background, tag in dense context
lowSoftSecondary badge background
defaultBrand standardCTA, hero button, main element
highSaturated/darkEmphasis text on light background
highestMaximumRare — very high prominence element
/* Brand chip — soft */
.chip--brand {
background: var(--semantic-color-brand-branding-first-lowest-background);
color: var(--semantic-color-brand-branding-first-lowest-txt-on);
border: 1px solid var(--semantic-color-brand-branding-first-low-border);
}

/* Brand badge — highlighted */
.badge--brand {
background: var(--semantic-color-brand-branding-first-default-background);
color: var(--semantic-color-brand-branding-first-default-txt-on);
}

Size Variants — Dimension Scale

Size variants (small, medium, large) of a component map to the semantic dimension scale. The most common convention:

Component sizeInternal spacingHeight sizing
xsmicroextraSmall
smextraSmallsmall
mdsmallmedium
lgmediumlarge
xllargeextraLarge
/* Medium button (default) */
.btn--md {
padding: var(--semantic-dimension-spacing-extra-small) var(--semantic-dimension-spacing-small);
height: var(--semantic-dimension-sizing-large);
font-size: var(--semantic-typography-font-sizes-small);
border-radius: var(--semantic-border-radii-small);
}

/* Small button */
.btn--sm {
padding: var(--semantic-dimension-spacing-micro) var(--semantic-dimension-spacing-extra-small);
height: var(--semantic-dimension-sizing-medium);
font-size: var(--semantic-typography-font-sizes-extra-small);
border-radius: var(--semantic-border-radii-extra-small);
}

/* Large button */
.btn--lg {
padding: var(--semantic-dimension-spacing-small) var(--semantic-dimension-spacing-medium);
height: var(--semantic-dimension-sizing-extra-large);
font-size: var(--semantic-typography-font-sizes-medium);
border-radius: var(--semantic-border-radii-medium);
}

The Disabled State — Two Patterns

There are two valid ways to implement the disabled state; the choice depends on the component type.

Pattern 1 — Disabled token (preferred for buttons and controls)

Use the interface.function.disabled tokens — they guarantee correct contrast over the current background:

.btn:disabled {
background: var(--semantic-color-interface-function-disabled-normal-background);
color: var(--semantic-color-interface-function-disabled-normal-txt-on);
border-color: var(--semantic-color-interface-function-disabled-normal-border);
}

Pattern 2 — Opacity (for content and entire regions)

When an entire section is disabled and it does not make sense to swap each individual token:

.section--disabled {
opacity: calc(var(--semantic-opacity-raw-translucid) / 100); /* 50% */
pointer-events: none;
}

Caution: Opacity alone does not guarantee sufficient contrast over all backgrounds. Use carefully on small text. For individual controls, always prefer Pattern 1.


Focus Ring — Accessibility by Token

The focus ring is a mandatory accessibility element (WCAG 2.4.7). Use the action variant border of the corresponding role:

/* Generic focus ring */
:focus-visible {
outline: 2px solid var(--semantic-color-interface-function-primary-action-border);
outline-offset: 2px;
}

/* Focus on feedback element (e.g., clickable alert) */
.alert:focus-visible {
outline-color: var(--semantic-color-interface-feedback-info-secondary-normal-border);
}

Product Variants — Badges and Seals

Product signaling components (promo badges, cashback seals, tier indicators) use semantic.color.product.*:

/* Promotion badge */
.badge--promo {
background: var(--semantic-color-product-promo-default-default-background);
color: var(--semantic-color-product-promo-default-default-txt-on);
border: 1px solid var(--semantic-color-product-promo-default-default-border);
}

/* Subtle promo variant (lower prominence) */
.badge--promo-subtle {
background: var(--semantic-color-product-promo-default-lowest-background);
color: var(--semantic-color-product-promo-default-lowest-txt-on);
}

/* Cashback badge */
.badge--cashback {
background: var(--semantic-color-product-cashback-default-default-background);
color: var(--semantic-color-product-cashback-default-default-txt-on);
}

Reference Component — Complete Map (Button)

The table below shows the complete token mapping for a Button component with 3 variants and 4 states:

VariantStateBackgroundTextBorder
primarynormalfunction.primary.normal.backgroundfunction.primary.normal.txtOnfunction.primary.normal.border
primaryhoverfunction.primary.action.backgroundfunction.primary.action.txtOnfunction.primary.action.border
primarypressedfunction.primary.active.backgroundfunction.primary.active.txtOnfunction.primary.active.border
primarydisabledfunction.disabled.normal.backgroundfunction.disabled.normal.txtOnfunction.disabled.normal.border
secondarynormalfunction.secondary.normal.backgroundfunction.secondary.normal.txtOnfunction.secondary.normal.border
secondaryhoverfunction.secondary.action.backgroundfunction.secondary.action.txtOnfunction.secondary.action.border
secondarypressedfunction.secondary.active.backgroundfunction.secondary.active.txtOnfunction.secondary.active.border
secondarydisabledfunction.disabled.normal.backgroundfunction.disabled.normal.txtOnfunction.disabled.normal.border
linknormaltransparentfunction.link.normal.txtOnnone
linkhoverfunction.link.action.backgroundfunction.link.action.txtOnnone
linkpressedfunction.link.active.backgroundfunction.link.active.txtOnnone
linkdisabledfunction.disabled.normal.backgroundfunction.disabled.normal.txtOnfunction.disabled.normal.border

All paths above are prefixed with semantic.color.interface..


References