In React or Solid, you configure components with props. In shadcn-html, data attributes are your props.
Every component uses a single CSS class to declare what it is, and data-* attributes to declare which version — variant, size, orientation, and behavior.
Props → Data Attributes
If you came from a component framework, this mapping should feel familiar.
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.
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.
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
Attribute
Component
Purpose
data-dialog-trigger
Dialog
Opens the <dialog> matching the attribute value
data-dialog-close
Dialog
Closes the parent dialog
data-alert-dialog-trigger
Alert Dialog
Opens an alert dialog by ID
data-alert-dialog-close
Alert Dialog
Closes the parent alert dialog
data-sheet-trigger
Sheet
Opens a sheet (drawer) by ID
data-sheet-close
Sheet
Closes the parent sheet
data-dropdown-trigger
Dropdown
Toggles a popover dropdown menu
data-action
Number Input
increment / decrement buttons
data-type
Accordion · Toggle Group
single or multiple selection mode
data-collapsible
Accordion
Allows 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
Attribute
Component
Purpose
data-highlighted
Dropdown · Combobox
Marks the currently keyboard-highlighted item
data-error
Avatar
Set when image fails to load, triggers fallback display
data-placeholder
Combobox
Present 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>