Konfigurasi CSS-first, integrasi @layer, container queries bawaan, performa engine baru, breaking changes, dan pengalaman migrasi jujur saya dari v3 ke v4.
Saya sudah menggunakan Tailwind CSS sejak v1.x, saat setengah komunitas menganggapnya kekejian dan setengah lainnya tidak bisa berhenti shipping dengannya. Setiap versi mayor merupakan lompatan signifikan, tapi v4 berbeda. Ini bukan sekadar feature release. Ini adalah penulisan ulang arsitektur dari dasar yang mengubah kontrak fundamental antara Anda dan framework-nya.
Setelah melakukan migrasi dua proyek produksi dari v3 ke v4 dan memulai tiga proyek baru dari nol di v4, saya punya gambaran jelas tentang apa yang benar-benar lebih baik, apa yang masih kasar, dan apakah Anda harus migrasi sekarang. Tanpa hype, tanpa kemarahan — hanya apa yang saya amati.
Tailwind CSS v4 adalah tiga hal sekaligus:
tailwind.config.js sebagai default@layer, container queries, , dan cascade layers menjadi first-class citizens@starting-styleHeadline yang akan Anda lihat di mana-mana adalah "10x lebih cepat." Itu nyata, tapi itu meremehkan perubahan sesungguhnya. Mental model untuk mengkonfigurasi dan memperluas Tailwind telah bergeser secara fundamental. Anda bekerja dengan CSS sekarang, bukan objek konfigurasi JavaScript yang menghasilkan CSS.
Berikut tampilan setup Tailwind v4 yang minimal:
/* app.css — ini seluruh setup-nya */
@import "tailwindcss";Itu saja. Tanpa file konfigurasi. Tanpa konfigurasi plugin PostCSS (untuk kebanyakan setup). Tanpa directive @tailwind base; @tailwind components; @tailwind utilities;. Satu import, dan Anda sudah jalan.
Bandingkan dengan v3:
/* v3 — app.css */
@tailwind base;
@tailwind components;
@tailwind utilities;// v3 — tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"],
theme: {
extend: {},
},
plugins: [],
};// v3 — postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};Tiga file dikurangi menjadi satu baris. Ini bukan sekadar lebih sedikit boilerplate — ini lebih sedikit area permukaan untuk miskonfigurasi. Di v4, deteksi konten bersifat otomatis. Ia memindai file proyek Anda tanpa perlu Anda menuliskan pola glob.
Ini adalah pergeseran konseptual terbesar. Di v3, Anda menyesuaikan Tailwind melalui objek konfigurasi JavaScript:
// v3 — tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
brand: {
50: "#eff6ff",
500: "#3b82f6",
900: "#1e3a5f",
},
},
fontFamily: {
display: ["Inter Variable", "sans-serif"],
},
spacing: {
18: "4.5rem",
112: "28rem",
},
borderRadius: {
"4xl": "2rem",
},
},
},
};Di v4, semua ini berada di CSS menggunakan directive @theme:
@import "tailwindcss";
@theme {
--color-brand-50: #eff6ff;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a5f;
--font-display: "Inter Variable", sans-serif;
--spacing-18: 4.5rem;
--spacing-112: 28rem;
--radius-4xl: 2rem;
}Awalnya, saya menolak ini. Saya suka punya satu objek JavaScript di mana saya bisa melihat seluruh design system saya. Tapi setelah seminggu dengan pendekatan CSS, saya berubah pikiran karena tiga alasan:
1. CSS custom properties native terekspos secara otomatis. Setiap nilai yang Anda definisikan di @theme menjadi CSS custom property di :root. Itu berarti nilai tema Anda dapat diakses di CSS biasa, di CSS Modules, di tag <style>, di mana pun CSS berjalan:
/* Anda mendapatkan ini secara gratis */
:root {
--color-brand-50: #eff6ff;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a5f;
}/* gunakan di mana saja — tanpa perlu Tailwind */
.custom-element {
border: 2px solid var(--color-brand-500);
}2. Anda bisa menggunakan fitur CSS di dalam @theme. Media queries, light-dark(), calc() — CSS asli bekerja di sini karena memang ini CSS asli:
@theme {
--color-surface: light-dark(#ffffff, #0a0a0a);
--color-text: light-dark(#0a0a0a, #fafafa);
--spacing-container: calc(100vw - 2rem);
}3. Co-location dengan CSS Anda yang lain. Tema, custom utilities, dan base styles Anda semuanya hidup dalam bahasa yang sama, file yang sama jika Anda mau. Tidak ada perpindahan konteks antara "dunia CSS" dan "dunia konfigurasi JavaScript."
Di v3 Anda punya theme (mengganti) vs theme.extend (menggabungkan). Di v4, mental model-nya berbeda:
@import "tailwindcss";
/* Ini MEMPERLUAS tema default — menambahkan warna brand di samping yang sudah ada */
@theme {
--color-brand-500: #3b82f6;
}Jika Anda ingin mengganti sebuah namespace sepenuhnya (seperti menghapus semua warna default), Anda menggunakan @theme dengan wildcard reset --color-*:
@import "tailwindcss";
@theme {
/* Hapus semua warna default terlebih dahulu */
--color-*: initial;
/* Sekarang definisikan hanya warna Anda */
--color-white: #ffffff;
--color-black: #000000;
--color-brand-50: #eff6ff;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a5f;
}Pola wildcard reset ini elegan. Anda memilih dengan tepat bagian mana dari tema default yang akan dipertahankan dan mana yang akan diganti. Ingin semua spacing default tapi warna custom? Reset --color-*: initial; dan biarkan spacing apa adanya.
Untuk proyek yang lebih besar, Anda bisa membagi tema di beberapa file:
/* styles/theme/colors.css */
@theme {
--color-brand-50: #eff6ff;
--color-brand-100: #dbeafe;
--color-brand-200: #bfdbfe;
--color-brand-300: #93c5fd;
--color-brand-400: #60a5fa;
--color-brand-500: #3b82f6;
--color-brand-600: #2563eb;
--color-brand-700: #1d4ed8;
--color-brand-800: #1e40af;
--color-brand-900: #1e3a5f;
--color-brand-950: #172554;
}
/* styles/theme/typography.css */
@theme {
--font-display: "Inter Variable", sans-serif;
--font-body: "Source Sans 3 Variable", sans-serif;
--font-mono: "JetBrains Mono Variable", monospace;
--text-display: 3.5rem;
--text-display--line-height: 1.1;
--text-display--letter-spacing: -0.02em;
}/* app.css */
@import "tailwindcss";
@import "./theme/colors.css";
@import "./theme/typography.css";Ini jauh lebih bersih daripada pola v3 yang memiliki tailwind.config.js raksasa atau mencoba memecahnya dengan require().
Engine Tailwind v4 adalah penulisan ulang lengkap dalam Rust. Mereka menyebutnya Oxide. Saya skeptis terhadap klaim "10x lebih cepat" — angka marketing jarang bertahan saat menghadapi proyek nyata. Jadi saya melakukan benchmark sendiri.
Proyek uji saya: Aplikasi Next.js dengan 847 komponen, 142 halaman, sekitar 23.000 penggunaan class Tailwind.
| Metrik | v3 (Node) | v4 (Oxide) | Peningkatan |
|---|---|---|---|
| Build awal | 4.280ms | 387ms | 11x |
| Incremental (edit 1 file) | 340ms | 18ms | 19x |
| Full rebuild (bersih) | 5.100ms | 510ms | 10x |
| Dev server start | 3.200ms | 290ms | 11x |
Klaim "10x" itu konservatif untuk proyek saya. Incremental builds adalah tempat ia benar-benar bersinar — 18ms berarti pada dasarnya instan. Anda menyimpan file dan browser sudah punya style baru sebelum Anda sempat berpindah tab.
Tiga alasan:
1. Rust alih-alih JavaScript. CSS parser inti, deteksi class, dan code generation semuanya native Rust. Ini bukan situasi "mari tulis ulang di Rust untuk bersenang-senang" — CSS parsing benar-benar pekerjaan CPU-bound di mana native code punya keunggulan masif dibandingkan V8.
2. Tidak ada PostCSS di hot path. Di v3, Tailwind adalah plugin PostCSS. Setiap build berarti: parse CSS menjadi PostCSS AST, jalankan plugin Tailwind, serialisasi kembali ke string CSS, lalu plugin PostCSS lainnya berjalan. Di v4, Tailwind punya CSS parser sendiri yang langsung dari source ke output. PostCSS masih didukung untuk kompatibilitas, tapi jalur utama melewatinya sepenuhnya.
3. Pemrosesan incremental yang lebih cerdas. Engine baru melakukan caching secara agresif. Saat Anda mengedit satu file, ia hanya memindai ulang file itu untuk nama class dan hanya menghasilkan ulang aturan CSS yang berubah. Engine v3 lebih cerdas dalam hal ini daripada yang orang kira (mode JIT sudah incremental), tapi v4 membawanya jauh lebih jauh dengan pelacakan dependensi yang detail.
Ya, tapi bukan karena alasan yang Anda duga. Untuk kebanyakan proyek, kecepatan build v3 "cukup baik." Anda menunggu beberapa ratus milidetik saat development. Tidak menyakitkan.
Kecepatan v4 penting karena membuat Tailwind tidak terlihat di toolchain Anda. Ketika build di bawah 20ms, Anda berhenti memikirkan Tailwind sebagai langkah build sama sekali. Ia menjadi seperti syntax highlighting — selalu ada, tidak pernah menghalangi. Perbedaan psikologis itu signifikan selama satu hari penuh development.
Di v3, Tailwind menggunakan sistem layer-nya sendiri dengan @layer base, @layer components, dan @layer utilities. Ini terlihat seperti CSS cascade layers tapi bukan — mereka adalah directive khusus Tailwind yang mengontrol di mana CSS yang dihasilkan muncul dalam output.
Di v4, Tailwind menggunakan CSS cascade layers yang sesungguhnya:
/* output v4 — disederhanakan */
@layer theme, base, components, utilities;
@layer base {
/* reset, preflight */
}
@layer components {
/* class komponen Anda */
}
@layer utilities {
/* semua class utility yang dihasilkan */
}Ini adalah perubahan signifikan karena CSS cascade layers memiliki implikasi specificity yang nyata. Aturan di layer berprioritas lebih rendah selalu kalah dari aturan di layer berprioritas lebih tinggi, terlepas dari specificity selector. Itu berarti:
@layer components {
/* specificity: 0-1-0 */
.card {
padding: 1rem;
}
}
@layer utilities {
/* specificity: 0-1-0 — specificity sama tapi menang karena layer utilities lebih belakang */
.p-4 {
padding: 1rem;
}
}Utilities selalu meng-override components. Components selalu meng-override base. Beginilah cara Tailwind bekerja secara konseptual di v3, tapi sekarang ditegakkan oleh mekanisme cascade layer browser, bukan oleh manipulasi urutan source.
Di v3, Anda mendefinisikan custom utilities dengan plugin API atau @layer utilities:
// v3 — pendekatan plugin
const plugin = require("tailwindcss/plugin");
module.exports = {
plugins: [
plugin(function ({ addUtilities }) {
addUtilities({
".text-balance": {
"text-wrap": "balance",
},
".text-pretty": {
"text-wrap": "pretty",
},
});
}),
],
};Di v4, custom utilities didefinisikan dengan directive @utility:
@import "tailwindcss";
@utility text-balance {
text-wrap: balance;
}
@utility text-pretty {
text-wrap: pretty;
}Directive @utility memberi tahu Tailwind "ini adalah class utility — letakkan di layer utilities dan izinkan digunakan dengan variants." Bagian terakhir itu kunci. Utility yang didefinisikan dengan @utility secara otomatis bekerja dengan hover:, focus:, md:, dan setiap variant lainnya:
<p class="text-pretty md:text-balance">...</p>Anda juga bisa mendefinisikan custom variants dengan @variant:
@import "tailwindcss";
@variant hocus (&:hover, &:focus);
@variant theme-dark (.dark &);<button class="hocus:bg-brand-500 theme-dark:text-white">Klik saya</button>Ini menggantikan plugin API v3 addVariant untuk kebanyakan kasus penggunaan. Ini kurang powerful (Anda tidak bisa melakukan generasi variant programatik), tapi mencakup 90% dari apa yang sebenarnya orang lakukan.
Container queries adalah salah satu fitur yang paling banyak diminta di v3. Anda bisa mendapatkannya dengan plugin @tailwindcss/container-queries, tapi itu adalah add-on. Di v4, mereka sudah built-in ke dalam framework.
Tandai sebuah container dengan @container dan query ukurannya dengan prefix @:
<!-- tandai parent sebagai container -->
<div class="@container">
<!-- responsif terhadap lebar parent, bukan viewport -->
<div class="flex flex-col @md:flex-row @lg:grid @lg:grid-cols-3">
<div class="p-4">Card 1</div>
<div class="p-4">Card 2</div>
<div class="p-4">Card 3</div>
</div>
</div>Variant @md, @lg, dll. bekerja seperti responsive breakpoints tapi relatif terhadap ancestor @container terdekat, bukan viewport. Nilai breakpoint-nya sesuai dengan breakpoints default Tailwind:
| Variant | Min-width |
|---|---|
@sm | 24rem (384px) |
@md | 28rem (448px) |
@lg | 32rem (512px) |
@xl | 36rem (576px) |
@2xl | 42rem (672px) |
Anda bisa memberi nama containers untuk melakukan query ancestor tertentu:
<div class="@container/sidebar">
<div class="@container/card">
<!-- query container card -->
<div class="@md/card:text-lg">...</div>
<!-- query container sidebar -->
<div class="@lg/sidebar:hidden">...</div>
</div>
</div>Container queries mengubah cara Anda berpikir tentang responsive design. Alih-alih "pada lebar viewport ini, tampilkan tiga kolom," Anda berkata "ketika container komponen ini cukup lebar, tampilkan tiga kolom." Komponen menjadi benar-benar self-contained. Anda bisa memindahkan komponen card dari layout full-width ke sidebar dan ia otomatis menyesuaikan. Tanpa akrobat media query.
Saya sudah melakukan refactoring component libraries saya untuk menggunakan container queries secara default alih-alih viewport breakpoints. Hasilnya adalah komponen yang bekerja di mana pun Anda letakkan, tanpa parent perlu tahu apa pun tentang perilaku responsif komponen tersebut.
<!-- Komponen ini menyesuaikan diri dengan container APA PUN tempat ia diletakkan -->
<article class="@container">
<div class="grid grid-cols-1 @md:grid-cols-[200px_1fr] gap-4">
<img
class="w-full @md:w-auto rounded-lg aspect-video @md:aspect-square object-cover"
src="/post-image.jpg"
alt=""
/>
<div>
<h2 class="text-lg @lg:text-xl font-semibold">Judul Post</h2>
<p class="mt-2 text-sm @md:text-base text-gray-600">Kutipan post di sini...</p>
<div class="mt-4 hidden @md:flex gap-2">
<span class="text-xs bg-gray-100 px-2 py-1 rounded">Tag</span>
</div>
</div>
</div>
</article>v4 menambahkan beberapa variant baru yang terus saya gunakan. Mereka mengisi gap yang nyata.
starting:Ini memetakan ke CSS @starting-style, yang memungkinkan Anda mendefinisikan state awal sebuah elemen saat pertama kali muncul. Ini adalah potongan puzzle yang hilang untuk menganimasikan kemunculan elemen tanpa JavaScript:
<dialog class="opacity-0 starting:opacity-0 open:opacity-100 transition-opacity duration-300">
<p>Dialog ini fade in saat dibuka</p>
</dialog>Variant starting: menghasilkan CSS di dalam blok @starting-style:
/* yang dihasilkan Tailwind */
@starting-style {
dialog[open] {
opacity: 0;
}
}
dialog[open] {
opacity: 1;
transition: opacity 300ms;
}Ini sangat besar untuk dialog, popover, dropdown menu — apa pun yang butuh animasi masuk. Sebelumnya, Anda memerlukan JavaScript untuk menambahkan class di frame berikutnya, atau Anda menggunakan @keyframes. Sekarang cukup sebuah utility class.
not-*Negasi. Sesuatu yang sudah lama kita inginkan:
<!-- setiap child kecuali yang terakhir mendapat border -->
<div class="divide-y">
<div class="not-last:pb-4">Item 1</div>
<div class="not-last:pb-4">Item 2</div>
<div class="not-last:pb-4">Item 3</div>
</div>
<!-- style semua yang tidak disabled -->
<input class="not-disabled:hover:border-brand-500" />
<!-- negasi data attributes -->
<div class="not-data-active:opacity-50">...</div>nth-*Akses langsung nth-child dan nth-of-type:
<ul>
<li class="nth-1:font-bold">Item pertama — bold</li>
<li class="nth-even:bg-gray-50">Baris genap — bg abu-abu</li>
<li class="nth-odd:bg-white">Baris ganjil — bg putih</li>
<li class="nth-[3n+1]:text-brand-500">Setiap ketiga+1 — warna brand</li>
</ul>Sintaks bracket (nth-[3n+1]) mendukung ekspresi nth-child yang valid apa pun. Ini menggantikan banyak custom CSS yang biasa saya tulis untuk table striping dan pola grid.
in-* (State Parent)Ini adalah kebalikan dari group-*. Alih-alih "ketika parent (group) saya di-hover, style saya," ini adalah "ketika saya berada di dalam parent yang cocok dengan state ini, style saya":
<div class="in-data-active:bg-brand-50">Ini mendapat background ketika ancestor mana pun memiliki data-active</div>**:Style semua descendant, bukan hanya direct children. Ini kekuatan yang terkontrol — gunakan dengan hemat, tapi sangat berharga untuk konten prose dan output CMS:
<!-- semua paragraf di dalam div ini, di kedalaman apa pun -->
<div class="**:data-highlight:bg-yellow-100">
<section>
<p data-highlight>Ini mendapat highlight</p>
<div>
<p data-highlight>Ini juga, bersarang lebih dalam</p>
</div>
</section>
</div>Izinkan saya terus terang. Jika Anda punya proyek v3 yang besar, migrasi tidaklah sepele. Berikut apa yang rusak di proyek saya:
tailwind.config.js Anda tidak langsung berfungsi. Anda perlu:
@theme (direkomendasikan untuk arsitektur baru)@config (jalur migrasi cepat)/* migrasi cepat — pertahankan config lama Anda */
@import "tailwindcss";
@config "../../tailwind.config.js";Bridge @config ini berfungsi, tapi secara eksplisit merupakan alat migrasi. Rekomendasinya adalah pindah ke @theme seiring waktu.
Beberapa utility yang deprecated di v3 sudah hilang:
/* DIHAPUS di v4 */
bg-opacity-* → gunakan bg-black/50 (sintaks slash opacity)
text-opacity-* → gunakan text-black/50
border-opacity-* → gunakan border-black/50
flex-shrink-* → gunakan shrink-*
flex-grow-* → gunakan grow-*
overflow-ellipsis → gunakan text-ellipsis
decoration-slice → gunakan box-decoration-slice
decoration-clone → gunakan box-decoration-clone
Jika Anda sudah menggunakan sintaks modern di v3 (slash opacity, shrink-*), Anda aman. Jika belum, ini adalah perubahan find-and-replace yang straightforward.
Palet warna default bergeser sedikit. Jika Anda bergantung pada nilai warna persis dari v3 (bukan berdasarkan nama tapi berdasarkan nilai hex yang sebenarnya), Anda mungkin melihat perbedaan visual. Warna bernama (blue-500, gray-200) masih ada tapi beberapa nilai hex berubah.
v3 memerlukan konfigurasi content secara eksplisit:
// v3
module.exports = {
content: ["./src/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}"],
};v4 menggunakan deteksi konten otomatis. Ia memindai root proyek Anda dan menemukan file template secara otomatis. Ini kebanyakan "langsung berfungsi," tapi jika Anda memiliki struktur proyek yang tidak biasa (monorepo dengan package di luar root proyek, file template di lokasi tak terduga), Anda mungkin perlu mengkonfigurasi jalur source secara eksplisit:
@import "tailwindcss";
@source "../shared-components/**/*.tsx";
@source "../design-system/src/**/*.tsx";Jika Anda menulis custom plugin, API-nya berubah. Fungsi addUtilities, addComponents, addBase, dan addVariant masih berfungsi melalui compatibility layer, tapi pendekatan idiomatis v4 adalah CSS-native:
// v3 plugin
plugin(function ({ addUtilities, theme }) {
addUtilities({
".scrollbar-hide": {
"-ms-overflow-style": "none",
"scrollbar-width": "none",
"&::-webkit-scrollbar": {
display: "none",
},
},
});
});/* v4 — cukup CSS */
@utility scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}Kebanyakan plugin first-party (@tailwindcss/typography, @tailwindcss/forms, @tailwindcss/aspect-ratio) punya versi kompatibel v4. Plugin pihak ketiga ada yang mendukung ada yang tidak — cek repo mereka sebelum migrasi.
Di v3, Anda bisa memilih keluar dari mode JIT (meskipun hampir tidak ada yang melakukannya). Di v4, tidak ada mode non-JIT. Semuanya dihasilkan sesuai permintaan, selalu. Jika Anda punya alasan untuk menggunakan engine AOT (ahead-of-time) yang lama, jalur itu sudah tidak ada.
Beberapa variant diganti nama atau berubah perilaku:
<!-- v3 -->
<div class="[&>*]:p-4">...</div>
<!-- v4 — bagian >* sekarang menggunakan sintaks inset variant -->
<div class="*:p-4">...</div>Sintaks arbitrary variant [&...] masih berfungsi, tapi v4 menyediakan alternatif bernama untuk pola umum.
Berikut cara saya benar-benar melakukan migrasi, bukan happy path dari dokumentasi tapi seperti apa proses sebenarnya.
Tailwind menyediakan codemod yang menangani kebanyakan perubahan mekanis:
npx @tailwindcss/upgradeIni melakukan banyak hal secara otomatis:
@tailwind ke @import "tailwindcss"bg-opacity-50 ke bg-black/50)@theme dasar dari konfigurasi Andatheme() di JavaScript)Untuk kebanyakan setup, Anda akan memperbarui konfigurasi PostCSS Anda:
// postcss.config.js — v4
module.exports = {
plugins: {
"@tailwindcss/postcss": {},
},
};Catatan: nama plugin berubah dari tailwindcss ke @tailwindcss/postcss. Jika Anda menggunakan Vite, Anda bisa melewati PostCSS sepenuhnya dan menggunakan plugin Vite:
// vite.config.ts
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [tailwindcss()],
});Ini adalah bagian manual. Ambil nilai tema tailwind.config.js Anda dan konversi ke @theme:
// v3 config — sebelum
module.exports = {
theme: {
extend: {
colors: {
brand: {
light: "#60a5fa",
DEFAULT: "#3b82f6",
dark: "#1d4ed8",
},
},
fontSize: {
"2xs": ["0.65rem", { lineHeight: "1rem" }],
},
animation: {
"fade-in": "fade-in 0.5s ease-out",
},
keyframes: {
"fade-in": {
"0%": { opacity: "0" },
"100%": { opacity: "1" },
},
},
},
},
};/* v4 CSS — sesudah */
@import "tailwindcss";
@theme {
--color-brand-light: #60a5fa;
--color-brand: #3b82f6;
--color-brand-dark: #1d4ed8;
--text-2xs: 0.65rem;
--text-2xs--line-height: 1rem;
--animate-fade-in: fade-in 0.5s ease-out;
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}Perhatikan bahwa keyframes pindah keluar dari @theme dan menjadi @keyframes CSS biasa. Nama animasi di @theme hanya mereferensikannya. Ini lebih bersih — keyframes adalah CSS, seharusnya ditulis sebagai CSS.
Ini tidak bisa ditawar. Setelah migrasi, saya membuka setiap halaman aplikasi saya dan memeriksanya secara visual. Saya juga menjalankan tes screenshot Playwright saya (jika Anda punya). Codemod-nya bagus tapi tidak sempurna. Hal-hal yang saya tangkap dalam review visual:
!important yang berperilaku berbeda dengan cascade layersPeriksa setiap package terkait Tailwind:
{
"@tailwindcss/typography": "^1.0.0",
"@tailwindcss/forms": "^1.0.0",
"@tailwindcss/container-queries": "HAPUS — sudah built-in sekarang",
"tailwindcss-animate": "cek dukungan v4",
"prettier-plugin-tailwindcss": "perbarui ke versi terbaru"
}Plugin @tailwindcss/container-queries tidak lagi diperlukan — container queries sudah built-in. Plugin lain memerlukan versi kompatibel v4 mereka.
Karena saya menggunakan Next.js untuk kebanyakan proyek, berikut setup spesifiknya.
Next.js menggunakan PostCSS di balik layar, jadi plugin PostCSS adalah pilihan yang natural:
npm install tailwindcss @tailwindcss/postcss// postcss.config.mjs
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};/* app/globals.css */
@import "tailwindcss";
@theme {
--font-sans: "Inter Variable", ui-sans-serif, system-ui, sans-serif;
--font-mono: "JetBrains Mono Variable", ui-monospace, monospace;
}Itu setup lengkapnya. Tanpa tailwind.config.js, tanpa autoprefixer (v4 menangani vendor prefixes secara internal).
Satu hal yang menjebak saya: urutan import CSS lebih penting di v4 karena cascade layers. @import "tailwindcss" Anda harus datang sebelum custom styles Anda:
/* urutan yang benar */
@import "tailwindcss";
@import "./theme.css";
@import "./custom-utilities.css";
/* inline @theme, @utility, dll. milik Anda */Jika Anda mengimport custom CSS sebelum Tailwind, style Anda mungkin berakhir di cascade layer yang lebih rendah dan di-override secara tak terduga.
Dark mode bekerja dengan cara yang sama secara konseptual tapi konfigurasinya pindah ke CSS:
@import "tailwindcss";
/* Gunakan dark mode berbasis class (default berbasis media) */
@variant dark (&:where(.dark, .dark *));Ini menggantikan konfigurasi v3:
// v3
module.exports = {
darkMode: "class",
};Pendekatan @variant lebih fleksibel. Anda bisa mendefinisikan dark mode sesuka Anda — berbasis class, berbasis data-attribute, atau berbasis media-query:
/* pendekatan data attribute */
@variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
/* media query — ini adalah default, jadi Anda tidak perlu mendeklarasikannya */
@variant dark (@media (prefers-color-scheme: dark));Jika Anda menggunakan Next.js dengan Turbopack (yang sekarang menjadi bundler dev default), v4 berfungsi dengan baik. Engine Rust cocok dengan arsitektur Turbopack yang juga berbasis Rust. Saya mengukur waktu startup dev:
| Setup | v3 + Webpack | v3 + Turbopack | v4 + Turbopack |
|---|---|---|---|
| Cold start | 4.8s | 2.1s | 1.3s |
| HMR (perubahan CSS) | 450ms | 180ms | 40ms |
40ms HMR untuk perubahan CSS nyaris tidak terasa. Rasanya instan.
Manfaat engine Oxide melampaui kecepatan build mentah.
v4 menggunakan memori yang jauh lebih sedikit. Di proyek 847-komponen saya:
| Metrik | v3 | v4 |
|---|---|---|
| Peak memory (build) | 380MB | 45MB |
| Steady-state (dev) | 210MB | 28MB |
Ini penting untuk pipeline CI/CD di mana memori terbatas, dan untuk mesin development yang menjalankan sepuluh proses secara bersamaan.
v4 menghasilkan output CSS yang sedikit lebih kecil karena engine baru lebih baik dalam deduplikasi dan dead code elimination:
Output v3: 34.2 KB (gzipped)
Output v4: 29.8 KB (gzipped)
Pengurangan 13% tanpa mengubah kode apa pun. Bukan transformatif, tapi performa gratis.
Di v4, jika Anda mendefinisikan nilai tema tapi tidak pernah menggunakannya di template, CSS custom property yang sesuai tetap dikeluarkan (ada di @theme, yang memetakan ke variabel :root). Namun, class utility untuk nilai yang tidak digunakan tidak dihasilkan. Ini sama dengan perilaku JIT v3 tapi perlu dicatat: CSS custom properties Anda selalu tersedia, bahkan untuk nilai tanpa penggunaan utility.
Jika Anda ingin mencegah nilai tema tertentu menghasilkan CSS custom properties, Anda bisa menggunakan @theme inline:
@theme inline {
/* Nilai-nilai ini menghasilkan utilities tapi BUKAN CSS custom properties */
--color-internal-debug: #ff00ff;
--spacing-magic-number: 3.7rem;
}Ini berguna untuk design token internal yang tidak ingin Anda ekspos sebagai variabel CSS.
Satu pola yang v4 buat jauh lebih mudah adalah theming multi-brand. Karena nilai tema adalah CSS custom properties, Anda bisa menukarnya saat runtime:
@import "tailwindcss";
@theme {
--color-brand: var(--brand-primary, #3b82f6);
--color-brand-light: var(--brand-light, #60a5fa);
--color-brand-dark: var(--brand-dark, #1d4ed8);
}
/* Override brand */
.theme-acme {
--brand-primary: #e11d48;
--brand-light: #fb7185;
--brand-dark: #9f1239;
}
.theme-globex {
--brand-primary: #059669;
--brand-light: #34d399;
--brand-dark: #047857;
}<body class="theme-acme">
<!-- semua bg-brand, text-brand, dll. menggunakan warna Acme -->
<div class="bg-brand text-white">Acme Corp</div>
</body>Di v3, ini memerlukan custom plugin atau setup variabel CSS yang kompleks di luar Tailwind. Di v4, ini natural — temanya adalah variabel CSS, dan variabel CSS mengalir melalui cascade. Inilah jenis hal yang membuat pendekatan CSS-first terasa tepat.
Izinkan saya bersikap seimbang. Ada hal-hal yang v3 lakukan yang benar-benar saya rindukan di v4:
1. Konfigurasi JavaScript untuk tema programatik. Saya punya proyek di mana kami menghasilkan skala warna dari satu warna brand menggunakan fungsi JavaScript di konfigurasi. Di v4, Anda tidak bisa melakukan itu di @theme — Anda perlu langkah build yang menghasilkan file CSS, atau Anda menghitung warnanya sekali dan menempelkannya. Compatibility layer @config membantu, tapi itu bukan cerita jangka panjang.
2. IntelliSense lebih baik saat peluncuran. Ekstensi VS Code v3 punya bertahun-tahun penyempurnaan. IntelliSense v4 berfungsi tapi punya beberapa gap di awal — nilai @theme custom kadang tidak autocomplete, dan definisi @utility tidak selalu terdeteksi. Ini sudah sangat membaik dengan update terbaru, tapi perlu dicatat.
3. Kematangan ekosistem. Ekosistem di sekitar v3 sangat besar. Headless UI, Radix, shadcn/ui, Flowbite, DaisyUI — semuanya diuji terhadap v3. Dukungan v4 sedang diluncurkan tapi belum universal. Saya harus mengirim PR ke satu component library untuk memperbaiki kompatibilitas v4.
Berikut kerangka keputusan saya setelah hidup dengan v4 selama beberapa minggu:
shrink-*, dll.)Untuk proyek baru, v4 adalah pilihan yang jelas. Konfigurasi CSS-first lebih bersih, engine-nya jauh lebih cepat, dan fitur baru (container queries, @starting-style, variant baru) benar-benar berguna.
Untuk proyek yang sudah ada, saya merekomendasikan pendekatan bertahap:
Migrasi tidak menyakitkan jika Anda mempersiapkannya. Codemod menangani 80% pekerjaan. 20% sisanya manual tapi straightforward. Anggarkan sehari untuk proyek menengah, dua sampai tiga hari untuk proyek besar.
Tailwind v4 adalah apa yang seharusnya Tailwind sejak awal. Konfigurasi JavaScript selalu merupakan kompromi terhadap tooling zamannya. Konfigurasi CSS-first, native cascade layers, engine Rust — ini bukan tren, ini framework yang mengejar ketertinggalan dari platform. Platform web menjadi lebih baik, dan Tailwind v4 memanfaatkannya alih-alih melawannya.
Langkah untuk menulis design token Anda di CSS, menyusunnya dengan fitur CSS, dan membiarkan cascade browser sendiri menangani specificity — itulah arah yang tepat. Butuh empat versi mayor untuk sampai di sini, tapi hasilnya adalah versi Tailwind yang paling koheren.
Mulai proyek berikutnya dengannya. Anda tidak akan menyesal.