Menulis CSS klasik berarti menamai setiap elemen lalu menulis berkas stylesheet terpisah: .kartu-produk, .harga-besar, .badge-stok-habis. Tailwind membalik pendekatan itu — daripada menulis CSS khusus, kita merangkai tampilan langsung di HTML dari utility class siap pakai seperti rounded-xl bg-white shadow-sm. Kabar baiknya: aplikasi segar Laravel 13 sudah membawa Tailwind v4 terpasang, lengkap dengan plugin Vite-nya. Chapter ini membedah integrasi itu, membuktikan utility benar-benar masuk ke hasil build, lalu menyentuh dua hal yang sering menjebak: tema kustom dan class dinamis.
Yang Sudah Terpasang di Aplikasi Segar #
Tiga jejak Tailwind ada di aplikasi baru. Pertama, package.json memuat tailwindcss dan @tailwindcss/vite sebagai devDependency. Kedua, vite.config.js mendaftarkan plugin-nya:
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [
laravel({ /* ... */ }),
tailwindcss(),
],
});Ketiga, resources/css/app.css bawaan hanya berisi beberapa baris:
@import 'tailwindcss';
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
@source '../../storage/framework/views/*.php';
@theme {
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif;
}Baris @import 'tailwindcss' adalah cara Tailwind v4 diaktifkan. Pada v3 dulu kita perlu tailwind.config.js berisi direktur content, plus @tailwind base; @tailwind components; @tailwind utilities; di CSS, dan proses lewat PostCSS. v4 memangkasnya menjadi satu plugin Vite plus satu @import — tanpa file config sama sekali untuk kebutuhan standar. Direktif @source di atas meluaskan area pemindaian ke view pagination bawaan Laravel dan view Blade yang sudah terkompilasi, sehingga utility di sana ikut terdeteksi. Kalau kita mulai dari aplikasi lama tanpa Tailwind, jalur resminya: npm install -D tailwindcss @tailwindcss/vite, daftarkan plugin di vite.config.js, lalu tambahkan @import 'tailwindcss' di app.css.
Filosofi Utility-First #
| Aspek | CSS komponen (.btn-primary) | Tailwind utility |
|---|---|---|
| Tempat gaya hidup | Berkas CSS terpisah | Langsung di markup |
| Penamaan | Wajib dirancang tiap elemen | Tidak perlu nama baru |
| Gaya berulang | DRY, satu class dipakai ulang | Diabstraksi lewat component Blade |
| Ukuran bundle | Kecil tapi mudah menumpuk dead CSS | Hanya class yang terpakai ikut terbuild |
Kunci keseimbangannya: utility ditulis di markup sebatahap mungkin. Begitu kombinasi utility mulai diulang, kita angkat menjadi component — dan itu pas sekali dengan Blade, seperti nanti di chapter Blade Components untuk UI .
Praktik: Halaman Etalase #
Kita buat halaman daftar produk dengan utility Tailwind di setiap elemennya:
{{-- resources/views/etalase.blade.php (inti) --}}
<body class="bg-slate-100 min-h-screen p-8 font-sans">
<main class="mx-auto max-w-3xl">
<h1 class="text-2xl font-semibold text-slate-900">Etalase Kopi</h1>
<div class="mt-6 grid gap-4 sm:grid-cols-2">
@foreach ($produk as $item)
<div class="rounded-xl bg-white p-5 shadow-sm ring-1 ring-slate-200">
<h2 class="font-semibold text-slate-900">{{ $item['nama'] }}</h2>
<p class="mt-3 text-lg font-bold text-indigo-600">{{ $item['harga'] }}</p>
@if ($item['stok'] > 5)
<span class="mt-3 inline-block rounded-full bg-emerald-100 px-3 py-1 text-xs font-medium text-emerald-800">Tersedia</span>
@endif
</div>
@endforeach
</div>
</main>
</body>Tailwind memindai berkas Blade ini dan hanya menghasilkan utility yang benar-benar dipakai. Ukuran CSS build tumbuh dari 93.28 kB (halaman bawaan saja) menjadi 94.05 kB setelah halaman etalase ditambahkan — bukti bahwa pemindaian bekerja selektif, bukan mengepak seluruh framework. Feature test memverifikasi utility tersebut betul-betul ada di berkas build:
public function test_etalase_merender_kartu_dengan_utility_class(): void
{
$response = $this->get('/etalase');
$response->assertStatus(200);
$response->assertSee('Kopi Gayo');
$manifest = json_decode(file_get_contents(public_path('build/manifest.json')), true);
$css = file_get_contents(public_path('build/'.$manifest['resources/css/app.css']['file']));
$this->assertStringContainsString('grid-cols-2', $css);
$this->assertStringContainsString('rounded-xl', $css);
}Output yang Dihasilkan:
PASS Tests\Feature\EtalaseTest
✓ etalase merender kartu dengan utility class 0.24s
Tests: 1 passed (5 assertions)
Duration: 0.40sTema Kustom via Variabel CSS #
v4 mengganti tailwind.config.js lama dengan variabel CSS di dalam @theme. Kita menambahkan warna kustom bernama kopi ke app.css:
@theme {
--color-kopi: #6f4e37;
}Setelah rebuild, utility baru bernapas otomatis: halaman etalase memakai border-kopi dan text-kopi, dan kami memverifikasi keduanya hadir sebagai selector .border-kopi serta .text-kopi di berkas CSS build. Nilai apa pun yang kita daftar di @theme langsung tersedia di seluruh sistem utility — warna, font, spacing, hingga breakpoint.
Jebakan: Class yang Dirakit Runtime #
Tailwind hanya bisa memindai teks yang tampak di kode sumber. Ketika nama class dirakit saat runtime — misalnya "bg-{$warna}-100" dengan $warna dari database — pemindai tidak tahu nilainya, dan utility itu tidak akan pernah terbuild. Kami membuktikannya di sandbox: class bg-lime-100 yang tidak muncul literal di berkas mana pun terbukti absen dari CSS build, lalu hadir sebagai selector .bg-lime-100 setelah kita mendaftarkannya lewat safelist v4:
@source inline("bg-lime-100 text-lime-800");Direktif @source inline(...) memaksa utility yang disebutkan tetap dihasilkan apa pun isi kode sumber. Sebagai pembanding, class lain yang juga tidak kami daftarkan — bg-fuchsia-100 — tetap absen dari hasil build.
Dengan Tailwind berjalan, halaman kita berpenampilan layak tanpa satu baris CSS kustom pun. Berikutnya kita naikkan tata letak yang berulang-ulang itu menjadi UI kit berbasis component Blade — tombol, kartu, badge, dan modal dengan variant teruji di Blade Components untuk UI .