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

DirectiveFungsi
@import "tailwindcss"Memuat Tailwind (theme default, preflight, utilities)
@theme { ... }Mendefinisikan design token → otomatis jadi utility
@custom-variantMembuat variant baru (atau menimpa dark)
@utilityMembuat utility kustom yang mendukung variant
@layer componentsClass komponen yang tetap bisa ditimpa utility
@applyMenyisipkan utility ke dalam CSS biasa
@variantMemakai variant di dalam CSS biasa
@sourceMenambah/mengecualikan file yang dipindai
@pluginMemuat plugin JavaScript
@referenceMengakses theme tanpa menduplikasi CSS (untuk <style> di komponen Vue/Svelte)
@configMemuat 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;
}
NamespaceContoh variableUtility yang lahir
--color-*--color-cream-100bg-cream-100, text-cream-100, border-cream-100, …
--font-*--font-headingfont-heading
--text-*--text-hugetext-huge
--breakpoint-*--breakpoint-3xlvariant 3xl:
--spacing--spacing: 0.25remSkala p-*, m-*, gap-*, w-*
--radius-*--radius-cardrounded-card
--shadow-*--shadow-softshadow-soft
--animate-*--animate-wiggleanimate-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:

FungsiContohHasil
--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>
KebutuhanPakai
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

  1. Menaruh token di :root lalu berharap utility muncul. Hanya variable di @theme yang menghasilkan class.
  2. Namespace salah. --brand-500 tidak menghasilkan bg-brand-500; harus --color-brand-500.
  3. Lupa @custom-variant dark saat pakai toggle. dark: akan tetap mengikuti setting OS, sehingga tombol toggle seolah tidak berfungsi.
  4. Script tema diletakkan di akhir <body>. Halaman sempat tampil terang lalu berkedip gelap (FOUC). Taruh inline di <head>.
  5. @apply di mana-mana. Jika setiap elemen dibungkus class @apply, kita kembali ke CSS tradisional dengan langkah tambahan. Utamakan komponen template.
  6. Mengharapkan hover:card bekerja. Class di @layer components tidak mendukung variant; gunakan @utility.
  7. @apply di <style> komponen tanpa @reference. Di Vue/Svelte/CSS modules, tambahkan @reference "../app.css"; agar token dan utility kustom dikenali.

Ringkasan

  • Di v4, konfigurasi = CSS: @theme untuk token, namespace menentukan utility.
  • @custom-variant dark (&:where(.dark, .dark *)) untuk dark mode berbasis class.
  • @layer components + @apply untuk pola berulang; @utility jika 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.

Comments