Lompat ke konten
·19 menit membaca

Tailwind CSS v4: Apa yang Benar-Benar Berubah dan Apakah Perlu Migrasi

Konfigurasi CSS-first, integrasi @layer, container queries bawaan, performa engine baru, breaking changes, dan pengalaman migrasi jujur saya dari v3 ke v4.

Bagikan:X / TwitterLinkedIn

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.

Gambaran Besar: Apa Sebenarnya v4 Itu#

Tailwind CSS v4 adalah tiga hal sekaligus:

  1. Engine baru — ditulis ulang dari JavaScript ke Rust (engine Oxide), membuat build jauh lebih cepat
  2. Paradigma konfigurasi baru — konfigurasi CSS-first menggantikan tailwind.config.js sebagai default
  3. Integrasi lebih erat dengan platform CSS — native @layer, container queries, @starting-style, dan cascade layers menjadi first-class citizens

Headline 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:

css
/* 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:

css
/* v3 — app.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
js
// v3 — tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./src/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};
js
// 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.

Konfigurasi CSS-First dengan @theme#

Ini adalah pergeseran konseptual terbesar. Di v3, Anda menyesuaikan Tailwind melalui objek konfigurasi JavaScript:

js
// 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:

css
@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:

css
/* Anda mendapatkan ini secara gratis */
:root {
  --color-brand-50: #eff6ff;
  --color-brand-500: #3b82f6;
  --color-brand-900: #1e3a5f;
}
css
/* 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:

css
@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."

Override vs. Extend Tema Default#

Di v3 Anda punya theme (mengganti) vs theme.extend (menggabungkan). Di v4, mental model-nya berbeda:

css
@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-*:

css
@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.

Beberapa File Tema#

Untuk proyek yang lebih besar, Anda bisa membagi tema di beberapa file:

css
/* 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;
}
css
/* 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 Oxide: Benar-Benar 10x Lebih Cepat#

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.

Metrikv3 (Node)v4 (Oxide)Peningkatan
Build awal4.280ms387ms11x
Incremental (edit 1 file)340ms18ms19x
Full rebuild (bersih)5.100ms510ms10x
Dev server start3.200ms290ms11x

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.

Mengapa Jauh Lebih Cepat?#

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.

Apakah Kecepatan Benar-Benar Penting?#

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.

Integrasi Native @layer#

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:

css
/* 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:

css
@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.

Menambahkan Custom Utilities#

Di v3, Anda mendefinisikan custom utilities dengan plugin API atau @layer utilities:

js
// 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:

css
@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:

html
<p class="text-pretty md:text-balance">...</p>

Custom Variants#

Anda juga bisa mendefinisikan custom variants dengan @variant:

css
@import "tailwindcss";
 
@variant hocus (&:hover, &:focus);
@variant theme-dark (.dark &);
html
<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: Built-In, Tanpa Plugin#

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.

Penggunaan Dasar#

Tandai sebuah container dengan @container dan query ukurannya dengan prefix @:

html
<!-- 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:

VariantMin-width
@sm24rem (384px)
@md28rem (448px)
@lg32rem (512px)
@xl36rem (576px)
@2xl42rem (672px)

Named Containers#

Anda bisa memberi nama containers untuk melakukan query ancestor tertentu:

html
<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>

Mengapa Ini Penting#

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.

html
<!-- 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>

Variant Baru yang Benar-Benar Berguna#

v4 menambahkan beberapa variant baru yang terus saya gunakan. Mereka mengisi gap yang nyata.

Variant 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:

html
<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:

css
/* 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.

Variant not-*#

Negasi. Sesuatu yang sudah lama kita inginkan:

html
<!-- 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>

Variant nth-*#

Akses langsung nth-child dan nth-of-type:

html
<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.

Variant 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":

html
<div class="in-data-active:bg-brand-50">
  Ini mendapat background ketika ancestor mana pun memiliki data-active
</div>

Variant Deep Universal **:#

Style semua descendant, bukan hanya direct children. Ini kekuatan yang terkontrol — gunakan dengan hemat, tapi sangat berharga untuk konten prose dan output CMS:

html
<!-- 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>

Breaking Changes: Apa yang Benar-Benar Rusak#

Izinkan saya terus terang. Jika Anda punya proyek v3 yang besar, migrasi tidaklah sepele. Berikut apa yang rusak di proyek saya:

1. Format Konfigurasi#

tailwind.config.js Anda tidak langsung berfungsi. Anda perlu:

  • Mengkonversinya ke CSS @theme (direkomendasikan untuk arsitektur baru)
  • Menggunakan compatibility layer directive @config (jalur migrasi cepat)
css
/* 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.

2. Utility yang Deprecated Dihapus#

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.

3. Perubahan Palet Warna Default#

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.

4. Deteksi Konten#

v3 memerlukan konfigurasi content secara eksplisit:

js
// 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:

css
@import "tailwindcss";
@source "../shared-components/**/*.tsx";
@source "../design-system/src/**/*.tsx";

