Penataan
Pengaturan gaya kondisional dan komposabel dengan kelas Tailwind.
Perpustakaan komponen modern membutuhkan sistem penataan yang fleksibel yang dapat menangani kebutuhan kompleks tanpa mengorbankan pengalaman pengembang. Kombinasi Tailwind CSS dengan penggabungan kelas yang cerdas telah muncul sebagai pola yang kuat untuk membangun komponen yang dapat dikustomisasi.
Pendekatan ini menyelesaikan ketegangan fundamental antara menyediakan pengaturan bawaan yang masuk akal dan memungkinkan kustomisasi penuh - sebuah tantangan yang telah lama mengganggu pustaka komponen.
Masalah dengan penataan tradisional
Pendekatan CSS tradisional sering menyebabkan perang spesifisitas, konflik gaya, dan penimpaan yang tidak dapat diprediksi. Ketika Anda meneruskan className="bg-blue-500" ke sebuah komponen yang sudah memiliki bg-red-500, mana yang menang?
Tanpa penanganan yang tepat, kedua kelas tersebut akan berlaku dan hasilnya bergantung pada banyak faktor - urutan sumber CSS, spesifisitas kelas, algoritma penggabungan kelas bundler, dll.
Menggabungkan kelas secara cerdas
Perpustakaan tailwind-merge menyelesaikan ini dengan memahami struktur kelas Tailwind dan menyelesaikan konflik secara cerdas. Ketika dua kelas menargetkan properti CSS yang sama, ia hanya menyimpan yang terakhir.
// Both bg-red-500 and bg-blue-500 apply - unpredictable result
<Button className="bg-blue-500" />
// Renders: className="bg-red-500 bg-blue-500"import { twMerge } from 'tailwind-merge';
// bg-blue-500 wins as it comes last
const className = twMerge('bg-red-500', 'bg-blue-500');
// Returns: "bg-blue-500"Ini bekerja untuk semua utilitas Tailwind:
twMerge('px-4 py-2', 'px-8'); // Returns: "py-2 px-8"
twMerge('text-sm', 'text-lg'); // Returns: "text-lg"
twMerge('hover:bg-red-500', 'hover:bg-blue-500'); // Returns: "hover:bg-blue-500"Perpustakaan ini juga memahami sistem modifier Tailwind:
// Modifiers are handled correctly
twMerge('hover:bg-red-500 focus:bg-red-500', 'hover:bg-blue-500');
// Returns: "focus:bg-red-500 hover:bg-blue-500"Kelas kondisional
Seringkali Anda perlu menerapkan kelas secara kondisional berdasarkan props atau state. Perpustakaan clsx menyediakan API yang bersih untuk ini:
import clsx from 'clsx';
// Basic conditionals
clsx('base', isActive && 'active');
// Returns: "base active" (if isActive is true)
// Object syntax
clsx('base', {
'active': isActive,
'disabled': isDisabled,
});
// Arrays
clsx(['base', isLarge ? 'text-lg' : 'text-sm']);
// Mixed
clsx(
'base',
['array-item'],
{ 'object-conditional': true },
isActive && 'conditional'
);Polanya yang umum adalah menggabungkan sekumpulan kelas bawaan dengan props yang masuk, serta logika kustom apa pun yang kita miliki:
const Component = ({ className, ...props }: ComponentProps) => {
const [isOpen, setIsOpen] = useState(false);
return (
<div
className={cn(
"rounded-lg border bg-white shadow-sm",
isOpen && "bg-blue-500",
className
)}
{...props}
/>
);
};Fungsi utilitas cn
Fungsi cn, yang dipopulerkan oleh shadcn/ui, menggabungkan clsx dan tailwind-merge sehingga Anda mendapatkan logika kondisional dan penggabungan cerdas sekaligus:
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}Kekuatan datang dari pengurutan - gaya dasar terlebih dahulu, kondisi kedua, override pengguna terakhir. Ini memastikan perilaku yang dapat diprediksi sambil mempertahankan kustomisasi penuh.
Class Variance Authority (CVA)
Untuk komponen kompleks dengan banyak varian, mengelola kelas kondisional secara manual menjadi tidak praktis. Class Variance Authority (CVA) menyediakan API deklaratif untuk mendefinisikan varian komponen.
Sebagai contoh, berikut kutipan dari komponen Button dari shadcn/ui:
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)Praktik terbaik
1. Urutan itu penting
Selalu terapkan kelas dalam urutan ini:
- Gaya dasar (selalu diterapkan)
- Gaya varian (berdasarkan props)
- Gaya kondisional (berdasarkan state)
- Override pengguna (prop className)
className={cn(
'base-styles', // 1. Base
variant && variantStyles, // 2. Variants
isActive && 'active', // 3. Conditionals
className // 4. User overrides
)}2. Dokumentasikan varian Anda
Gunakan TypeScript dan JSDoc untuk mendokumentasikan apa yang dilakukan setiap varian:
type ButtonProps = {
/**
* The visual style of the button
* @default "primary"
*/
variant?: 'primary' | 'secondary' | 'destructive' | 'ghost';
/**
* The size of the button
* @default "md"
*/
size?: 'sm' | 'md' | 'lg';
};3. Ekstrak pola yang berulang
Jika Anda mendapati diri menulis logika kondisional yang sama berulang kali, ekstraklah:
export const focusRing = 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500';
export const disabled = 'disabled:pointer-events-none disabled:opacity-50';
// Use in components
className={cn(focusRing, disabled, className)}Panduan migrasi
Jika Anda bermigrasi dari pendekatan penataan yang berbeda, berikut cara mengadaptasi pola umum:
Dari CSS Modules
import styles from './Button.module.css';
<button className={`${styles.button} ${styles[variant]} ${className}`} />import { cn } from '@/lib/utils';
<button className={cn(
'px-4 py-2 rounded-lg',
variant === 'primary' && 'bg-blue-500 text-white',
className
)} />Dari styled-components
const Button = styled.button<{ $primary?: boolean }>`
padding: 8px 16px;
background: ${props => props.$primary ? 'blue' : 'gray'};
`;function Button({ primary, className, ...props }) {
return (
<button
className={cn(
'px-4 py-2',
primary ? 'bg-blue-500' : 'bg-gray-500',
className
)}
{...props}
/>
);
}Pertimbangan kinerja
Baik clsx maupun tailwind-merge sangat dioptimalkan, tetapi perhatikan tips berikut:
-
Definisikan varian di luar komponen - Varian CVA sebaiknya didefinisikan di luar komponen untuk menghindari pembuatan ulang setiap render.
-
Memoisasikan komputasi yang kompleks - Jika Anda memiliki logika kondisional yang mahal, pertimbangkan untuk mememoisasinya:
const className = useMemo(
() => cn(
baseStyles,
expensiveComputation(props),
className
),
[props, className]
);- Gunakan variabel CSS untuk nilai dinamis - Alih-alih menghasilkan kelas secara dinamis, gunakan variabel CSS:
// Good
<div
className="bg-[var(--color)]"
style={{ '--color': dynamicColor } as React.CSSProperties}
/>
// Avoid
<div className={`bg-[${dynamicColor}]`} />Kombinasi Tailwind CSS, penggabungan kelas yang cerdas, dan API varian memberikan fondasi yang kuat untuk penataan komponen. Pendekatan ini dapat diskalakan dari tombol sederhana hingga sistem desain kompleks sambil mempertahankan prediktabilitas dan pengalaman pengembang.