Modifiers

Controls combine a base class or element, such as .btn or aside, with names that adjust their appearance. Each card below shows one name on every element that accepts it, labelled with the selector that produced it. The rules for choosing a class or an attribute follow the cards.

Variants
Modifiers
Attributes

Variant or modifier

Every name is on one of two axes, and each component page says which.

variant
Mutually exclusive — at most one. A variant defines the style, such as a primary, secondary, or destructive button. Applying two variants produces no error, but both CSS rules match and the later rule takes precedence.
modifier
Stackable — any number, alongside any variant. A modifier adjusts one property, such as size, width, border, or animation.

Use these terms consistently across component documentation. For example, a status pill's different fills are its variants. Both the button and the pill below take the destructive variant, each with a modifier.

destructive · pulse
Show code

Variants

At most one per element. An element without a variant class shows its default style.

outline

variant
.btn.outline
Badge
.badge.outline

A transparent fill with a border.

Taken by .btn and .badge.

destructive

variant
.btn.destructive
Badge
.badge.destructive

A red fill, --red-9 with white text in both light and dark mode. On a control it warns what activating it does; on a status pill it reports what happened.

Taken by .btn and .badge.

ghost

variant
.btn.ghost

Transparent with muted text — the lowest emphasis. It gains a fill under the pointer.

Taken by .btn.

secondary

variant
Badge
.badge.secondary

A muted surface fill, for a status that needs no attention.

Taken by .badge.

brand

variant
Badge
.badge.brand

The brand fill, which is also the badge's default. Name it when the other badges nearby have variants, so the markup states the choice.

Taken by .badge.

warning

variant
Badge
.badge.warning
Above the list price. Check that this is intended.
.field-help.warning

An allowed but unusual state that needs attention: an orange fill on a badge, and warning-coloured text on a field's help line.

Taken by .badge and .field-help.

info

variant
250 including 25% VAT.
.field-help.info

A result worked out from the field's current value, in the info colour.

Taken by .field-help.

cancelled

variant
Badge
.badge.cancelled

A muted grey fill, for a cancelled or inactive status.

Taken by .badge.

Modifiers

Any number per element, alongside any variant.

sm

modifier
.btn.sm

Compact: a smaller font size and less padding.

Taken by .btn.

full-width

modifier
.btn.full-width

Stretches the button across its container and centres the label, for a form's only submit button.

Taken by .btn.

bordered

modifier
Saved as you type.
.callout.bordered

Adds an outline in the same tint and pushes the actions to the far edge.

Taken by .callout.

pulse

modifier
Badge
.badge.pulse

A 2-second opacity fade for something happening right now. It stops under prefers-reduced-motion.

Taken by .badge.

Appearance set by attribute

An element that the library selects by its tag or role takes its appearance from a data-* value instead of a class. The attribute holds one value, so these are variants by construction.

data-look

variant
fieldset[data-look="segmented"]

Draws a radio group as one control divided into segments.

Taken by a radio group.

data-state

variant
.input-group[data-state="flux"]
.input-group[data-state="updating"]
.input-group[data-state="locked"]

Sets the frame of a field whose value is changing, being saved, or locked. The control still carries its own state attribute for a screen reader.

Taken by .input-group.

data-side

variant

aside[data-side="start|end"] picks the edge a collapsible panel opens from, and [data-handle="tab|rail|float|grip"] picks the shape of its handle. Both need a page layout to show, so they are demonstrated on their own page.

Taken by the side panel.

By element

Each component page shows its element with every variant and modifier it takes.

ElementVariants (at most one)Modifiers (any number)
.btn outline, ghost, destructive sm, full-width, plus the [aria-pressed] state
.btn-icon none none — icon buttons use their own class (Button § Modifiers)
.badge secondary, outline, brand, destructive, warning, cancelled pulse
.callout none — the tint is --_tint, set by the caller bordered
.field-help info, warning none
.option-card none — the tint is --_tint, and selection is the [aria-pressed] / :checked state none. On a radio card, [radio-option] specifies the structure; the card class supplies the appearance

Shared and element-specific names

