DevToolBoxGRATIS
Blog

Pola Komponen Tailwind CSS: Membangun UI yang Dapat Digunakan Kembali di 2026

14 menitoleh DevToolBox

Pendekatan utility-first Tailwind CSS dapat menyebabkan JSX yang membengkak jika tidak diatur dengan benar.

class-variance-authority (cva): Varian Bertipe

cva adalah cara standar untuk mengelola varian komponen di Tailwind.

// Using class-variance-authority (cva) for type-safe variants
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';

const buttonVariants = cva(
    // Base classes applied to every variant
    'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50',
    {
        variants: {
            variant: {
                default: 'bg-primary text-primary-foreground hover:bg-primary/90',
                destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
                outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
                ghost: 'hover:bg-accent hover:text-accent-foreground',
                link: 'text-primary underline-offset-4 hover:underline',
            },
            size: {
                default: 'h-10 px-4 py-2',
                sm: 'h-9 rounded-md px-3',
                lg: 'h-11 rounded-md px-8',
                icon: 'h-10 w-10',
            },
        },
        defaultVariants: {
            variant: 'default',
            size: 'default',
        },
    }
);

interface ButtonProps
    extends React.ButtonHTMLAttributes<HTMLButtonElement>,
        VariantProps<typeof buttonVariants> {}

export function Button({ className, variant, size, ...props }: ButtonProps) {
    return (
        <button
            className={cn(buttonVariants({ variant, size, className }))}
            {...props}
        />
    );
}

// Usage:
// <Button variant="outline" size="sm">Click me</Button>
// <Button variant="destructive">Delete</Button>

Kapan Menggunakan @apply

@apply mengekstrak kombinasi kelas Tailwind yang berulang ke kelas CSS.

/* globals.css — using @apply to extract reusable patterns */
@layer components {
    /* Card component pattern */
    .card {
        @apply rounded-lg border bg-card text-card-foreground shadow-sm;
    }

    .card-header {
        @apply flex flex-col space-y-1.5 p-6;
    }

    .card-title {
        @apply text-2xl font-semibold leading-none tracking-tight;
    }

    .card-content {
        @apply p-6 pt-0;
    }

    /* Form field pattern */
    .form-label {
        @apply text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70;
    }

    .form-input {
        @apply flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm
               ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium
               placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2
               focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50;
    }

    /* Badge pattern */
    .badge {
        @apply inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold
               transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2;
    }
}

/* Use sparingly — prefer utility classes in JSX for maintainability */

Utilitas cn(): Menggabungkan Kelas dengan Aman

clsx menangani kelas kondisional, tailwind-merge menyelesaikan konflik Tailwind.

// lib/utils.ts — the cn() helper (used everywhere in shadcn/ui)
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';

// clsx: conditionally join class names
// twMerge: intelligently merge Tailwind classes (handles conflicts)
export function cn(...inputs: ClassValue[]) {
    return twMerge(clsx(inputs));
}

// Examples:
cn('px-4 py-2', 'px-6')           // 'py-2 px-6' (px-4 overridden)
cn('text-red-500', isActive && 'text-blue-500')  // conditional
cn({ 'opacity-50': disabled, 'cursor-not-allowed': disabled })

Headless UI: Komponen Interaktif yang Dapat Diakses

Headless UI menyediakan komponen interaktif yang sepenuhnya dapat diakses tanpa gaya.

// Using Headless UI (Tailwind Labs) for accessible components
import { Listbox, Transition } from '@headlessui/react';
import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid';
import { Fragment, useState } from 'react';

const people = [
    { id: 1, name: 'Durward Reynolds' },
    { id: 2, name: 'Kenton Towne' },
    { id: 3, name: 'Therese Wunsch' },
];

export function Select() {
    const [selected, setSelected] = useState(people[0]);

    return (
        <Listbox value={selected} onChange={setSelected}>
            <Listbox.Button className="relative w-full cursor-default rounded-lg bg-white py-2 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 sm:text-sm">
                <span className="block truncate">{selected.name}</span>
                <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
                    <ChevronUpDownIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
                </span>
            </Listbox.Button>
            <Transition as={Fragment} leave="transition ease-in duration-100" leaveFrom="opacity-100" leaveTo="opacity-0">
                <Listbox.Options className="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
                    {people.map((person) => (
                        <Listbox.Option key={person.id} value={person} className={({ active }) => cn('relative cursor-default select-none py-2 pl-10 pr-4', active ? 'bg-amber-100 text-amber-900' : 'text-gray-900')}>
                            {({ selected }) => (
                                <>
                                    <span className={cn('block truncate', selected ? 'font-medium' : 'font-normal')}>{person.name}</span>
                                    {selected ? <span className="absolute inset-y-0 left-0 flex items-center pl-3 text-amber-600"><CheckIcon className="h-5 w-5" /></span> : null}
                                </>
                            )}
                        </Listbox.Option>
                    ))}
                </Listbox.Options>
            </Transition>
        </Listbox>
    );
}

