Data Attributes
Using data attributes for declarative styling and component identification.
Data attributes provide a powerful way to expose component state and structure to consumers, enabling flexible styling without prop explosion. Modern component libraries use two primary patterns: data-state
for visual states and data-slot
for component identification.
Styling state with data-state
One of the most common anti-patterns in component styling is exposing separate className props for different states.
In less modern components, you'll often see APIs like this:
<Dialog
openClassName="bg-black"
closedClassName="bg-white"
classes={{
open: "opacity-100",
closed: "opacity-0"
}}
/>
This approach has several problems:
- It couples the component's internal state to its styling API
- It creates an explosion of props as components grow more complex
- It makes the component harder to use and maintain
- It prevents styling based on state combinations
The solution: data-state attributes
Instead, use data-*
attributes to expose component state declaratively. This allows consumers to style components based on state using standard CSS selectors:
const Dialog = ({ className, ...props }: DialogProps) => {
const [isOpen, setIsOpen] = useState(false);
return (
<div
data-state={isOpen ? 'open' : 'closed'}
className={cn('transition-all', className)}
{...props}
/>
);
};
Now consumers can style the component based on state from the outside:
<Dialog className="data-[state=open]:opacity-100 data-[state=closed]:opacity-0" />
Benefits of this approach
- Single className prop - No need for multiple state-specific className props
- Composable - Combine multiple data attributes for complex states
- Standard CSS - Works with any CSS-in-JS solution or plain CSS
- Type-safe - TypeScript can infer data attribute values
- Inspectable - States are visible in DevTools as HTML attributes
Common state patterns
Use data attributes for all kinds of component state:
// Open/closed state
<Accordion data-state={isOpen ? 'open' : 'closed'} />
// Selected state
<Tab data-state={isSelected ? 'active' : 'inactive'} />
// Disabled state (in addition to disabled attribute)
<Button data-disabled={isDisabled} disabled={isDisabled} />
// Loading state
<Button data-loading={isLoading} />
// Orientation
<Slider data-orientation="horizontal" />
// Side/position
<Tooltip data-side="top" />
Styling with Tailwind
Tailwind supports arbitrary variants, making data attribute styling elegant:
<Dialog
className={cn(
// Base styles
'rounded-lg border p-4',
// State-based styles
'data-[state=open]:animate-in data-[state=open]:fade-in',
'data-[state=closed]:animate-out data-[state=closed]:fade-out',
// Multiple attributes
'data-[state=open][data-side=top]:slide-in-from-top-2'
)}
/>
For commonly-used states, you can extend Tailwind's configuration:
module.exports = {
theme: {
extend: {
data: {
open: 'state="open"',
closed: 'state="closed"',
active: 'state="active"',
}
}
}
}
Now you can use shorthand:
<Dialog className="data-open:opacity-100 data-closed:opacity-0" />
Integration with Radix UI
This pattern is used extensively by Radix UI, which automatically applies data attributes to its primitives:
import * as Dialog from '@radix-ui/react-dialog';
<Dialog.Root>
<Dialog.Trigger />
<Dialog.Portal>
{/* Radix automatically adds data-state="open" | "closed" */}
<Dialog.Overlay className="data-[state=open]:animate-in data-[state=closed]:animate-out" />
<Dialog.Content className="data-[state=open]:fade-in data-[state=closed]:fade-out" />
</Dialog.Portal>
</Dialog.Root>
Other data attributes Radix provides include:
data-state
- open/closed, active/inactive, on/offdata-side
- top/right/bottom/left (for positioned elements)data-align
- start/center/end (for positioned elements)data-orientation
- horizontal/verticaldata-disabled
- present when disableddata-placeholder
- present when showing placeholder
Component identification with data-slot
While data-state
tracks visual states, data-slot
identifies component types within a composition. This pattern, popularized by shadcn/ui, allows parent components to target and style specific child components without relying on fragile class names or element selectors.
The problem with child targeting
Traditional approaches to styling child components have significant limitations:
// Relies on element types - breaks if implementation changes
<form className="[&_input]:rounded-lg [&_button]:mt-4" />
// Relies on class names - breaks if classes change
<form className="[&_.text-input]:rounded-lg" />
// Requires passing classes through props - verbose
<form>
<input className={inputClasses} />
<button className={buttonClasses} />
</form>
The solution: data-slot attributes
Use data-slot
to give components stable identifiers that can be targeted by parents:
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
return (
<fieldset
data-slot="field-set"
className={cn(
"flex flex-col gap-6",
// Target specific child slots
"has-[>[data-slot=checkbox-group]]:gap-3",
"has-[>[data-slot=radio-group]]:gap-3",
className
)}
{...props}
/>
);
}
function CheckboxGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="checkbox-group"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
);
}
Benefits of data-slot
- Stable identifiers - Won't break when implementation details change
- Semantic targeting - Target based on component purpose, not structure
- Encapsulation - Internal classes remain private
- Composable - Works with arbitrary nesting and composition
- Type-safe - Can be validated and documented
Using has-[]
for parent-aware styling
Tailwind's has-[]
selector combined with data-slot
creates powerful parent-aware styling:
function Form({ className, ...props }: React.ComponentProps<"form">) {
return (
<form
data-slot="form"
className={cn(
"space-y-4",
// Adjust spacing when specific slots are present
"has-[>[data-slot=form-section]]:space-y-6",
"has-[>[data-slot=inline-fields]]:space-y-2",
// Style based on slot states
"has-[[data-slot=submit-button][data-loading=true]]:opacity-50",
className
)}
{...props}
/>
);
}
Using [&_]
for descendant targeting
For deeper nesting, use the [&_selector]
pattern to target any descendant:
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"rounded-lg border p-4",
// Target any descendant with data-slot
"[&_[data-slot=card-header]]:mb-4",
"[&_[data-slot=card-title]]:text-lg [&_[data-slot=card-title]]:font-semibold",
"[&_[data-slot=card-description]]:text-sm [&_[data-slot=card-description]]:text-muted-foreground",
"[&_[data-slot=card-footer]]:mt-4 [&_[data-slot=card-footer]]:border-t [&_[data-slot=card-footer]]:pt-4",
className
)}
{...props}
/>
);
}
Global CSS with data-slot
Data slots work beautifully with global CSS for theme-wide consistency:
/* Style all buttons within forms */
[data-slot="form"] [data-slot="button"] {
@apply w-full sm:w-auto;
}
/* Style submit buttons specifically */
[data-slot="form"] [data-slot="submit-button"] {
@apply bg-primary text-primary-foreground;
}
/* Adjust inputs within inline layouts */
[data-slot="inline-fields"] [data-slot="input"] {
@apply flex-1;
}
/* Style based on state combinations */
[data-slot="dialog"][data-state="open"] [data-slot="dialog-content"] {
@apply animate-in fade-in;
}
Naming conventions
Follow these conventions for consistent data-slot
naming:
- Use kebab-case -
data-slot="form-field"
notdata-slot="formField"
- Be specific -
data-slot="submit-button"
notdata-slot="button"
- Match component purpose - Name reflects what it does, not how it looks
- Avoid implementation details -
data-slot="user-avatar"
notdata-slot="rounded-image"
// Good examples
data-slot="search-input"
data-slot="navigation-menu"
data-slot="error-message"
data-slot="submit-button"
data-slot="card-header"
// Avoid
data-slot="input" // Too generic
data-slot="blueButton" // Includes styling
data-slot="div-wrapper" // Implementation detail
data-slot="mainContent" // Use camelCase
When to use data attributes vs props
Understanding when to use each pattern is key to a clean API:
data-state
use cases
- Visual states - open/closed, active/inactive, loading, etc.
- Layout states - orientation, side, alignment
- Interaction states - hover, focus, disabled (when you need to style children)
data-slot
use cases
- Component identification - Stable identifiers for targeting
- Composition patterns - Parent-child relationships
- Global styling - Theme-wide component styling
- Variant-independent targeting - Target any variant of a component
props
use cases
- Variants - Different visual designs (primary, secondary, destructive)
- Sizes - sm, md, lg
- Behavioral configuration - controlled/uncontrolled, default values
- Event handlers - onClick, onChange, etc.
Combined approach
A well-designed component uses all three patterns appropriately:
type ButtonProps = {
variant?: 'primary' | 'secondary' | 'destructive';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
disabled?: boolean;
onClick?: () => void;
className?: string;
};
const Button = ({
variant = 'primary',
size = 'md',
loading,
disabled,
className,
...props
}: ButtonProps) => {
return (
<button
// Slot for targeting
data-slot="button"
// State for conditional styling
data-loading={loading}
data-disabled={disabled}
className={cn(
// Variant styles via props
buttonVariants({ variant, size }),
// Additional state styling allowed via className
className
)}
disabled={disabled}
{...props}
/>
);
};
Now the button can be used and styled in multiple ways:
// Basic usage with variants
<Button variant="primary" size="lg">Submit</Button>
// Parent targeting via data-slot
<form className="[&_[data-slot=button]]:w-full">
<Button>Submit</Button>
</form>
// State-based styling via data-state
<Button
loading={isLoading}
className="data-[loading=true]:opacity-50"
>
Submit
</Button>
// Global CSS can target any button
// [data-slot="button"][data-loading="true"] { ... }
Data attributes provide a robust foundation for styling modern component libraries. By using data-state
for visual states and data-slot
for component identification, you create a flexible, maintainable API that scales from simple components to complex design systems.