Saya baru saja memindahkan sebuah situs klien ke Next.js dengan Tailwind v4, dan salah satu hal pertama yang saya kerjakan adalah tipografinya. Desainnya minta satu Google Font yang bersih, jadi saya wire lewat next/font, expose sebagai CSS variable, lalu daftarkan variabel itu di dalam blok @theme Tailwind. Semuanya terasa benar di kepala saya. Tapi begitu saya buka halamannya, teksnya jatuh ke stack sans default. Bukan font yang saya pilih, cuma Arial-ish generik yang membosankan.
Yang bikin ini menyebalkan: tidak ada error. Tidak ada warning di konsol, tidak ada garis merah di terminal, next dev hijau-hijau saja. Font variable-nya jelas ada, saya bisa lihat di DevTools terpasang di elemen html. Class font-sans juga ada di elemen yang saya cek. Tapi --font-sans yang harusnya menunjuk ke font saya malah kosong.
Setup awal yang saya kira benar
Konfigurasinya kira-kira begini. next/font di-load dan diberi variabel:
import { Quicksand } from "next/font/google";
const quicksand = Quicksand({
subsets: ["latin"],
variable: "--font-quicksand",
});Lalu variabelnya ditempel ke html, dan di globals.css saya kira cukup mereferensikannya di dalam @theme:
@theme {
--font-sans: var(--font-quicksand), ui-sans-serif, system-ui, sans-serif;
}Logikanya, Tailwind bakal bikin token font-sans yang menunjuk ke var(--font-quicksand), dan next/font mengisi variabel itu saat runtime. Kelihatan rapi. Kenyataannya tidak jalan sama sekali.
Kenapa ini terjadi
Butuh beberapa saat sampai saya sadar akar masalahnya, dan begitu klik, rasanya jelas banget. Blok @theme di Tailwind v4 itu diresolusi saat build. Tailwind membaca isi @theme, menghitung setiap token, lalu menuliskannya jadi CSS custom property di generated stylesheet-nya. Proses itu terjadi di build time, di dalam pipeline Tailwind, sebelum ada browser sama sekali.
Masalahnya, var(--font-quicksand) itu variabel runtime. Nilainya baru ada setelah next/font menyuntikkan variabel itu ke DOM di sisi klien. Saat Tailwind sedang meresolusi @theme di build time, --font-quicksand belum ada nilainya, jadi token --font-sans yang dihasilkan Tailwind berakhir kosong atau invalid. Class font-sans tetap ada, tapi ia menunjuk ke token yang tidak berisi font apa pun. Hasilnya jatuh ke fallback.
Jadi bukan next/font yang gagal, bukan variabelnya yang salah nama. Yang salah adalah saya menaruh referensi variabel runtime di tempat yang cuma paham nilai build time. Dua dunia yang berbeda, dan saya menyeberangkannya di titik yang salah.
Perbaikannya
Kuncinya adalah memisahkan dua urusan: token yang dibutuhkan Tailwind saat build, dan font runtime yang disuntik next/font.
Pertama, di dalam @theme, pakai nama font family literal, bukan var(). Ini memberi Tailwind token yang valid dan konkret saat build:
@theme {
--font-sans: "Quicksand", ui-sans-serif, system-ui, sans-serif;
}Lalu, secara terpisah, terapkan variabel runtime dari next/font langsung di sebuah rule CSS biasa pada body:
body {
font-family: var(--font-quicksand), ui-sans-serif, system-ui, sans-serif;
}Sekarang alurnya benar. Tailwind punya token literal yang valid di build time, jadi font-sans tidak lagi kosong. Dan body memakai variabel runtime yang beneran diisi next/font di klien, jadi font aslinya yang tampil. Begitu saya reload, teksnya langsung ganti ke Quicksand. Tidak ada lagi fallback yang membosankan.
Detail kecil yang penting: jangan mengandalkan var() di dalam @theme untuk apa pun yang nilainya baru muncul saat runtime. @theme bukan tempat untuk nilai dinamis. Kalau nilai variabel diinjeksi belakangan oleh JavaScript atau oleh loader seperti next/font, referensinya harus hidup di CSS runtime biasa, bukan di dalam blok build time Tailwind.
Pelajaran
Kombinasi Tailwind v4 dan next/font punya jebakan halus di batas antara build time dan runtime. @theme itu build time, next/font itu runtime, dan var() yang menghubungkan keduanya diam-diam diresolusi kosong. Aturan praktis yang saya pegang sekarang: taruh nilai family literal di @theme supaya Tailwind punya token yang sah, dan pisahkan variabel runtime next/font ke rule CSS biasa di body. Sejak itu, setiap kali font tidak mau muncul padahal variabelnya jelas terdefinisi, pertanyaan pertama saya bukan lagi "variabelnya kenapa" tapi "nilai ini diresolusi kapan, saat build atau saat runtime?".
