Membangun UI yang konsisten dengan Tailwind CSS: design tokens untuk warna dan spacing, reusable components, dark mode, serta praktik menjaga konsistensi visual di seluruh lapisan aplikasi fullstack.

Setelah delapan episode membangun logika, database, dan auth, aplikasi kalian masih "telanjang". Episode ini membahas styling & design integration: bagaimana membuat aplikasi fullstack tidak hanya bekerja dengan benar, tetapi juga terlihat profesional dan konsisten — dan mengapa konsistensi visual bukan sekadar urusan estetika, melainkan bagian dari kualitas produk.
Konsistensi bukan tentang jadi desainer. Ia tentang sistem: mendefinisikan warna, spacing, tipografi, dan bentuk sekali, lalu memakainya di mana-mana. Dengan sistem yang baik, halaman baru yang kalian buat akan otomatis terlihat seperti bagian dari produk yang sama.
Design tokens adalah nilai-nilai desain dasar (warna, ukuran, radius, font) yang diberi nama dan dipakai berulang. Tailwind sudah memiliki sistem token bawaan (text-red-500, p-4, rounded-lg), tetapi kalian bisa menyesuaikannya lewat theme.extend:
import type { Config } from "tailwindcss"
export default {
content: ["./src/**/*.{ts,tsx}"],
darkMode: "class",
theme: {
extend: {
colors: {
brand: {
50: "#eef2ff",
500: "#6366f1",
600: "#4f46e5",
700: "#4338ca",
},
},
borderRadius: {
DEFAULT: "0.5rem",
},
},
},
plugins: [],
} satisfies ConfigSekarang bg-brand-600 menjadi warna utama brand kalian. Kuncinya: jangan pakai warna mentah (#4f46e5) langsung di komponen — selalu lewat token, agar mengubah identitas brand cukup mengedit satu tempat.
Note
Design token adalah alasan mengapa Tailwind tidak "semrawut" walau terlihat banyak utility class. Ia mengganti CSS custom property yang ditulis manual dengan sistem terukur: skala spacing 1, 2, 3, 4... dan palet warna bernama. Konsistensi otomatis muncul karena tidak ada yang menulis nilai bebas.
Konsistensi visual paling mudah dijaga lewat komponen yang dipakai ulang. Tombol, kartu, dan input seharusnya tidak ditulis ulang di setiap halaman — karena itu jalan paling cepat menuju ketidakkonsistenan.
import { cn } from "@/lib/cn"
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: "primary" | "secondary"
}
export function Button({
variant = "primary",
className,
...props
}: ButtonProps) {
return (
<button
className={cn(
"px-4 py-2 rounded font-medium transition-colors",
variant === "primary" &&
"bg-brand-600 text-white hover:bg-brand-700",
variant === "secondary" &&
"bg-gray-100 text-gray-700 hover:bg-gray-200",
className,
)}
{...props}
/>
)
}export function Card({
children,
className,
}: {
children: React.ReactNode
className?: string
}) {
return (
<div className={cn("rounded-lg border border-gray-200 p-6", className)}>
{children}
</div>
)
}cn (penggabung className dari clsx + tailwind-merge) memungkinkan komponen dipakai dengan class tambahan tanpa konflik:
<Button variant="primary">Simpan</Button>
<Card className="max-w-md">Konten kartu</Card>Dark mode yang baik tidak berarti "warna hitam". Ia berarti token yang responsif terhadap mode. Dengan darkMode: "class" di atas, komponen bisa memakai variant dark di class yang sama:
<div className="bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-800">
<p className="text-gray-800 dark:text-gray-100">Konten</p>
</div>Toggle dark mode dilakukan di tingkat layout dengan next-themes, sehingga seluruh halaman berubah konsisten:
"use client"
import { ThemeProvider } from "next-themes"
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
)
}Styling tidak berhenti di komponen. Aplikasi fullstack harus konsisten di setiap lapisan:
p-4, gap-6) — jangan nilai arbitrer.text-2xl untuk halaman, text-lg untuk section) di semua halaman.opacity-50 pointer-events-none), error, dan empty state harus punya pola visual yang sama di mana pun.emerald/red) — bukan asal-asalan.export function EmptyState({ message }: { message: string }) {
return (
<div className="py-12 text-center text-gray-500 dark:text-gray-400">
<p className="text-lg">{message}</p>
</div>
)
}Warning
Perangkap terbesar konsistensi adalah "halaman ini butuh style khusus". Sebelum menulis class baru yang belum pernah ada, tanyakan: apakah ini memang pola baru, atau penyimpangan? Jaga disiplin — setiap komponen baru harus lahir dari sistem, bukan dari kreativitas dadakan.
Selesaikan dengan mengaudit aplikasi catatan:
Button, Card, dan EmptyState sebagai reusable component.Button — tidak ada lagi <button> mentah.next-themes dan uji toggle-nya.rg "<button" src/ --glob "!components/ui/button.tsx"Output harus kosong — tanda tidak ada tombol mentah yang tersisa.
Inti yang harus dibawa pulang:
Di episode 11 selanjutnya kita membahas error handling & logging — error boundaries, structured logging, dan observability dasar — agar aplikasi tidak hanya berjalan, tapi juga bisa didiagnosis saat terjadi masalah. Sampai jumpa di episode 11!