Props → Data Attributes

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

React / Solid (props)
<Button
  variant="destructive"
  size="lg"
>
  Delete
</Button>
shadcn-html (data attributes)
<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.

class Identifies what the element is — .btn, .badge, .card, .dialog. One class per component. This is the base selector in CSS. data-variant Selects the visual style — default, outline, ghost, destructive, etc. Think of it as the component's "skin." data-size Selects 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.

Modifier classes
<button class="btn btn-destructive btn-lg">
  Delete
</button>

/* Conflicting specificity */
.btn-primary { ... }
.btn-lg { ... }
.btn-primary.btn-lg { ... }
Data attributes
<button class="btn"
  data-variant="destructive"
  data-size="lg">
  Delete
</button>

/* Flat specificity */
.btn[data-variant="destructive"] { ... }
.btn[data-size="lg"] { ... }
/* no combo needed */
Flat specificity All variant/size selectors have equal weight. No specificity wars, no !important. Independent axes Variant and size combine freely without needing combinatorial class names like .btn-outline-lg. Uniform API Every component follows the same pattern. Learn it once, apply everywhere. Easy for AI to generate. Clean class attribute The 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.

trigger / close pattern
<!-- 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.

native state as CSS selectors
/* 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;
  pointer-events: none;
}

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.

the universal component pattern
<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.