Function yang menghasilkan string class dan compiler yang mengeluarkan CSS menyelesaikan masalah yang berbeda, walaupun keduanya pada akhirnya dapat menghasilkan atribut `class` di browser.
Perbedaan itu penting ketika recipe komponen Tailwind dimulai seperti ini:
const button = tv({
base: 'inline-flex rounded-md',
variants: {
color: {
success: 'bg-green-500 hover:bg-green-700',
},
disabled: {
true: 'pointer-events-none opacity-50',
},
},
compoundVariants: [
{
color: 'success',
disabled: true,
class: 'bg-green-300 hover:bg-green-300',
},
],
});Mudah untuk mengira bahwa build step dapat mengubah recipe tersebut menjadi selector semantik seperti:
.button-color-success {
/* generated declarations */
}Itu bukan fungsi `tailwind-variants`. Output-nya adalah string class. CSS di balik utility class tersebut harus sudah dihasilkan oleh Tailwind. Batas yang sama berlaku untuk `clsx`: library ini memilih dan menggabungkan nama class, tetapi tidak membuat CSS rule baru.
Komposisi class bekerja pada nama
`clsx` adalah utility untuk menyusun string class secara kondisional. Dari string, array, dan object kondisi, library ini menghasilkan satu string yang dipisahkan spasi.
import clsx from 'clsx';
const classes = clsx(
'button',
active && 'button-active',
disabled && 'button-disabled',
);Jika `active` bernilai true dan `disabled` false, hasilnya setara dengan:
button button-activeOperasi tersebut tidak mendefinisikan arti `.button` atau `.button-active`. Selector itu harus berasal dari stylesheet atau pipeline generasi CSS lain.
Aliran datanya adalah:
conditions
|
v
clsx(...)
|
v
"class names"
|
v
DOM class attributeFunction ini bekerja pada lapisan string, bukan lapisan CSS rule.
Tailwind Variants menambahkan model variant, bukan CSS compiler
`tailwind-variants` memberi struktur di atas string class Tailwind. Sebuah recipe dapat mendefinisikan base class, sumbu variant, state boolean, default, slot, dan compound variant.
import { tv } from 'tailwind-variants';
const button = tv({
base: 'inline-flex rounded-md px-4 py-2',
variants: {
color: {
success: 'bg-green-500 text-white hover:bg-green-700',
neutral: 'bg-zinc-200 text-zinc-900',
},
size: {
sm: 'text-sm',
lg: 'text-lg',
},
},
defaultVariants: {
color: 'neutral',
size: 'sm',
},
});Pemanggilan recipe menghasilkan string class:
button({ color: 'success', size: 'lg' });Secara konseptual:
inline-flex rounded-md px-4 py-2
bg-green-500 text-white hover:bg-green-700
text-lgBuild default-nya juga dapat menyelesaikan utility Tailwind yang saling konflik. Mekanisme itu tetap bekerja pada token class. Recipe tidak diubah menjadi selector baru seperti `.button-success-lg`.
Pemisahan ini berguna karena recipe yang sama dapat dipakai oleh beberapa framework:
<!-- conceptual output -->
<button class="inline-flex rounded-md px-4 py-2 bg-green-500 text-white text-lg">
Save
</button>Vue dapat mengikat string tersebut ke `:class`, Svelte ke `class`, dan vanilla JavaScript dapat memasukkannya ke `element.className`. Recipe tetap framework-agnostic karena produknya hanya berupa string.
Tailwind menghasilkan utility dari source detection
Tailwind berada pada tahap yang berbeda. Tailwind memindai source yang dikonfigurasi untuk mencari kandidat utility lalu menghasilkan CSS untuk class yang dikenali.
Ketika recipe berisi:
color: {
success: 'bg-green-500 hover:bg-green-700',
}Tailwind harus melihat string utility tersebut di source path yang ikut dipindai. Tailwind Variants tidak menggantikan kebutuhan itu.
Hubungan build-nya lebih tepat digambarkan sebagai:
tv() source strings -------------------+
|
Tailwind source detection |
| |
v |
generated utility CSS |
| |
+------------------------------+
|
v
browser matches classesRecipe memilih nama utility saat runtime atau render. Tailwind menghasilkan CSS yang sesuai saat build.
Class semantik membutuhkan tahap authoring CSS yang eksplisit
Jika HTML yang diinginkan adalah:
<button class="button-color-success">Save</button>maka `.button-color-success` harus ditulis atau dihasilkan sebagai CSS selector.
Dengan Tailwind, salah satu pilihan langsung adalah custom CSS layer dengan `@apply` pada kasus yang sesuai:
.button {
@apply inline-flex rounded-md px-4 py-2;
}
.button-color-success {
@apply bg-green-500 text-white hover:bg-green-700;
}
.button-disabled {
@apply pointer-events-none opacity-50;
}
.button-color-success.button-disabled {
@apply bg-green-300 hover:bg-green-300;
}HTML kemudian dapat memakai class semantik:
<button class="button button-color-success">Save</button>Arsitektur ini berbeda dari memanggil `button({ color: ‘success’ })`. Model pertama memusatkan pemilihan state di JavaScript; model kedua mengekspos selector yang dapat dirujuk langsung oleh HTML.
Tidak ada yang secara otomatis lebih benar. Poin pentingnya adalah bahwa konversi recipe variant JavaScript menjadi selector semantik membutuhkan generator yang memang dirancang untuk itu. `tailwind-variants` bukan generator tersebut.
CSS Modules mengganti nama selector saat build
CSS Modules ikut dalam transformasi CSS. Source file dapat memakai nama class lokal yang mudah dibaca:
/* button.module.css */
.button {
padding: 0.5rem 1rem;
border-radius: 0.375rem;
}
.success {
background: green;
color: white;
}Kode aplikasi mengimpor mapping:
import styles from './button.module.css';
element.className = styles.button + ' ' + styles.success;Bundler dapat mengubah nama lokal itu menjadi nama scoped seperti:
<button class="_button_1a2b3_1 _success_1a2b3_6">Save</button>Output persisnya bergantung pada konfigurasi CSS Modules. Pada Vite, penamaan CSS Module dapat dikonfigurasi melalui opsi seperti `generateScopedName` dan `hashPrefix`.
Perbedaan penting dari `clsx` adalah build plugin menguasai kedua sisi mapping:
.button in source CSS
|
v
CSS Modules transform
|
+--> generated selector in CSS
|
+--> generated name exported to JavaScriptKarena itu nama class yang di-hash atau di-scope tetap sinkron dengan deklarasi CSS-nya.
Nama acak biasanya bukan requirement yang tepat
Production build biasanya membutuhkan nama scoped yang deterministik, bukan randomness murni.
Misalnya nama class berubah tanpa pola yang dapat direproduksi:
build 1 -> _x7k2p
build 2 -> _q9m4cPerubahan nama antar-build sendiri tidak bermasalah jika HTML dan CSS dihasilkan bersama. Masalah muncul ketika nama berubah secara independen atau berubah saat runtime tanpa CSS yang cocok.
Karena itu build tooling umumnya menurunkan nama dari input yang stabil seperti local class name, file path, content, atau hash. Hasilnya bisa terlihat acak bagi manusia, tetapi tetap cukup reproducible bagi pipeline build.
Invariant yang harus dijaga adalah:
generated class referenced by markup
==
generated selector emitted in CSSObfuscation hanya efek tambahan. Sinkronisasi adalah properti correctness yang sebenarnya.
StyleX adalah sistem styling berbasis compiler
StyleX bergerak lebih jauh ke generasi CSS yang dikelola compiler. Style ditulis sebagai object JavaScript:
import * as stylex from '@stylexjs/stylex';
const styles = stylex.create({
button: {
paddingBlock: '0.5rem',
paddingInline: '1rem',
borderRadius: '0.375rem',
},
success: {
backgroundColor: 'green',
color: 'white',
},
});Compiler StyleX mengekstrak static style menjadi collision-free atomic CSS saat compile time. Kode aplikasi kemudian memakai representasi yang dihasilkan melalui API seperti `stylex.props()`.
Pipeline ini secara struktur berbeda dari `clsx`:
StyleX object
|
v
StyleX compiler
|
+--> static atomic CSS
|
+--> generated references used by application codeDokumentasi StyleX secara eksplisit menjelaskan generasi static CSS saat compile time dan tidak melakukan runtime style injection untuk style yang diekstrak.
Karena itu StyleX memang dapat disebut sistem generasi CSS, sedangkan `clsx` tidak.
StyleX dan Tailwind dapat hidup berdampingan, tetapi menguasai CSS yang berbeda
Satu project secara teknis dapat memakai kedua sistem:
<div className="grid gap-4 p-6">
<button {...stylex.props(styles.button, styles.success)}>
Save
</button>
</div>Dalam susunan ini:
Tailwind
-> utility classes seperti grid, gap-4, p-6
StyleX
-> compiler-generated atomic classesKeduanya akhirnya memengaruhi DOM yang sama, tetapi pipeline build-nya tidak melebur menjadi satu variant compiler bersama. Utility Tailwind bukan StyleX style declaration, dan object StyleX tidak berubah menjadi utility Tailwind.
Memakai keduanya untuk property yang sama juga menciptakan masalah ownership. Jika Tailwind dan StyleX sama-sama mengatur `background-color`, hasil akhir bergantung pada urutan CSS hasil generate dan perilaku cascade masing-masing sistem. Boundary yang lebih bersih adalah memberi satu sistem ownership atas satu kelompok property atau memisahkan keduanya berdasarkan scope komponen.
Vue dan Svelte tidak mengubah batas ini
Sintaks framework hanya mengubah cara string class mencapai DOM, bukan apa yang dihasilkan library.
Vue:
<script setup>
import { tv } from 'tailwind-variants';
const button = tv({
variants: {
color: {
success: 'bg-green-500 text-white',
},
},
});
</script>
<template>
<button :class="button({ color: 'success' })">
Save
</button>
</template>Svelte:
<script>
import { tv } from 'tailwind-variants';
const button = tv({
variants: {
color: {
success: 'bg-green-500 text-white',
},
},
});
</script>
<button class={button({ color: 'success' })}>
Save
</button>Pada kedua contoh, `tv()` tetap mengembalikan nama class. Framework tidak mengubah recipe itu menjadi stylesheet.
CSS Modules juga tetap menjalankan peran build-time di framework tersebut jika didukung bundler. Sintaks untuk mapping hasil import dapat berbeda, tetapi invariant-nya sama: transform CSS menghasilkan selector dan mengekspor nama yang sesuai ke kode aplikasi.
Pilih tool berdasarkan layer yang perlu dikendalikan
Tool yang dibahas di sini menempati empat layer berbeda:
| Tool | Output utama | Membuat CSS rule? | Peran umum |
|---|---|---|---|
| `clsx` | string class | Tidak | komposisi class kondisional |
| `tailwind-variants` | string class Tailwind | Tidak | variant komponen bertipe dan penanganan konflik class |
| CSS Modules | scoped class mapping + transformed CSS | Ya | scoping selector lokal dan penamaan class saat build |
| StyleX | generated references + atomic CSS | Ya | styling aplikasi berbasis compiler |
Pertanyaan arsitekturalnya bukan apakah sebuah tool dapat menghasilkan nama class yang terlihat aneh. Pertanyaannya adalah tahap mana yang memiliki CSS rule.
Jika HTML harus merujuk class semantik yang stabil, tulis atau generate selector itu.
Jika state komponen perlu memilih utility Tailwind yang sudah ada, variant recipe cocok digunakan.
Jika nama class harus di-scope atau di-hash sekaligus tetap sinkron dengan CSS, gunakan transform build-time seperti CSS Modules.
Jika style ingin ditulis di JavaScript lalu diekstrak menjadi atomic CSS, gunakan sistem berbasis compiler seperti StyleX.
Pemisahan layer ini mencegah satu kesalahan kategori yang umum: mengharapkan utility penyusun string class berperilaku seperti CSS compiler.