Episode ini memperkenalkan class-variance-authority (CVA) untuk membangun komponen berbasis variants, pola utility wrapper memakai clsx dan tailwind-merge, serta cara menguji output class dalam unit test dengan snapshot dan DOM-based assertions.

Komponen Tailwind yang sehat butuh API yang rapi: bagaimana konsumen menambahkan class, bagaimana variants digabung, dan bagaimana konflik antar class diselesaikan. Episode 15 memperkenalkan tiga library yang menjadi standar industri: class-variance-authority (CVA), clsx, dan tailwind-merge.
Tanpa alat bantu ini, komponen mudah berubah menjadi perangkap: variants bersarang dengan ternary, class yang bertabrakan, dan override yang tidak bisa diprediksi. Dengan CVA plus utility wrapper, API komponen menjadi deklaratif, deterministik, dan mudah diuji.
CVA mendefinisikan variants komponen secara deklaratif:
npm install clsx tailwind-merge class-variance-authorityimport { cva, type VariantProps } from "class-variance-authority";
const button = cva("rounded px-4 py-2 font-medium", {
variants: {
variant: {
primary: "bg-blue-500 text-white",
ghost: "bg-transparent text-gray-700",
},
size: {
sm: "text-sm px-3 py-1",
lg: "text-lg px-6 py-3",
},
},
defaultVariants: {
variant: "primary",
size: "sm",
},
});
export type ButtonVariants = VariantProps<typeof button>;cva("rounded px-4 py-2 font-medium", {...}) menerima base class lalu tabel variants. Fungsi button({ variant: "primary", size: "lg" }) menghasilkan string class yang lengkap — dan tipe ButtonVariants menjaga pemakaian variants tetap aman di sisi TypeScript.
clsx menggabungkan class secara kondisional dengan rapi:
import { clsx } from "clsx";
const className = clsx(
"btn",
isActive && "btn-active",
[enabled ? "bg-blue-500" : "bg-gray-300"],
);clsx("btn", isActive && "btn-active") mem-filter nilai falsy otomatis — jauh lebih bersih daripada rangkaian template string dan ternary.
Masalahnya: dua utility yang bertabrakan seperti px-4 dan px-6 akan saling menimpa secara tidak terduga. tailwind-merge menyelesaikannya — class terakhir yang menang:
import { twMerge } from "tailwind-merge";
const merged = twMerge("px-4 py-2", "px-6");
// hasil: "py-2 px-6" — px-6 menggantikan px-4twMerge("px-4 py-2", "px-6") mengenali kelompok utility yang saling bertentangan dan memilih yang terakhir. Kombinasi khas dalam komponen:
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { cva } from "class-variance-authority";
const button = cva("rounded px-4 py-2", { variants: { ... } });
export function Button({ variant, size, className }) {
return <button className={twMerge(clsx(button({ variant, size }), className))} />;
}Alur twMerge(clsx(button({...}), className)) menggabungkan variants CVA, kondisi dinamis, dan class dari konsumen — dengan konflik terselesaikan secara konsisten.
API komponen yang berbasis string class sangat mudah diuji. Dua pendekatan umum:
Snapshot testing pada fungsi CVA:
import { expect, test } from "vitest";
import { button } from "./button";
test("primary large menghasilkan class yang diharapkan", () => {
expect(button({ variant: "primary", size: "lg" })).toMatchInlineSnapshot(
`"rounded px-4 py-2 font-medium bg-blue-500 text-white px-6 py-3"`,
);
});DOM-based assertions dengan framework testing seperti Testing Library:
import { render } from "@testing-library/react";
import { Button } from "./button";
test("class override konsumen menggantikan default", () => {
const { container } = render(<Button className="px-8" />);
expect(container.firstChild).toHaveClass("px-8");
});expect(container.firstChild).toHaveClass("px-8") memverifikasi bahwa override konsumen benar-benar diterapkan — ini menangkap regresi resolusi konflik yang sulit dilihat secara visual.
Info
Snapshot mudah rapuh jika berubah sering. Pilih snapshot untuk struktur variants yang stabil, dan gunakan assertion eksplisit (toHaveClass) untuk perilaku override yang penting — kombinasi keduanya menjaga uji tetap informatif.
Episode 15 membawa kalian ke component API patterns modern: CVA untuk variants deklaratif, clsx dan tailwind-merge untuk penggabungan dan resolusi konflik, serta pengujian output class yang menjaga API tetap terkunci.
Inti yang harus dibawa pulang:
clsx menggabungkan class kondisional dengan filter falsy.twMerge menyelesaikan konflik antar utility — yang terakhir menang.twMerge(clsx(button({...}), className)).toHaveClass mengunci perilaku override.Di episode 16 selanjutnya kita akan membahas advanced responsive techniques & container queries — layout adaptif dengan container queries, fluid typography memakai clamp(), serta komposisi layout yang digerakkan oleh state.