5. Perubahan Plugin API#

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:

js
// v3 plugin
plugin(function ({ addUtilities, theme }) {
  addUtilities({
    ".scrollbar-hide": {
      "-ms-overflow-style": "none",
      "scrollbar-width": "none",
      "&::-webkit-scrollbar": {
        display: "none",
      },
    },
  });
});
css
/* 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.

6. JIT Adalah Satu-Satunya Mode#

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.

7. Beberapa Perubahan Sintaks Variant#

Beberapa variant diganti nama atau berubah perilaku:

html
<!-- 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.

Panduan Migrasi: Proses yang Sebenarnya#

Berikut cara saya benar-benar melakukan migrasi, bukan happy path dari dokumentasi tapi seperti apa proses sebenarnya.

Langkah 1: Jalankan Codemod Resmi#

Tailwind menyediakan codemod yang menangani kebanyakan perubahan mekanis:

bash
npx @tailwindcss/upgrade

Ini melakukan banyak hal secara otomatis:

  • Mengkonversi directive @tailwind ke @import "tailwindcss"
  • Mengganti nama class utility yang deprecated
  • Memperbarui sintaks variant
  • Mengkonversi opacity utilities ke sintaks slash (bg-opacity-50 ke bg-black/50)
  • Membuat blok @theme dasar dari konfigurasi Anda

Apa yang Ditangani Codemod dengan Baik#

  • Penggantian nama class utility (hampir sempurna)
  • Perubahan sintaks directive
  • Nilai tema sederhana (warna, spacing, font)
  • Migrasi sintaks opacity

Apa yang TIDAK Ditangani Codemod#

  • Konversi plugin yang kompleks
  • Nilai konfigurasi dinamis (panggilan theme() di JavaScript)
  • Konfigurasi tema kondisional (misalnya, nilai tema berdasarkan environment)
  • Migrasi custom plugin API
  • Edge case arbitrary value di mana parser baru menginterpretasikan secara berbeda
  • Nama class yang dibangun secara dinamis di JavaScript (template literals, string concatenation)

Langkah 2: Perbaiki Konfigurasi PostCSS#

Untuk kebanyakan setup, Anda akan memperbarui konfigurasi PostCSS Anda:

js
// 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:

js
// vite.config.ts
import tailwindcss from "@tailwindcss/vite";
 
export default defineConfig({
  plugins: [tailwindcss()],
});

Langkah 3: Konversi Konfigurasi Tema#

Ini adalah bagian manual. Ambil nilai tema tailwind.config.js Anda dan konversi ke @theme:

js
// 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" },
        },
      },
    },
  },
};
css
/* 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.

Langkah 4: Visual Regression Testing#

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:

  • Beberapa tempat di mana migrasi sintaks opacity menghasilkan hasil yang sedikit berbeda
  • Output custom plugin yang tidak terbawa
  • Perubahan z-index stacking akibat urutan layer
  • Beberapa override !important yang berperilaku berbeda dengan cascade layers

Langkah 5: Perbarui Dependensi Pihak Ketiga#

Periksa setiap package terkait Tailwind:

json
{
  "@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.

Bekerja dengan Next.js#

Karena saya menggunakan Next.js untuk kebanyakan proyek, berikut setup spesifiknya.

Pendekatan PostCSS (Direkomendasikan untuk Next.js)#

Next.js menggunakan PostCSS di balik layar, jadi plugin PostCSS adalah pilihan yang natural:

bash
npm install tailwindcss @tailwindcss/postcss
js
// postcss.config.mjs
export default {
  plugins: {
    "@tailwindcss/postcss": {},
  },
};
css
/* 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).

Urutan Import CSS#

Satu hal yang menjebak saya: urutan import CSS lebih penting di v4 karena cascade layers. @import "tailwindcss" Anda harus datang sebelum custom styles Anda:

css
/* 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#

Dark mode bekerja dengan cara yang sama secara konseptual tapi konfigurasinya pindah ke CSS:

css
@import "tailwindcss";
 
/* Gunakan dark mode berbasis class (default berbasis media) */
@variant dark (&:where(.dark, .dark *));

