mirror of
https://github.com/vercel/components.build.git
synced 2026-09-14 20:06:39 +08:00
55fae375a4
* Update Geistdocs * Update geistdocs.tsx * Run translation script * Delete toc.tsx * Update route.ts * Update route.ts
400 lines
12 KiB
Plaintext
400 lines
12 KiB
Plaintext
---
|
|
title: Attributs data
|
|
description: 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 :
|
|
|
|
```tsx
|
|
<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 :
|
|
|
|
```tsx title="component.tsx"
|
|
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 :
|
|
|
|
```tsx title="app.tsx"
|
|
<Dialog className="data-[state=open]:opacity-100 data-[state=closed]:opacity-0" />
|
|
```
|
|
|
|
### Avantages de cette approche
|
|
|
|
1. **Single className prop** - Pas besoin de multiples props className spécifiques à l'état
|
|
2. **Composable** - Combinez plusieurs attributs data pour des états complexes
|
|
3. **Standard CSS** - Fonctionne avec n'importe quelle solution CSS-in-JS ou avec du CSS pur
|
|
4. **Sécurisé par le typage** - TypeScript peut inférer les valeurs des attributs data
|
|
5. **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 :
|
|
|
|
```tsx
|
|
// 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 :
|
|
|
|
```tsx
|
|
<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 :
|
|
|
|
```js title="tailwind.config.js"
|
|
module.exports = {
|
|
theme: {
|
|
extend: {
|
|
data: {
|
|
open: 'state="open"',
|
|
closed: 'state="closed"',
|
|
active: 'state="active"',
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Vous pouvez alors utiliser la forme abrégée :
|
|
|
|
```tsx
|
|
<Dialog className="data-open:opacity-100 data-closed:opacity-0" />
|
|
```
|
|
|
|
### Intégration avec Radix UI
|
|
|
|
Ce modèle est largement utilisé par [Radix UI](https://www.radix-ui.com/), qui applique automatiquement des attributs data à ses primitives :
|
|
|
|
```tsx
|
|
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/off
|
|
- `data-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/vertical
|
|
- `data-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](https://ui.shadcn.com/), 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 :
|
|
|
|
```tsx
|
|
// 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 :
|
|
|
|
```tsx title="field-set.tsx"
|
|
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}
|
|
/>
|
|
);
|
|
}
|
|
```
|
|
|
|
```tsx title="checkbox-group.tsx"
|
|
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
|
|
|
|
1. **Identifiants stables** - Ne casse pas lorsque les détails d'implémentation changent
|
|
2. **Ciblage sémantique** - Ciblez en fonction du rôle du composant, pas de sa structure
|
|
3. **Encapsulation** - Les classes internes restent privées
|
|
4. **Composable** - Fonctionne avec n'importe quel niveau d'imbrication et de composition
|
|
5. **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 :
|
|
|
|
```tsx title="form.tsx"
|
|
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 :
|
|
|
|
```tsx title="card.tsx"
|
|
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 :
|
|
|
|
```css title="globals.css"
|
|
/* 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 :
|
|
|
|
1. **Use kebab-case** - `data-slot="form-field"` not `data-slot="formField"`
|
|
2. **Be specific** - `data-slot="submit-button"` not `data-slot="button"`
|
|
3. **Match component purpose** - Le nom reflète ce que fait le composant, pas son apparence
|
|
4. **Avoid implementation details** - `data-slot="user-avatar"` not `data-slot="rounded-image"`
|
|
|
|
```tsx
|
|
// 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
|
|
```
|
|
|
|
## Quand 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 :
|
|
|
|
```tsx title="button.tsx"
|
|
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 :
|
|
|
|
```tsx
|
|
// 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. |