kodingup.camp

Beranda/Tailwind Mastery

Komposisi Styling yang Dapat Dipakai Ulang

Modul 14 dari 163 menit bacaReusable Styling Composition

Isi modul

    Kenapa ini penting

    Setelah beberapa minggu memakai Tailwind, kamu akan menemukan pola berulang: tombol yang sama muncul di 40 tempat dengan 12 class yang sama. Kalau desainer meminta sudut tombol diubah, apakah kamu harus mengedit 40 tempat? Tentu tidak. Pertanyaannya: bagaimana cara memakai ulang gaya dengan benar?

    Ada dua jalan besar: membuat komponen (React, Vue, atau partial template) atau membuat class dengan @apply. Modul ini membahas kapan memakai yang mana, plus tiga alat populer di ekosistem React: clsx (menggabungkan class secara kondisional), tailwind-merge (menyelesaikan konflik class), dan cva (mengelola variant komponen). Versi terverifikasi: clsx 2.1.1, tailwind-merge 3.7.0 (seri 3.x mendukung Tailwind v4), class-variance-authority 0.7.1.

    Konsep inti

    Jalan 1: komponen (dianjurkan)

    Kalau kamu memakai React, daftar class panjang cukup ditulis sekali di dalam komponen <Button>. Komponen tidak hanya memakai ulang gaya, tetapi juga struktur HTML dan perilaku (misalnya ikon loading). Ini adalah cara utama yang dianjurkan dokumentasi Tailwind.

    Untuk HTML statis tanpa framework, pemakaian ulang bisa lewat multi-cursor editing di editor atau loop di template engine.

    Jalan 2: @apply

    @apply menyalin utility ke dalam class CSS buatanmu:

    .btn-primary { @apply rounded-lg bg-indigo-600 px-4 py-2 text-white; }
    

    Gunakan @apply dengan hemat, untuk kasus seperti:

    Kenapa hemat? Karena kalau semua dijadikan @apply, kamu kembali ke masalah CSS semantik: harus memikirkan nama, file CSS membesar, dan kehilangan kemampuan membaca gaya langsung di HTML.

    Catatan v4: kalau kamu memakai @apply di blok <style> komponen (Vue/Svelte) atau CSS Modules, file itu terpisah dari CSS utama sehingga tidak tahu token tema. Tambahkan @reference "../style.css"; di atasnya agar @apply mengenali utility dan tema tanpa menduplikasi CSS.

    clsx: menggabungkan class secara kondisional

    clsx("px-4", aktif && "bg-indigo-600", { "opacity-50": nonaktif })
    

    clsx membuang nilai false, null, undefined, lalu menggabungkan sisanya dengan spasi.

    tailwind-merge: menyelesaikan konflik

    Masalahnya: komponen <Button className="px-8"> padahal di dalamnya sudah ada px-4. Hasil "px-4 px-8" tidak bisa ditebak, karena yang menang bergantung pada urutan di CSS, bukan urutan di string. twMerge("px-4 px-8") menghasilkan "px-8": class terakhir menang, konflik dihapus.

    Gabungan keduanya biasanya dibungkus fungsi kecil bernama cn(). Pola ini sangat populer di ekosistem React.

    cva: variant terstruktur

    cva (class-variance-authority) membantu mendefinisikan variant komponen (misalnya variant: primary | secondary | danger dan size: sm | md | lg) dalam satu objek yang rapi, lengkap dengan default dan kombinasi khusus. Analogi: cva seperti formulir pemesanan kopi: kamu tentukan pilihan ukuran dan jenis susu, dan barista (cva) tahu resep class untuk setiap kombinasi.

    Contoh kode

    npm install clsx tailwind-merge class-variance-authority
    
    // src/lib/cn.js, fungsi pembantu yang dipakai di seluruh proyek
    import { clsx } from "clsx";
    import { twMerge } from "tailwind-merge";
    
    export function cn(...inputs) {
      return twMerge(clsx(inputs));
    }
    
    // Contoh hasil:
    // cn("px-4 py-2", "px-8")                 → "py-2 px-8"
    // cn("text-sm", false && "font-bold")     → "text-sm"
    // cn("bg-red-500", { "bg-blue-500": true }) → "bg-blue-500"
    
    // src/components/Badge.jsx, komponen sederhana dengan clsx + tailwind-merge
    import { cn } from "../lib/cn";
    
    export function Badge({ tone = "netral", className, children }) {
      return (
        <span
          className={cn(
            "inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium",
            tone === "netral" && "bg-gray-100 text-gray-700",
            tone === "sukses" && "bg-green-100 text-green-800",
            tone === "bahaya" && "bg-red-100 text-red-800",
            className // className dari luar boleh menimpa, konflik diselesaikan twMerge
          )}
        >
          {children}
        </span>
      );
    }
    
    // <Badge tone="sukses">Lunas</Badge>              → lencana hijau
    // <Badge tone="bahaya" className="px-4">Telat</Badge> → px-4 menggantikan px-2.5
    
    // src/components/Tombol.jsx, variant terstruktur dengan cva
    import { cva } from "class-variance-authority";
    import { cn } from "../lib/cn";
    
    const tombolVariants = cva(
      // class dasar untuk semua tombol
      "inline-flex cursor-pointer items-center justify-center gap-2 rounded-lg font-semibold transition focus-visible:outline-2 focus-visible:outline-offset-2 disabled:pointer-events-none disabled:opacity-50",
      {
        variants: {
          variant: {
            primary: "bg-indigo-600 text-white hover:bg-indigo-700 focus-visible:outline-indigo-600",
            secondary: "border border-gray-300 bg-white text-gray-900 hover:bg-gray-50",
            danger: "bg-red-600 text-white hover:bg-red-700 focus-visible:outline-red-600",
          },
          size: {
            sm: "h-8 px-3 text-sm",
            md: "h-10 px-4",
            lg: "h-12 px-6 text-lg",
          },
        },
        defaultVariants: { variant: "primary", size: "md" },
      }
    );
    
    export function Tombol({ variant, size, className, ...props }) {
      return <button className={cn(tombolVariants({ variant, size }), className)} {...props} />;
    }
    
    // <Tombol>Simpan</Tombol>                         → primary, md
    // <Tombol variant="danger" size="sm">Hapus</Tombol> → merah, kecil
    
    /* Kapan @apply masuk akal: menata HTML dari CMS yang tidak bisa diberi class */
    @import "tailwindcss";
    
    @layer components {
      .konten-cms a {
        @apply font-medium text-indigo-600 underline underline-offset-2 hover:text-indigo-800;
      }
      .konten-cms table {
        @apply w-full border-collapse text-sm;
      }
      .konten-cms td {
        @apply border border-gray-200 px-3 py-2;
      }
    }
    
    /* Tombol.module.css, CSS Module terpisah butuh @reference agar kenal tema */
    @reference "../style.css";
    
    .tombolKhusus {
      @apply rounded-lg bg-brand-500 px-4 py-2 text-white;
    }
    

    Kesalahan umum

    1. Membuat @apply untuk semuanya (.card, .card-title, .card-body, ...). Kamu kembali ke CSS semantik. Utamakan komponen.
    2. Menggabungkan className dari luar tanpa tailwind-merge. Hasil px-4 px-8 tidak bisa ditebak. Pakai cn().
    3. Membuat nama class dinamis di cva (`bg-${warna}-600`). cva tetap hanya string; tulis class lengkap di setiap variant agar terdeteksi.
    4. Lupa @reference di CSS Modules atau <style> komponen sehingga muncul error "Cannot apply unknown utility class".
    5. Memakai tailwind-merge versi 2.x dengan Tailwind v4. Seri 2.x untuk Tailwind v3; gunakan 3.x untuk v4.

    Latihan

    1. Mudah, Apa hasil cn("p-2 text-sm", "p-6", false, "font-bold")?
    2. Sedang, Buat komponen Alert dengan prop tipe (info, sukses, peringatan) memakai cn(), yang menerima className tambahan.
    3. Menantang, Ubah Alert memakai cva dengan variant tipe dan ukuran (ringkas, normal), default info dan normal, plus compound variant: kalau tipe="peringatan" dan ukuran="normal", tambahkan border-l-4.
    Lihat solusi
    1. Hasilnya "text-sm p-6 font-bold". p-2 dibuang karena konflik dengan p-6, dan false diabaikan.

    2. Alert dengan cn:

    import { cn } from "../lib/cn";
    
    export function Alert({ tipe = "info", className, children }) {
      return (
        <div
          role="alert"
          className={cn(
            "rounded-lg p-4 text-sm",
            tipe === "info" && "bg-sky-50 text-sky-800",
            tipe === "sukses" && "bg-green-50 text-green-800",
            tipe === "peringatan" && "bg-amber-50 text-amber-900",
            className
          )}
        >
          {children}
        </div>
      );
    }
    
    1. Alert dengan cva:
    import { cva } from "class-variance-authority";
    import { cn } from "../lib/cn";
    
    const alertVariants = cva("rounded-lg", {
      variants: {
        tipe: {
          info: "bg-sky-50 text-sky-800 border-sky-500",
          sukses: "bg-green-50 text-green-800 border-green-500",
          peringatan: "bg-amber-50 text-amber-900 border-amber-500",
        },
        ukuran: {
          ringkas: "p-2 text-xs",
          normal: "p-4 text-sm",
        },
      },
      compoundVariants: [{ tipe: "peringatan", ukuran: "normal", class: "border-l-4" }],
      defaultVariants: { tipe: "info", ukuran: "normal" },
    });
    
    export function Alert({ tipe, ukuran, className, children }) {
      return (
        <div role="alert" className={cn(alertVariants({ tipe, ukuran }), className)}>
          {children}
        </div>
      );
    }
    // <Alert tipe="peringatan">Kuota hampir habis</Alert> → kuning dengan garis kiri 4px
    

    Ringkasan

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.