Ini menggantikan konfigurasi v3:

js
// 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:

css
/* 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));

Kompatibilitas Turbopack#

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:

Setupv3 + Webpackv3 + Turbopackv4 + Turbopack
Cold start4.8s2.1s1.3s
HMR (perubahan CSS)450ms180ms40ms

40ms HMR untuk perubahan CSS nyaris tidak terasa. Rasanya instan.

Deep Dive Performa: Melampaui Kecepatan Build#

Manfaat engine Oxide melampaui kecepatan build mentah.

Penggunaan Memori#

v4 menggunakan memori yang jauh lebih sedikit. Di proyek 847-komponen saya:

Metrikv3v4
Peak memory (build)380MB45MB
Steady-state (dev)210MB28MB

Ini penting untuk pipeline CI/CD di mana memori terbatas, dan untuk mesin development yang menjalankan sepuluh proses secara bersamaan.

Ukuran Output CSS#

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.

Tree Shaking Nilai Tema#

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:

css
@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.

Lanjutan: Menyusun Tema untuk Multi-Brand#

Satu pola yang v4 buat jauh lebih mudah adalah theming multi-brand. Karena nilai tema adalah CSS custom properties, Anda bisa menukarnya saat runtime:

css
@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;
}
html
<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.

Yang Saya Rindukan dari v3#

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.

Haruskah Anda Migrasi?#

Berikut kerangka keputusan saya setelah hidup dengan v4 selama beberapa minggu:

Migrasi Sekarang Jika:#

  • Anda memulai proyek baru (pilihan yang jelas — mulai dengan v4)
  • Proyek Anda memiliki custom plugin yang minimal
  • Anda menginginkan manfaat performa untuk proyek besar
  • Anda sudah menggunakan pola Tailwind modern (slash opacity, shrink-*, dll.)
  • Anda butuh container queries dan lebih memilih tidak menambahkan plugin

Tunggu Jika:#

  • Anda sangat bergantung pada plugin Tailwind pihak ketiga yang belum mendukung v4
  • Anda punya konfigurasi tema programatik yang kompleks
  • Proyek Anda stabil dan tidak aktif dikembangkan (kenapa menyentuhnya?)
  • Anda sedang di tengah sprint fitur (migrasi antar sprint, bukan selama sprint)

Jangan Migrasi Jika:#

  • Anda di v2 atau lebih awal (upgrade ke v3 dulu, stabilkan, lalu pertimbangkan v4)
  • Proyek Anda berakhir dalam beberapa bulan ke depan (tidak sepadan dengan churn-nya)

Pendapat Jujur Saya#

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:

  1. Sekarang: Mulai proyek baru apa pun di v4
  2. Segera: Bereksperimen dengan mengkonversi proyek internal kecil ke v4
  3. Saat siap: Migrasikan proyek produksi saat sprint yang tenang, dengan visual regression testing

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.

Artikel Terkait