Lompat ke konten
Kembali ke modul

Browser: HTML, CSS, DOM, dan Aksesibilitas · 5/8

Tailwind: CSS sebagai utility class

Pendekatan styling yang dipakai proyek ini, kenapa banyak tim memilihnya, dan disiplin design token di baliknya.

Baca 28 menit

Setelah pelajaran ini kamu bisa

  • Menjelaskan apa itu utility class dan kenapa banyak tim mengadopsinya
  • Membaca daftar class Tailwind dengan lancar
  • Menjelaskan kenapa class kondisional butuh `cn()` dan bukan penggabungan string
  • Memakai design token semantik alih-alih warna mentah

Tailwind memberimu satu class per deklarasi CSS. p-4 itu padding: 1rem. flex itu display: flex. Kamu menulis style-nya di markup alih-alih di file terpisah.

Awalnya terlihat salah. Alasan banyak tim tetap mengadopsinya bukan soal singkatnya — tapi karena dua masalah tersulit CSS dalam skala besar jadi hilang.

  • Penamaan. Kamu tidak perlu lagi menciptakan nama class seperti .card-inner-wrapper-alt, dan tidak perlu berdebat soal itu.
  • Penghapusan. Menghapus sebuah component sekaligus menghapus style-nya. Di stylesheet biasa, tidak ada yang berani menghapus aturan, jadi file CSS-nya cuma bertambah.
  • Kedekatan. Style-nya ada di tempat markup-nya. Kamu tidak perlu membaca dua file untuk memahami satu elemen.
  • Batasan. p-4 dan p-6 ada; p-17px tidak. Skala yang tetap menjaga desain tetap konsisten secara default.

Membaca daftar class

html
<div class="flex items-center gap-2 rounded-lg border border-border bg-card p-4
            text-sm hover:bg-accent sm:p-6 dark:bg-card/50">

<!-- flex            display: flex
     items-center    align-items: center
     gap-2           gap: 0.5rem
     rounded-lg      border-radius: var(--radius-lg)
     border          border-width: 1px
     border-border   border-color: var(--border)
     bg-card         background: var(--card)
     p-4             padding: 1rem
     text-sm         font-size: 0.875rem
     hover:bg-accent background on hover only
     sm:p-6          padding: 1.5rem at the small breakpoint and up
     dark:bg-card/50 in dark mode, card colour at 50% opacity          -->

Awalan-awalannya itulah kekuatan sebenarnya. hover:, focus-visible:, sm:, dark:, disabled: dan group-hover: semuanya menerapkan sebuah utility secara kondisional — hal-hal yang butuh aturan dan selector terpisah di CSS biasa.

Token semantik, bukan warna mentah

html
<!-- Fragile: hardcoded, and dark mode needs a second guess every time. -->
<div class="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">

<!-- Robust: the token already knows what it should be in each theme. -->
<div class="bg-card text-card-foreground">
bg-card mengarah ke var(--card), yang didefinisikan sekali per tema di globals.css. Ubah paletnya di sana dan setiap component mengikuti.

Token datang berpasangan — bg-primary dengan text-primary-foreground, bg-card dengan text-card-foreground. Pakai pasangannya dan kontrasnya benar secara otomatis, di kedua tema. Itu sebabnya aturan proyek ini melarang nilai hex mentah di dalam component.

Class kondisional dan cn()

Utility itu cuma class, jadi dua class yang bertentangan bisa sama-sama berakhir di satu elemen. CSS lalu memutuskan berdasarkan urutan di stylesheet yang dihasilkan — bukan urutan penulisanmu. Itu membuat penggabungan string yang naif jadi tidak bisa diprediksi.

Coba sendiri

`tailwind-merge` versi mini: utility terakhir yang bertentangan yang menang, per kelompok property. Inilah yang dilakukan `cn()` untukmu.

Hasil

Tekan Jalankan untuk melihat hasilnya.

Ini jalan di browser kamu, di dalam sandbox. Apa pun yang kamu tulis di sini tidak bisa merusak situs.

tsx
import { cn } from "@/lib/utils";

function Badge({ tone, className, children }) {
  return (
    <span
      className={cn(
        "inline-flex items-center rounded-md px-2 py-1 text-xs",  // base
        tone === "success" && "bg-success-subtle text-success",   // conditional
        tone === "danger" && "bg-destructive-subtle text-destructive",
        className,                                                // caller wins
      )}
    >
      {children}
    </span>
  );
}

// The caller can now override, and it actually takes effect:
<Badge tone="success" className="px-4 text-sm" />
className ditaruh terakhir dengan sengaja — itulah yang membuat penimpaan dari pemanggilnya menang.

Tugas praktik

Buka components/ui/button.tsx di proyek ini dan temukan pemanggilan cva-nya. Identifikasi class dasarnya, tabel variant-nya, dan tabel size-nya. Lalu temukan di mana cn() dipanggil dan jelaskan dalam satu kalimat kenapa className dikirim ke dalam buttonVariants() alih-alih digabungkan setelahnya.

Hasil yang diharapkan

Supaya tailwind-merge bisa menyelesaikan konflik antara class varian dan class dari pemanggilnya.