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:

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}
    />
  );
};

Sekarang konsumen dapat menggaya komponen berdasarkan status dari luar:

app.tsx
<Dialog className="data-[state=open]:opacity-100 data-[state=closed]:opacity-0" />

Manfaat pendekatan ini

  1. Satu prop className - Tidak perlu prop className terpisah untuk tiap status
  2. Komposabel - Gabungkan beberapa atribut data untuk status kompleks
  3. CSS standar - Bekerja dengan solusi CSS-in-JS apa pun atau CSS biasa
  4. Type-safe - TypeScript dapat menginfer nilai atribut data
  5. 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:

tailwind.config.js
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/off
  • data-side - top/right/bottom/left (untuk elemen yang diposisikan)
  • data-align - start/center/end (untuk elemen yang diposisikan)
  • data-orientation - horizontal/vertical
  • data-disabled - hadir saat dinonaktifkan
  • data-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:

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}
    />
  );
}
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}
    />
  );
}

Manfaat data-slot

  1. Pengidentifikasi yang stabil - Tidak akan rusak saat detail implementasi berubah
  2. Penargetan semantik - Menarget berdasarkan tujuan komponen, bukan struktur
  3. Enkapsulasi - Kelas internal tetap privat
  4. Komposabel - Bekerja dengan nesting dan komposisi arbitrer
  5. 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:

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}
    />
  );
}

Menggunakan [&_] untuk penargetan keturunan

Untuk nesting yang lebih dalam, gunakan pola [&_selector] untuk menargetkan keturunan mana pun:

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 dengan data-slot

Data slot bekerja sangat baik dengan CSS global untuk konsistensi tema secara menyeluruh:

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;
}

Konvensi penamaan

Ikuti konvensi berikut untuk penamaan data-slot yang konsisten:

  1. Gunakan kebab-case - data-slot="form-field" bukan data-slot="formField"
  2. Jadilah spesifik - data-slot="submit-button" bukan data-slot="button"
  3. Cocokkan tujuan komponen - Nama mencerminkan apa yang dilakukan, bukan tampilannya
  4. Hindari detail implementasi - data-slot="user-avatar" bukan data-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

Kapan 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:

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}
    />
  );
};

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.