Atribut Data
Menggunakan atribut data untuk penggayaan deklaratif dan identifikasi komponen.
Atribut data menyediakan cara yang kuat untuk mengekspos status dan struktur komponen kepada konsumen, memungkinkan penggayaan yang fleksibel tanpa ledakan props. Perpustakaan komponen modern menggunakan dua pola utama: data-state untuk status visual dan data-slot untuk identifikasi komponen.
Menata status dengan data-state
Salah satu anti-pola yang paling umum dalam penggayaan komponen adalah mengekspos prop className terpisah untuk berbagai status.
Pada komponen yang kurang modern, Anda sering melihat API seperti ini:
<Dialog
openClassName="bg-black"
closedClassName="bg-white"
classes={{
open: "opacity-100",
closed: "opacity-0"
}}
/>Pendekatan ini memiliki beberapa masalah:
- Mengaitkan status internal komponen ke API penggayaannya
- Membuat ledakan jumlah props saat komponen menjadi lebih kompleks
- Membuat komponen lebih sulit digunakan dan dipelihara
- Mencegah penggayaan berdasarkan kombinasi status
Solusi: atribut data-state
Sebagai gantinya, gunakan atribut data-* untuk mengekspos status komponen secara deklaratif. Ini memungkinkan konsumen untuk menggaya komponen berdasarkan status menggunakan selektor CSS standar:
const Dialog = ({ className, ...props }: DialogProps) => {
const [isOpen, setIsOpen] = useState(false);
return (
<div
data-state={isOpen ? 'open' : 'closed'}
className={cn('transition-all', className)}
{...props}
/>
);
};Sekarang konsumen dapat menggaya komponen berdasarkan status dari luar:
<Dialog className="data-[state=open]:opacity-100 data-[state=closed]:opacity-0" />Manfaat pendekatan ini
- Satu prop className - Tidak perlu prop className terpisah untuk tiap status
- Komposabel - Gabungkan beberapa atribut data untuk status kompleks
- CSS standar - Bekerja dengan solusi CSS-in-JS apa pun atau CSS biasa
- Type-safe - TypeScript dapat menginfer nilai atribut data
- Dapat diinspeksi - Status terlihat di DevTools sebagai atribut HTML
Pola status umum
Gunakan atribut data untuk semua jenis status komponen:
// 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" />Penggayaan dengan Tailwind
Tailwind mendukung varian arbitrer, membuat penggayaan atribut data menjadi elegan:
<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'
)}
/>Untuk status yang sering digunakan, Anda dapat memperluas konfigurasi Tailwind:
module.exports = {
theme: {
extend: {
data: {
open: 'state="open"',
closed: 'state="closed"',
active: 'state="active"',
}
}
}
}Sekarang Anda dapat menggunakan shorthand:
<Dialog className="data-open:opacity-100 data-closed:opacity-0" />Integrasi dengan Radix UI
Pola ini digunakan secara luas oleh Radix UI, yang secara otomatis menambahkan atribut data ke primitifnya:
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>Atribut data lain yang disediakan Radix meliputi:
data-state- open/closed, active/inactive, on/offdata-side- top/right/bottom/left (untuk elemen yang diposisikan)data-align- start/center/end (untuk elemen yang diposisikan)data-orientation- horizontal/verticaldata-disabled- hadir saat dinonaktifkandata-placeholder- hadir saat menampilkan placeholder
Identifikasi komponen dengan data-slot
Sementara data-state melacak status visual, data-slot mengidentifikasi tipe komponen dalam sebuah komposisi. Pola ini, yang dipopulerkan oleh shadcn/ui, memungkinkan komponen induk menarget dan menggaya anak komponen tertentu tanpa bergantung pada nama kelas atau selektor elemen yang rapuh.
Masalah dengan penargetan anak
Pendekatan tradisional untuk menggaya anak komponen memiliki keterbatasan signifikan:
// 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>Solusi: atribut data-slot
Gunakan data-slot untuk memberi komponen pengidentifikasi yang stabil yang dapat ditarget oleh induk:
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}
/>
);
}Manfaat data-slot
- Pengidentifikasi yang stabil - Tidak akan rusak saat detail implementasi berubah
- Penargetan semantik - Menarget berdasarkan tujuan komponen, bukan struktur
- Enkapsulasi - Kelas internal tetap privat
- Komposabel - Bekerja dengan nesting dan komposisi arbitrer
- Type-safe - Dapat divalidasi dan didokumentasikan
Menggunakan has-[] untuk penggayaan yang peka-induk
Selektor has-[] Tailwind dikombinasikan dengan data-slot menciptakan penggayaan peka-induk yang kuat:
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}
/>
);
}Menggunakan [&_] untuk penargetan keturunan
Untuk nesting yang lebih dalam, gunakan pola [&_selector] untuk menargetkan keturunan mana pun:
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 dengan data-slot
Data slot bekerja sangat baik dengan CSS global untuk konsistensi tema secara menyeluruh:
/* 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;
}Konvensi penamaan
Ikuti konvensi berikut untuk penamaan data-slot yang konsisten:
- Gunakan kebab-case -
data-slot="form-field"bukandata-slot="formField" - Jadilah spesifik -
data-slot="submit-button"bukandata-slot="button" - Cocokkan tujuan komponen - Nama mencerminkan apa yang dilakukan, bukan tampilannya
- Hindari detail implementasi -
data-slot="user-avatar"bukandata-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 camelCaseKapan menggunakan atribut data vs props
Memahami kapan menggunakan masing-masing pola adalah kunci untuk API yang bersih:
Kasus penggunaan data-state
- Status visual - open/closed, active/inactive, loading, dll.
- Status tata letak - orientation, side, alignment
- Status interaksi - hover, focus, disabled (ketika Anda perlu menggaya anak)
Kasus penggunaan data-slot
- Identifikasi komponen - Pengidentifikasi stabil untuk penargetan
- Pola komposisi - Hubungan induk-anak
- Penggayaan global - Penggayaan komponen di seluruh tema
- Penargetan independen varian - Menarget setiap varian dari sebuah komponen
Kasus penggunaan props
- Varian - Desain visual berbeda (primary, secondary, destructive)
- Ukuran - sm, md, lg
- Konfigurasi perilaku - controlled/uncontrolled, nilai default
- Event handler - onClick, onChange, dll.
Pendekatan gabungan
Komponen yang dirancang dengan baik menggunakan ketiga pola ini secara tepat:
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}
/>
);
};Sekarang tombol dapat digunakan dan digaya dengan berbagai cara:
// 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"] { ... }Atribut data memberikan fondasi yang kuat untuk penggayaan perpustakaan komponen modern. Dengan menggunakan data-state untuk status visual dan data-slot untuk identifikasi komponen, Anda menciptakan API yang fleksibel dan mudah dipelihara yang dapat diskalakan dari komponen sederhana hingga sistem desain yang kompleks.