Theme
On this page (8)

§Props → Data Attributes

If you came from a component framework, this mapping should feel familiar.

<Button
  variant="destructive"
  size="lg"
>
  Delete
</Button>
<button class="btn"
  data-variant="destructive"
  data-size="lg"
>
  Delete
</button>

§How it works

Every component has exactly two layers in its markup: a class and its data attributes.

classIdentifies what the element is - .btn, .badge, .card, .dialog. One class per component. This is the base selector in CSS.data-variantSelects the visual style - default, outline, ghost, destructive, etc. Think of it as the component's "skin."data-sizeSelects the size - sm, lg, icon, etc. Controls height, padding, and font-size.data-*Additional axes like data-orientation, data-side, data-position - each controls one independent dimension. Compose freely.

The CSS combines these with attribute selectors: .btn[data-variant="outline"]. No modifier classes, no BEM suffixes, no utility chains.

§Why data attributes instead of modifier classes?

Traditional systems like Bootstrap and BEM use modifier classes: .btn-primary, .btn--lg, .btn-outline-danger. Here's why we don't.

<button class="btn btn-destructive btn-lg">
  Delete
</button>
<style>
  /* Conflicting specificity */
  .btn-primary { ... }
  .btn-lg { ... }
  .btn-primary.btn-lg { ... }
</style>
<button class="btn"
  data-variant="destructive"
  data-size="lg">
  Delete
</button>
<style>
  /* Flat specificity - no combos needed */
  .btn[data-variant="destructive"] { ... }
  .btn[data-size="lg"] { ... }
</style>
Flat specificityAll variant/size selectors have equal weight. No specificity wars, no !important.Independent axesVariant and size combine freely without needing combinatorial class names like .btn-outline-lg.Uniform APIEvery component follows the same pattern. Learn it once, apply everywhere. Easy for AI to generate.Clean class attributeThe class attribute stays a single token - class="btn" - not a long chain of modifiers.

§Styling attributes

These are set in your HTML and read by CSS. They control the visual appearance of components.

data-variant

The most common attribute. Controls the visual style of a component.

data-variant values by component
ComponentValues
.btndefault · secondary · outline · ghost · destructive · link
.badgedefault · secondary · outline
.tagdefault · secondary · outline
.alertdefault · destructive
.toggledefault · outline
.toggle-groupdefault · outline
.toastdefault · destructive · success · warning · info
.linkdefault · muted
.tab-listdefault · line
.listdefault · none · icon
.dropdown-itemdefault · destructive

data-size

Controls height, padding, and sometimes font-size.

data-size values by component
ComponentValues
.btnxs · sm · default · lg · icon · icon-sm · icon-lg
.inputsm · default · lg
.selectsm · default · lg
.dialogsm · default · lg · xl · full
.togglesm · default · lg
.toggle-groupsm · default · lg
.avatarsm · default · lg
.spinnersm · default · lg
.switchsm · default

Other styling attributes

Some components have additional axes beyond variant and size.

additional styling data attributes
AttributeComponentValuesPurpose
data-orientation.btn-group · .sep · .toolbarhorizontal · verticalLayout direction
data-side.sheet (dialog)left · right · top · bottomSlide-in direction
data-position.toast-containertop-left · top-right · top-center · bottom-left · bottom-right · bottom-centerViewport placement
data-ratio.image1/1 · 4/3 · 16/9 · 21/9Aspect ratio
data-layout.descriptionshorizontal · verticalTerm/definition layout
data-trend.stat-trendup · downTrend indicator color
data-spacing.toggle-group(boolean)Adds gap between toggles

§Behavioral attributes

These wire up JavaScript interactions. They follow the pattern data-{component}-trigger and data-{component}-close - connecting a button to the element it controls.

<!-- The trigger: its value is the ID of the target -->
<button class="btn" data-dialog-trigger="confirm">
  Open Dialog
</button>
<!-- The target: matched by ID -->
<dialog id="confirm" class="dialog">
  <div class="dialog-content">
    <p>Are you sure?</p>
    <!-- The close: placed inside, closes the nearest parent -->
    <button class="btn" data-dialog-close>Cancel</button>
  </div>
</dialog>
all behavioral data attributes
AttributeComponentPurpose
data-dialog-triggerDialogOpens the <dialog> matching the attribute value
data-dialog-closeDialogCloses the parent dialog
data-alert-dialog-triggerAlert DialogOpens an alert dialog by ID
data-alert-dialog-closeAlert DialogCloses the parent alert dialog
data-sheet-triggerSheetOpens a sheet (drawer) by ID
data-sheet-closeSheetCloses the parent sheet
data-dropdown-triggerDropdownToggles a popover dropdown menu
data-actionNumber Inputincrement / decrement buttons
data-typeAccordion · Toggle Groupsingle or multiple selection mode
data-collapsibleAccordionAllows all panels to be closed

§State attributes

These are set and removed by JavaScript at runtime to represent UI state. CSS reads them to apply state-specific styles. You don't write these in markup - the JS manages them.

JS-managed state attributes
AttributeComponentPurpose
data-highlightedDropdown · ComboboxMarks the currently keyboard-highlighted item
data-errorAvatarSet when image fails to load, triggers fallback display
data-placeholderComboboxPresent when no selection has been made

§ARIA attributes as styling hooks

Where the browser natively provides state APIs - like aria-selected, aria-expanded, or disabled - we style against those directly. No data attributes are needed because the browser already exposes the state.

/* Tab is selected - use ARIA, not a data attribute */
.tab-trigger[aria-selected="true"] {
  color: var(--foreground);
  border-color: var(--primary);
}
/* Toggle is pressed - native ARIA attribute */
.toggle[aria-pressed="true"] {
  background: var(--accent);
}
/* Button is disabled - native HTML attribute */
.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

Rule of thumb: if the browser has a native attribute for a state (disabled, aria-selected, aria-expanded, open), we style against that. Data attributes are only used when no native equivalent exists.

§The pattern at a glance

Every component follows this structure. Once you learn one, you know them all.

<element
  class="{component}"             ← what it is
  data-variant="{variant}"        ← visual style
  data-size="{size}"              ← dimensions
  data-{axis}="{value}"           ← other config
  data-{component}-trigger="{id}" ← JS wiring
  aria-*/disabled/open            ← native state
>
  content
</element>
button
<button class="btn"
  data-variant="outline"
  data-size="sm">
  Edit
</button>
badge
<span class="badge"
  data-variant="secondary">
  New
</span>
sheet
<dialog class="sheet"
  data-side="left"
  id="nav">
  ...
</dialog>

Each component's component-skill.md lists the complete set of supported data attributes, variants, sizes, and ARIA requirements.

Comments, ideas or improvements? Edit this page's source on GitHub