Attributs data
Utilisation des attributs data pour un style déclaratif et l'identification des composants.
Les attributs data offrent un moyen puissant d'exposer l'état et la structure des composants aux consommateurs, permettant un stylage flexible sans explosion de props. Les bibliothèques de composants modernes utilisent deux principaux modèles : data-state pour les états visuels et data-slot pour l'identification des composants.
Styliser l'état avec data-state
Un des anti-patterns les plus courants dans le stylage des composants est d'exposer des props className distinctes pour différents états.
Dans des composants moins modernes, vous verrez souvent des API comme ceci :
<Dialog
openClassName="bg-black"
closedClassName="bg-white"
classes={{
open: "opacity-100",
closed: "opacity-0"
}}
/>Cette approche présente plusieurs problèmes :
- Elle couple l'état interne du composant à son API de stylage
- Elle crée une explosion de props à mesure que les composants deviennent plus complexes
- Elle rend le composant plus difficile à utiliser et à maintenir
- Elle empêche le stylage basé sur des combinaisons d'états
La solution : les attributs data-state
À la place, utilisez des attributs data-* pour exposer l'état du composant de manière déclarative. Cela permet aux consommateurs de styler les composants en fonction de l'état en utilisant des sélecteurs CSS standard :
const Dialog = ({ className, ...props }: DialogProps) => {
const [isOpen, setIsOpen] = useState(false);
return (
<div
data-state={isOpen ? 'open' : 'closed'}
className={cn('transition-all', className)}
{...props}
/>
);
};Désormais, les consommateurs peuvent styliser le composant depuis l'extérieur en fonction de l'état :
<Dialog className="data-[state=open]:opacity-100 data-[state=closed]:opacity-0" />Avantages de cette approche
- Single className prop - Pas besoin de multiples props className spécifiques à l'état
- Composable - Combinez plusieurs attributs data pour des états complexes
- Standard CSS - Fonctionne avec n'importe quelle solution CSS-in-JS ou avec du CSS pur
- Sécurisé par le typage - TypeScript peut inférer les valeurs des attributs data
- Inspectable - Les états sont visibles dans DevTools en tant qu'attributs HTML
Modèles d'état courants
Utilisez des attributs data pour toutes sortes d'états de composants :
// 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" />Styliser avec Tailwind
Tailwind prend en charge les variantes arbitraires, rendant le stylage via attributs data élégant :
<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'
)}
/>Pour les états couramment utilisés, vous pouvez étendre la configuration de Tailwind :
module.exports = {
theme: {
extend: {
data: {
open: 'state="open"',
closed: 'state="closed"',
active: 'state="active"',
}
}
}
}Vous pouvez alors utiliser la forme abrégée :
<Dialog className="data-open:opacity-100 data-closed:opacity-0" />Intégration avec Radix UI
Ce modèle est largement utilisé par Radix UI, qui applique automatiquement des attributs data à ses 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>Autres attributs data fournis par Radix :
data-state- open/closed, active/inactive, on/offdata-side- top/right/bottom/left (pour les éléments positionnés)data-align- start/center/end (pour les éléments positionnés)data-orientation- horizontal/verticaldata-disabled- présent lorsque désactivédata-placeholder- présent lors de l'affichage d'un placeholder
Identification des composants avec data-slot
Tandis que data-state suit les états visuels, data-slot identifie les types de composants au sein d'une composition. Ce modèle, popularisé par shadcn/ui, permet aux composants parents de cibler et styliser des composants enfants spécifiques sans s'appuyer sur des noms de classes fragiles ou des sélecteurs d'éléments.
Le problème du ciblage des enfants
Les approches traditionnelles pour styliser les composants enfants ont des limitations significatives :
// 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>La solution : les attributs data-slot
Utilisez data-slot pour donner aux composants des identifiants stables pouvant être ciblés par les 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}
/>
);
}Avantages de data-slot
- Identifiants stables - Ne casse pas lorsque les détails d'implémentation changent
- Ciblage sémantique - Ciblez en fonction du rôle du composant, pas de sa structure
- Encapsulation - Les classes internes restent privées
- Composable - Fonctionne avec n'importe quel niveau d'imbrication et de composition
- Sécurisé par le typage - Peut être validé et documenté
Utiliser has-[] pour un stylage conscient du parent
Le sélecteur has-[] de Tailwind combiné à data-slot crée un stylage puissant qui tient compte des parents :
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}
/>
);
}Utiliser [&_] pour le ciblage des descendants
Pour des imbrications plus profondes, utilisez le modèle [&_selector] pour cibler n'importe quel 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}
/>
);
}CSS global avec data-slot
Les data slots fonctionnent parfaitement avec le CSS global pour une cohérence à l'échelle du thème :
/* 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;
}Conventions de nommage
Suivez ces conventions pour des noms data-slot cohérents :
- Use kebab-case -
data-slot="form-field"notdata-slot="formField" - Be specific -
data-slot="submit-button"notdata-slot="button" - Match component purpose - Le nom reflète ce que fait le composant, pas son apparence
- 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 camelCaseQuand utiliser les attributs data vs props
Comprendre quand utiliser chaque modèle est la clé d'une API propre :
Cas d'utilisation de data-state
- Visual states - open/closed, active/inactive, loading, etc.
- Layout states - orientation, side, alignment
- Interaction states - hover, focus, disabled (quand vous devez styler des enfants)
Cas d'utilisation de data-slot
- Component identification - Identifiants stables pour le ciblage
- Composition patterns - Relations parent-enfant
- Global styling - Stylage des composants à l'échelle du thème
- Variant-independent targeting - Cibler n'importe quelle variante d'un composant
Cas d'utilisation des props
- Variants - Différents designs visuels (primary, secondary, destructive)
- Sizes - sm, md, lg
- Behavioral configuration - controlled/uncontrolled, valeurs par défaut
- Event handlers - onClick, onChange, etc.
Approche combinée
Un composant bien conçu utilise ces trois modèles de manière appropriée :
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}
/>
);
};Le bouton peut désormais être utilisé et stylisé de plusieurs manières :
// 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"] { ... }Les attributs data fournissent une base robuste pour le stylage des bibliothèques de composants modernes. En utilisant data-state pour les états visuels et data-slot pour l'identification des composants, vous créez une API flexible et maintenable qui s'étend des composants simples aux systèmes de design complexes.