outline, destructive and warning apply to multiple elements. Other names are currently specific to one element because their meanings depend on its role. For example, a button has no cancelled variant, and a non-interactive badge has no full-width modifier. Each component page documents which names it supports and any relevant limitations.

Class, attribute, or neither

Choose the mechanism according to what it represents. Use the same approach across elements so a new component does not introduce another way to express an existing concept.

KindMechanismIn this layer
State — changes while the page is open, and a user has to be able to perceive it the corresponding ARIA or native attribute [aria-pressed], [aria-selected], [aria-expanded], [aria-disabled], [aria-busy], :disabled, [open], :checked, :user-invalid
Appearance, on an element the layer already names with a class a class, on one of the two axes in Variant or modifier .btn.outline, .badge.secondary, .btn.sm, .callout.bordered
Appearance, on an element named by its tag or its role a data-* attribute with a descriptive value aside[data-side="start|end"], [data-handle="tab|rail|float|grip"], [data-look="segmented"], .input-group[data-state="locked|flux|updating"]
Composition — the element sits inside another that takes over part of its job a bare marker attribute [raw], [chips], [multiple], [alertdialog], [panel-toggle], [dialog-dismiss], [radio-option]
An open-ended axis — a colour, size or radius the caller picks, with no closed set of names a --_ private property the caller overrides --_tint on .option-card and .callout, --segmented-*, --switch-*

Expose state through standard attributes

Use native or ARIA attributes to expose state to assistive technology and to select its styles. WCAG 2.2 SC 4.1.2 Name, Role, Value requires that “states, properties, and values that can be set by the user can be programmatically determined”. CSS classes do not appear in the accessibility tree. Duplicating state in a class and an attribute also risks updating one without the other, leaving the visual and accessible states inconsistent. A toggle button therefore uses aria-pressed, per the APG button pattern, and ui.css uses that attribute to select its styles.

Show code /* Good — the attribute assistive technology reads is the styling hook */ .btn { &[aria-pressed="true"] { background: var(--surface-3); } } /* Not this — .pressed is invisible to a screen reader, and now two things have to be kept in step */ .btn.pressed { background: var(--surface-3); }

Prefer the native attribute where one exists — disabled, open, :checked, :user-invalid — per the first rule of ARIA use: a native element or attribute with the required semantics avoids recreating those semantics manually. aria-disabled over disabled is a deliberate exception for controls that must remain focusable so users can find out why they are unavailable (Button § States).

Use data-* for mutually exclusive variants

Prefer selectors based on the element and its existing attributes. For example, an <aside> can be selected by its tag, so its opening side is expressed with data-side and the collapsible panel needs no styling class (Side panel). An attribute can have only one value, so an element cannot be data-side="start" and data-side="end" at once. In contrast, class="start end" applies both classes and lets CSS source order decide the result.

Why composition uses an attribute without a value

A composition marker describes how nested elements divide responsibility. [raw] on a field makes the enclosing .input-group responsible for its border, background, and focus ring; [chips] moves the field frame onto the host; [panel-toggle] identifies the panel's handle button. These markers describe structure and behavior, so they are separate from appearance variants and modifiers.

Place the marker on the element whose styles change. The three markers above go on the inner element. [radio-option] goes on the containing element. It makes the <label> responsible for the whole option's hit area: the label receives position, and the radio expands across that area while remaining visually hidden. Appearance is configured separately (.option-card, data-look="segmented", or an application-specific class).

An attribute suits this purpose for three reasons:

Before adding a name

Work through this list before adding a name.

  1. Is it state? Use the corresponding ARIA or native attribute.
  2. Is it composition — does it assign responsibility to a surrounding element? Use an attribute without a value.
  3. Is the axis open-ended — a colour or size the caller chooses, with no closed set to name? Then it is a --_ private prop (Tokens § Private props).
  4. Does a listed name already mean the appearance you want? Reuse it so authors can apply what they learned from other components.
  5. Add a new name only if needed — use a class when the base element already has a styling class, or a data-* value otherwise. On the component’s documentation page, state whether the name is a mutually exclusive variant or a modifier that can be combined with others.

See also