Konfigurasi Tailwind: Token Desain dan Tema

Memperluas Tailwind dengan variabel CSS memungkinkan pergantian tema tanpa JavaScript.

// tailwind.config.ts — extend with design tokens
import type { Config } from 'tailwindcss';

const config: Config = {
    darkMode: ['class'],
    content: ['./src/**/*.{ts,tsx}'],
    theme: {
        extend: {
            colors: {
                // CSS variable-based colors for theming
                border: 'hsl(var(--border))',
                input: 'hsl(var(--input))',
                ring: 'hsl(var(--ring))',
                background: 'hsl(var(--background))',
                foreground: 'hsl(var(--foreground))',
                primary: {
                    DEFAULT: 'hsl(var(--primary))',
                    foreground: 'hsl(var(--primary-foreground))',
                },
                destructive: {
                    DEFAULT: 'hsl(var(--destructive))',
                    foreground: 'hsl(var(--destructive-foreground))',
                },
            },
            borderRadius: {
                lg: 'var(--radius)',
                md: 'calc(var(--radius) - 2px)',
                sm: 'calc(var(--radius) - 4px)',
            },
            keyframes: {
                'accordion-down': {
                    from: { height: '0' },
                    to: { height: 'var(--radix-accordion-content-height)' },
                },
            },
            animation: {
                'accordion-down': 'accordion-down 0.2s ease-out',
            },
        },
    },
    plugins: [require('tailwindcss-animate')],
};

export default config;

Pendekatan Perpustakaan Komponen

LibraryApproachCode OwnershipThemingAccessibility
shadcn/uiCopy-paste + Radix + CVAYou own the codeCSS variablesRadix primitives
Chakra UIComponent librarynpm dependencyTheme objectBuilt-in
MUI (Material)Component librarynpm dependencyTheme providerBuilt-in
MantineComponent librarynpm dependencyCSS variablesBuilt-in
DaisyUITailwind pluginConfig onlyCSS variablesManual

Praktik Terbaik

  • Gunakan cva untuk setiap komponen dengan 2+ varian.
  • Selalu gunakan cn() untuk menggabungkan kelas.
  • Hindari @apply untuk komponen.
  • Token desain via variabel CSS untuk dark mode.
  • Untuk dropdown, modal — gunakan Headless UI atau Radix UI.

Pertanyaan yang Sering Diajukan

Tailwind atau CSS-in-JS?

Di 2026, Tailwind + CVA + shadcn/ui adalah pola dominan untuk proyek React baru.

Apa itu shadcn/ui?

shadcn/ui adalah kumpulan komponen copy-paste yang dibangun dengan Radix UI + Tailwind + CVA.

Bagaimana menangani dark mode dengan Tailwind?

Tambahkan darkMode: "class" ke tailwind.config.ts dan gunakan prefiks dark:.

Bagaimana mencegah purging kelas Tailwind di produksi?

Konfigurasikan array content di tailwind.config.ts untuk menyertakan semua file.

Apakah Tailwind baik untuk sistem desain?

Ya, tetapi membutuhkan disiplin. Gunakan token desain (variabel CSS).

Alat Terkait

𝕏 Twitterin LinkedIn
Apakah ini membantu?

Tetap Update

Dapatkan tips dev mingguan dan tool baru.

Tanpa spam. Berhenti kapan saja.

Coba Alat Terkait

🌈CSS Gradient GeneratorCSS Triangle Generator🎨Color Picker Online

Artikel Terkait

CSS Grid Mastery: Panduan Lengkap dengan Contoh Nyata 2026

Kuasai CSS Grid Layout 2026: grid-template areas, penempatan otomatis, subgrid dan layout responsif.

Pola React Query 2026: Data Fetching, Caching, dan Mutations dengan TanStack Query

Kuasai pola React Query (TanStack Query) 2026: useQuery, useMutation, pembaruan optimis.