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:
- Menata HTML yang tidak bisa kamu kontrol class-nya (output CMS, library pihak ketiga).
- Elemen kecil yang sangat sering dipakai di proyek tanpa framework komponen.
- Menimpa gaya dari library lain.
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
- Membuat
@applyuntuk semuanya (.card,.card-title,.card-body, ...). Kamu kembali ke CSS semantik. Utamakan komponen. - Menggabungkan
classNamedari luar tanpatailwind-merge. Hasilpx-4 px-8tidak bisa ditebak. Pakaicn(). - Membuat nama class dinamis di cva (
`bg-${warna}-600`). cva tetap hanya string; tulis class lengkap di setiap variant agar terdeteksi. - Lupa
@referencedi CSS Modules atau<style>komponen sehingga muncul error "Cannot apply unknown utility class". - Memakai
tailwind-mergeversi 2.x dengan Tailwind v4. Seri 2.x untuk Tailwind v3; gunakan 3.x untuk v4.
Latihan
- Mudah, Apa hasil
cn("p-2 text-sm", "p-6", false, "font-bold")? - Sedang, Buat komponen
Alertdengan proptipe(info,sukses,peringatan) memakaicn(), yang menerimaclassNametambahan. - Menantang, Ubah
Alertmemakaicvadengan varianttipedanukuran(ringkas,normal), defaultinfodannormal, plus compound variant: kalautipe="peringatan"danukuran="normal", tambahkanborder-l-4.
Lihat solusi
-
Hasilnya
"text-sm p-6 font-bold".p-2dibuang karena konflik denganp-6, danfalsediabaikan. -
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>
);
}
- 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
- Cara utama memakai ulang gaya adalah komponen; daftar class ditulis sekali.
@applyuntuk kasus terbatas: HTML yang tidak bisa diberi class, atau menimpa library.- CSS terpisah (CSS Modules,
<style>komponen) butuh@referenceagar@applykenal tema. cn()=twMerge(clsx(...)): gabung kondisional dan hapus konflik class.cvamengelola variant, default, dan compound variant secara terstruktur.