outline
variantA transparent fill with a border.
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.
Every name is on one of two axes, and each component page says which.
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.
At most one per element. An element without a variant class shows its default style.
A transparent fill with a border.
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.
Transparent with muted text — the lowest emphasis. It gains a fill under the pointer.
Taken by .btn.
A muted surface fill, for a status that needs no attention.
Taken by .badge.
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.
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.
A result worked out from the field's current value, in the info colour.
Taken by .field-help.
A muted grey fill, for a cancelled or inactive status.
Taken by .badge.
Any number per element, alongside any variant.
Compact: a smaller font size and less padding.
Taken by .btn.
Stretches the button across its container and centres the label, for a form's only submit button.
Taken by .btn.
Adds an outline in the same tint and pushes the actions to the far edge.
Taken by .callout.
A 2-second opacity fade for something happening right now. It stops under prefers-reduced-motion.
Taken by .badge.
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.
Draws a radio group as one control divided into segments.
Taken by a radio group.
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.
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.
Each component page shows its element with every variant and modifier it takes.
| Element | Variants (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 |
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.
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.
| Kind | Mechanism | In 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-* |
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.
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).
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.
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:
hasAttribute) and the enclosing element's selectors read it
(.input-group > x-select[raw] > button). This makes the required configuration explicit.
> button) can unintentionally match another child added later.
Work through this list before adding a name.
--_ private prop
(Tokens § Private props).
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.
.btn — Button — the variants and modifiers in use, with demos..badge — Badge — the status variants and .pulse..callout — Callout — the caller-set --_tint, .bordered, and the action cluster.--_ pattern and the published-hook indirection.