September 28, 2026
Kustomisasi Tailwind CSS v4.
Mengatur design system Tailwind v4 langsung dari CSS: design token dengan @theme, dark mode berbasis class dengan @custom-variant, class komponen via @layer components dan @apply, utility kustom dengan @utility, serta plugin lewat @plugin.
Di Tailwind v3, kustomisasi berarti mengedit tailwind.config.js. Di v4, semuanya pindah ke CSS: design token ditulis sebagai CSS variable di dalam @theme, variant baru dibuat dengan @custom-variant, dan plugin dimuat dengan @plugin. Hasilnya, satu file CSS menjadi sumber kebenaran design system.
Contoh nyata di catatan ini diambil dari situs ini sendiri — Hugo Portfolio & Blog
memakai Tailwind v4 dengan palet cream/ochre kustom di assets/css/main.css. Dasar-dasarnya ada di Dasar Tailwind CSS v4
.
1. Peta Directive v4
| Directive | Fungsi |
|---|---|
@import "tailwindcss" | Memuat Tailwind (theme default, preflight, utilities) |
@theme { ... } | Mendefinisikan design token → otomatis jadi utility |
@custom-variant | Membuat variant baru (atau menimpa dark) |
@utility | Membuat utility kustom yang mendukung variant |
@layer components | Class komponen yang tetap bisa ditimpa utility |
@apply | Menyisipkan utility ke dalam CSS biasa |
@variant | Memakai variant di dalam CSS biasa |
@source | Menambah/mengecualikan file yang dipindai |
@plugin | Memuat plugin JavaScript |
@reference | Mengakses theme tanpa menduplikasi CSS (untuk <style> di komponen Vue/Svelte) |
@config | Memuat tailwind.config.js lama (kompatibilitas v3) |
2. Design Token dengan @theme
Setiap variable di @theme punya namespace yang menentukan utility apa yang dihasilkan:
@import "tailwindcss";
@theme {
--font-heading: "Outfit", ui-sans-serif, sans-serif;
--color-cream-100: #f8f4e6;
--color-accent-800: #92400e;
--breakpoint-3xl: 120rem;
--radius-card: 0.875rem;
}
| Namespace | Contoh variable | Utility yang lahir |
|---|---|---|
--color-* | --color-cream-100 | bg-cream-100, text-cream-100, border-cream-100, … |
--font-* | --font-heading | font-heading |
--text-* | --text-huge | text-huge |
--breakpoint-* | --breakpoint-3xl | variant 3xl: |
--spacing | --spacing: 0.25rem | Skala p-*, m-*, gap-*, w-* |
--radius-* | --radius-card | rounded-card |
--shadow-* | --shadow-soft | shadow-soft |
--animate-* | --animate-wiggle | animate-wiggle |
Beda @theme dengan :root biasa: variable di @theme menghasilkan utility, sedangkan :root hanya variable CSS. Keduanya tetap dirender sebagai CSS variable, jadi bisa dipakai di CSS manual: color: var(--color-accent-800).
Contoh dari situs ini
Potongan asli assets/css/main.css:
@theme {
/* Typography */
--font-sans: "Inter", ui-sans-serif, -apple-system, BlinkMacSystemFont,
"Segoe UI", Helvetica, Arial, sans-serif;
--font-heading: "Outfit", "Inter", ui-sans-serif, -apple-system, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, "SFMono-Regular", Menlo,
monospace;
/* Warm paper surface */
--color-cream-50: #fbf8ee;
--color-cream-100: #f8f4e6; /* page background */
--color-cream-200: #f1ead5; /* cards / surfaces */
/* Ink (dark mode surfaces) */
--color-ink-800: #28282e;
--color-ink-900: #202025;
}
Menimpa --font-sans otomatis mengganti font default seluruh halaman, dan palet cream-*/ink-* langsung bisa dipakai sebagai bg-cream-100 dark:bg-ink-900.
Menghapus default
Untuk design system yang ketat (mis. hanya warna brand), reset seluruh namespace:
@theme {
--color-*: initial;
--color-white: #fff;
--color-brand-500: #b45309;
}
Sekarang bg-red-500 tidak lagi ada — hanya warna yang didefinisikan.
@theme inline
Jika token merujuk variable lain (mis. untuk theming runtime), pakai inline agar utility berisi nilai rujukannya, bukan variable perantara:
@theme inline {
--font-sans: var(--font-inter);
}
3. Dark Mode dengan @custom-variant
Default v4: dark: mengikuti prefers-color-scheme sistem. Untuk toggle manual, timpa variant dark:
/* berbasis class .dark di <html> — dipakai situs ini */
@custom-variant dark (&:where(.dark, .dark *));
/* alternatif: berbasis atribut */
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
:where() membuat spesifisitas selector tetap nol, sehingga dark: tidak “menang” secara tak terduga atas utility lain.
Script di <head> (inline, agar tidak berkedip saat load):
<script>
document.documentElement.classList.toggle(
"dark",
localStorage.theme === "dark" ||
(!("theme" in localStorage) &&
window.matchMedia("(prefers-color-scheme: dark)").matches)
);
</script>
Tombol toggle cukup mengubah localStorage.theme lalu toggle class dark lagi. Karakteristik localStorage (dan bedanya dengan cookie) dibahas di Autentikasi Sisi Klien
.
@custom-variant tidak terbatas untuk dark mode:
@custom-variant theme-sepia (&:where([data-theme="sepia"] *));
@custom-variant scrolled (&:where(.is-scrolled *));
<header class="bg-transparent scrolled:bg-white scrolled:shadow">...</header>
4. Class Komponen: @layer components + @apply
Untuk pola yang berulang di banyak template, buat class komponen:
@layer components {
.btn {
@apply inline-flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-sm font-semibold transition-all active:scale-95;
}
.card {
background-color: var(--color-cream-200);
border-radius: var(--radius-lg);
padding: --spacing(4);
}
}
Karena berada di layer components, utility tetap bisa menimpanya: <div class="card p-8"> → padding jadi p-8.
Situs ini memakai pola serupa untuk .btn-tania, .post-row, dan .kicker:
.kicker {
@apply font-mono text-xs uppercase tracking-[0.12em] text-zinc-500 dark:text-zinc-400;
}
Perhatikan bahwa @apply juga menerima variant (dark:, hover:) — termasuk variant kustom.
Fungsi bantu v4 yang berguna di CSS manual:
| Fungsi | Contoh | Hasil |
|---|---|---|
--spacing(n) | margin: --spacing(4) | calc(var(--spacing) * 4) |
--alpha(color / %) | color: --alpha(var(--color-lime-300) / 50%) | color-mix(...) dengan transparansi |
5. Utility Kustom dengan @utility
Class di @layer components tidak bisa dipakai dengan variant (hover:card tidak ada). Jika butuh itu, gunakan @utility:
@utility content-auto {
content-visibility: auto;
}
/* utility fungsional dengan nilai dari theme */
@theme {
--tab-size-2: 2;
--tab-size-4: 4;
}
@utility tab-* {
tab-size: --value(--tab-size-*, integer);
}
<div class="lg:content-auto">...</div>
<pre class="tab-4 md:tab-2">...</pre>
| Kebutuhan | Pakai |
|---|---|
| Satu-dua deklarasi, butuh variant | @utility |
| Blok gaya komponen, boleh ditimpa utility | @layer components |
| Gaya elemen dasar (h1, a, body) | @layer base |
6. Plugin
Plugin JavaScript dimuat dengan @plugin:
npm install -D @tailwindcss/typography
@import "tailwindcss";
@plugin "@tailwindcss/typography";
Situs ini memuat @tailwindcss/typography (class prose untuk konten Markdown) dan tailwind-scrollbar. Banyak fungsi yang dulu butuh plugin — container queries, aria-*, transform 3D — kini sudah bawaan v4, jadi periksa dokumentasi sebelum menambah dependency.
Kesalahan Umum
- Menaruh token di
:rootlalu berharap utility muncul. Hanya variable di@themeyang menghasilkan class. - Namespace salah.
--brand-500tidak menghasilkanbg-brand-500; harus--color-brand-500. - Lupa
@custom-variant darksaat pakai toggle.dark:akan tetap mengikuti setting OS, sehingga tombol toggle seolah tidak berfungsi. - Script tema diletakkan di akhir
<body>. Halaman sempat tampil terang lalu berkedip gelap (FOUC). Taruh inline di<head>. @applydi mana-mana. Jika setiap elemen dibungkus class@apply, kita kembali ke CSS tradisional dengan langkah tambahan. Utamakan komponen template.- Mengharapkan
hover:cardbekerja. Class di@layer componentstidak mendukung variant; gunakan@utility. @applydi<style>komponen tanpa@reference. Di Vue/Svelte/CSS modules, tambahkan@reference "../app.css";agar token dan utility kustom dikenali.
Ringkasan
- Di v4, konfigurasi = CSS:
@themeuntuk token, namespace menentukan utility. @custom-variant dark (&:where(.dark, .dark *))untuk dark mode berbasis class.@layer components+@applyuntuk pola berulang;@utilityjika butuh variant.- Plugin dimuat dengan
@plugin; banyak fitur plugin lama sudah bawaan.

Hey! I’m Fanny, the software engineer tending to this digital garden. You can read more about me, or subscribe by email.