Bagian terakhir ini membuat galeri terasa hidup dan siap dipakai orang lain. Ada empat hal yang ditambahkan: transisi dan state hover pada kartu serta tombol, cincin fokus untuk navigasi keyboard, penurunan gerak otomatis bagi pengguna yang memintanya, dan tema gelap yang mengandalkan satu kekuatan arsitektur dari Bagian 1 — semua komponen memanggil token, maka menimpa token berarti mengubah seluruh halaman. Snapshot pada bagian ini adalah kondisi akhir proyek dan identik dengan berkas di src/.

Hover yang Memberi Umpan Balik #

Setiap .kartu kini punya transition untuk box-shadow, transform, dan border-color, dengan durasi dan pelunakan dari token --durasi dan --pelunakan. Saat kursor melayang, .kartu:hover mengangkat kartu 2 px dan menaikkan bayangan ke --bayangan-2. Kartu promosi naik lebih jauh: .kartu--produk:hover memakai --bayangan-3, konsisten dengan hierarki elevasi Bagian 3.

Kartu artikel diperlakukan berbeda karena ia adalah tautan. .kartu--artikel:hover menahan angkatannya dengan transform: none dan box-shadow: none, lalu mengganti umpan balik: border berubah ke --warna-utama dan judulnya bergaris bawah berwarna utama dengan text-underline-offset: 3px. Dua bahasa hover dalam satu galeri, sesuai peran masing-masing kartu. Tombol juga diberi kehalusan: transisi cepat 120 ms pada latar, plus .tombol:active { transform: translateY(1px); } yang menurunkan tombol sesaat saat ditekan.

Fokus Keyboard dan Reduced Motion #

Aturan :focus-visible global memberi cincin outline: 2px solid var(--warna-fokus) dengan outline-offset: 2px. Selector ini hanya aktif saat fokus berasal dari keyboard, sehingga klik tetap bersih sementara pengguna Tab tetap selalu tahu posisinya. .tombol:focus-visible melebarkan jarak cincin menjadi 3 px karena tombol beradius kecil.

Semua gerakan tadi lalu dibungkus rasa hormat: di dalam @media (prefers-reduced-motion: reduce), transisi kartu dan tombol dimatikan serta angkatan hover dihilangkan dengan transform: none. Pengguna yang menonaktifkan animasi di sistemnya mendapat galeri yang tenang tanpa kehilangan satu pun fungsi.

Tema Gelap dari Token #

Blok [data-tema="gelap"] hanyalah daftar penimpaan token: latar gelap cokelat, permukaan yang sedikit lebih terang, teks krem, dan deret warna utama yang dibalik arahnya — tombol kini berlatar cokelat muda dengan teks gelap. Tiga token bayangan diganti versi bertopasitas hitam agar tetap terlihat di atas latar gelap. Tidak ada satu pun selector komponen yang disentuh; semua kartu, tombol, dan lencana ikut berganti tema karena semuanya membaca token yang sama.

main.js yang menyertainya tetap di bawah 40 baris. Ia mencari #tombol-tema dan #label-tema, lalu fungsi terapkanTema() memasang atau melepas atribut data-tema="gelap" pada <html>, mengganti teks label, dan menyinkronkan aria-pressed agar pembaca layar tahu keadaan tombol. Klik tombol hanya membalik keadaan saat ini. Kondisi awal dibaca dari matchMedia("(prefers-color-scheme: dark)"), sehingga galeri terbuka mengikuti setelan sistem tanpa menyimpan apa pun. Di index.html, tombol toggle diletakkan di header dan skrip dimuat dengan defer agar DOM sudah siap.

css
/* ============================================================
   Koleksi Kartu UI — stylesheet utama
   Urutan: token → base → kerangka halaman → tombol → kartu
   → varian → elevasi → tipografi → state → tema gelap
   ============================================================ */

/* ---------- 1. Design token ---------- */
:root {
  /* Warna semantik, tema terang */
  --warna-latar: #f6f1e7;        --warna-permukaan: #ffffff;
  --warna-teks: #2a2118;         --warna-teks-lembut: #6d5c48;
  --warna-garis: #e4d9c6;        --warna-utama: #7a4a21;
  --warna-utama-pekat: #5c3517;  --warna-utama-kontras: #ffffff;
  --warna-utama-lembut: #f0e2cf; --warna-aksen: #b3541e;
  --warna-sukses: #1f7a4d;       --warna-bahaya: #b3372b;
  --warna-fokus: #1d4ed8;

  /* Skala spasi, basis empat dan delapan piksel */
  --jarak-1: 4px;  --jarak-2: 8px;
  --jarak-3: 12px; --jarak-4: 16px;
  --jarak-5: 24px; --jarak-6: 32px;
  --jarak-7: 48px; --jarak-8: 64px;

  /* Radius sudut */
  --radius-kecil: 6px; --radius: 12px;
  --radius-penuh: 999px;

  /* Elevasi bayangan, tiga tingkat */
  --bayangan-1: 0 1px 2px rgba(42, 33, 24, 0.08);
  --bayangan-2: 0 4px 12px rgba(42, 33, 24, 0.14);
  --bayangan-3: 0 12px 28px rgba(42, 33, 24, 0.18);

  /* Skala tipografi */
  --font-dasar: system-ui, "Segoe UI", sans-serif;
  --ukuran-teks-kecil: 0.8125rem;
  --ukuran-teks-sedang: 0.9375rem;
  --ukuran-teks-dasar: 1rem;
  --ukuran-teks-subjudul: 1.25rem;
  --ukuran-teks-judul: 1.5rem;
  --ukuran-angka: 2.25rem;
  --tinggi-baris: 1.55;
  --jejak-huruf-lebar: 0.08em;

  /* Gerak */
  --durasi-cepat: 120ms; --durasi: 200ms; --pelunakan: ease-out;

  /* Kerangka halaman */
  --lebar-isi: 1080px;
}

/* ---------- 2. Base ---------- */
*, *::before, *::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: var(--font-dasar);
  line-height: var(--tinggi-baris);
  background-color: var(--warna-latar);
  color: var(--warna-teks);
}

h1, h2, h3, p, figure { margin: 0; }
img { display: block; max-width: 100%; }

/* ---------- 3. Kerangka halaman ---------- */
.pembungkus {
  max-width: var(--lebar-isi);
  margin-inline: auto;
  padding-inline: 16px;
}

.laman-kepala {
  padding-block: 24px;
  background-color: var(--warna-permukaan);
  border-bottom: 1px solid var(--warna-garis);
}

.kepala-baris {
  display: flex; align-items: center;
  justify-content: space-between;
  gap: 16px; flex-wrap: wrap;
}

.brand { display: flex; align-items: center; gap: 12px; }
.brand img { width: 40px; height: 40px; }
.brand-nama { font-size: 20px; font-weight: 700; }

.laman-utama { padding-block: 48px 64px; }
.section-judul { font-size: 28px; font-weight: 800; }
.pengantar { margin-top: 8px; max-width: 65ch; color: var(--warna-teks-lembut); font-size: 15px; }

.pamer-token {
  display: flex; flex-wrap: wrap;
  gap: 12px 24px; margin-top: 24px;
  padding: 16px; background-color: var(--warna-permukaan);
  border: 1px solid var(--warna-garis); border-radius: 12px;
}

.token { display: flex; align-items: center; gap: 8px; font-size: 13px; }
.token-contoh { width: 24px; height: 24px; border-radius: 6px; border: 1px solid var(--warna-garis); }
.token-nama { color: var(--warna-teks-lembut); }

.galeri-kartu {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 24px; margin-top: 32px;
}

.laman-kaki {
  background-color: var(--warna-permukaan);
  border-top: 1px solid var(--warna-garis);
}

.kaki-baris {
  display: flex; justify-content: space-between;
  flex-wrap: wrap; gap: 8px 16px;
  padding-block: 24px; font-size: 13.5px;
  color: var(--warna-teks-lembut);
}

/* ---------- 4. Tombol ---------- */
.tombol {
  display: inline-block;
  padding: 9px 18px; border: none; border-radius: 6px;
  background-color: var(--warna-utama);
  color: var(--warna-utama-kontras);
  font: inherit; font-weight: 600; cursor: pointer;
}

.tombol:hover { background-color: var(--warna-utama-pekat); }
.tombol--sekunder { background-color: var(--warna-utama-lembut); color: var(--warna-utama-pekat); }
.tombol--sekunder:hover { background-color: var(--warna-garis); }

/* ---------- 5. Kartu ---------- */
.kartu {
  display: flex; flex-direction: column;
  gap: 12px; padding: 16px;
  background-color: var(--warna-permukaan);
  border: 1px solid var(--warna-garis); border-radius: 12px;
}

.kartu-lencana {
  align-self: flex-start;
  padding: 4px 12px; border-radius: var(--radius-penuh);
  background-color: var(--warna-utama-lembut); color: var(--warna-utama-pekat);
  font-size: 11.5px; font-weight: 700;
}

.kartu--produk .kartu-lencana { background-color: var(--warna-aksen); color: #ffffff; }

.kartu-media {
  aspect-ratio: 16 / 10;
  border-radius: 8px;
  background: linear-gradient(135deg, #d8a26a 0%, #7a4a21 100%);
}

.kartu-judul { font-size: 20px; line-height: 1.3; font-weight: 700; }

.kartu-teks { font-size: 14.5px; color: var(--warna-teks-lembut); }

.kartu-meta {
  display: flex; align-items: center;
  gap: 8px; font-size: 12.5px;
  color: var(--warna-teks-lembut);
}

.kartu-aksi {
  margin-top: auto;
  display: flex; align-items: center;
  justify-content: space-between; gap: 8px;
}

.kartu-harga { font-size: 16px; font-weight: 700; color: var(--warna-utama-pekat); }

/* ---------- 6. Varian kartu ---------- */
.kartu--artikel { color: inherit; text-decoration: none; }

.kartu--artikel .kartu-meta { margin-top: auto; padding-top: 4px; }

.kartu--profil { align-items: center; text-align: center; }

.kartu-avatar {
  width: 72px; height: 72px;
  display: grid; place-items: center;
  border-radius: var(--radius-penuh);
  background: linear-gradient(135deg, #b3541e, #7a4a21);
  color: #ffffff; font-size: 26px; font-weight: 700;
}

.kartu-peran { font-size: 13px; color: var(--warna-teks-lembut); }

.kartu-angka { font-size: 34px; font-weight: 800; line-height: 1.1; }

.kartu-tren { align-self: flex-start; padding: 2px 10px; border-radius: var(--radius-penuh); font-size: 12.5px; font-weight: 700; }
.kartu-tren--naik { color: var(--warna-sukses); }
.kartu-tren--turun { color: var(--warna-bahaya); }

/* ---------- 7. Elevasi dan aksen border ---------- */
.kartu { box-shadow: var(--bayangan-1); }

/* Kartu promosi naik satu tingkat dan melepas garis */
.kartu--produk {
  box-shadow: var(--bayangan-2);
  border-color: transparent;
}

/* Kartu artikel mengandalkan garis, bukan bayangan */
.kartu--artikel { box-shadow: none; }

/* Aksen kiri menandai kartu angka */
.kartu--statistik { border-left: 4px solid var(--warna-utama); }

.kartu-avatar {
  border: 3px solid var(--warna-permukaan);
  box-shadow: var(--bayangan-1);
}

/* ---------- 8. Tipografi dan ritme ---------- */
/* Nilai yang sama diikat ke token: satu perubahan token
   menggerakkan ritme seluruh halaman, bukan rule per rule. */

.laman-kepala { padding-block: var(--jarak-5); }

.laman-utama { padding-block: var(--jarak-7) var(--jarak-8); }

.laman-utama section { margin-top: var(--jarak-7); }

.pengantar {
  font-size: var(--ukuran-teks-sedang);
  line-height: 1.65;
}

.section-judul { letter-spacing: -0.01em; }

.pamer-token {
  margin-top: var(--jarak-5);
  padding: var(--jarak-4);
}

.token-nama {
  font-family: ui-monospace, "Cascadia Mono", monospace;
  font-size: var(--ukuran-teks-kecil);
}

.galeri-kartu {
  gap: var(--jarak-5);
  margin-top: var(--jarak-5);
}

.kartu {
  gap: var(--jarak-3);
  padding: var(--jarak-4);
}

.kartu-lencana {
  letter-spacing: var(--jejak-huruf-lebar);
  text-transform: uppercase;
}

.kartu-teks { line-height: 1.6; }

.kaki-baris { padding-block: var(--jarak-5); }

/* ---------- 9. State: hover, fokus, dan transisi ---------- */
.kartu {
  transition: box-shadow var(--durasi) var(--pelunakan), transform var(--durasi) var(--pelunakan), border-color var(--durasi) var(--pelunakan);
}

.kartu:hover {
  box-shadow: var(--bayangan-2);
  transform: translateY(-2px);
}

.kartu--artikel { transition: border-color var(--durasi) var(--pelunakan); }

.kartu--artikel:hover {
  box-shadow: none;
  transform: none;
  border-color: var(--warna-utama);
}

.kartu--artikel:hover .kartu-judul {
  color: var(--warna-utama);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.kartu--produk:hover { box-shadow: var(--bayangan-3); }

.tombol { transition: background-color var(--durasi-cepat) var(--pelunakan), transform var(--durasi-cepat) var(--pelunakan); }

.tombol:active { transform: translateY(1px); }

:focus-visible {
  outline: 2px solid var(--warna-fokus);
  outline-offset: 2px;
}

.tombol:focus-visible { outline-offset: 3px; }

@media (prefers-reduced-motion: reduce) {
  .kartu, .tombol { transition: none; }

  .kartu:hover { transform: none; }
}

/* ---------- 10. Tema gelap ---------- */
/* Cukup menimpa token: seluruh komponen ikut berganti tema
   tanpa satu pun selector komponen disentuh ulang. */
[data-tema="gelap"] {
  --warna-latar: #211a13;        --warna-permukaan: #2d241a;
  --warna-teks: #f2e8d9;
  --warna-teks-lembut: #c4b29b;  --warna-garis: #46382a;
  --warna-utama: #dfa75f;        --warna-utama-pekat: #edbe85;
  --warna-utama-kontras: #2a2118;
  --warna-utama-lembut: #3c2f1f; --warna-aksen: #e08244;
  --warna-sukses: #5cb98a;       --warna-bahaya: #e07a6d;
  --warna-fokus: #9db9ff;

  --bayangan-1: 0 1px 2px rgba(0, 0, 0, 0.4);
  --bayangan-2: 0 4px 12px rgba(0, 0, 0, 0.5);
  --bayangan-3: 0 12px 28px rgba(0, 0, 0, 0.55);
}
html
<!DOCTYPE html>
<html lang="id">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Koleksi Kartu UI</title>
  <link rel="icon" href="assets/logo.svg" type="image/svg+xml">
  <link rel="stylesheet" href="css/styles.css">
  <script src="js/main.js" defer></script>
</head>
<body>
  <header class="laman-kepala">
    <div class="pembungkus kepala-baris">
      <div class="brand">
        <img src="assets/logo.svg" alt="Logo Koleksi Kartu UI" width="40" height="40">
        <span class="brand-nama">Koleksi Kartu UI</span>
      </div>
      <button id="tombol-tema" class="tombol tombol--sekunder" type="button" aria-pressed="false">
        <span id="label-tema">Mode gelap</span>
      </button>
    </div>
  </header>

  <main class="laman-utama">
    <div class="pembungkus">
      <h1 class="section-judul">Galeri komponen kartu</h1>
      <p class="pengantar">Satu halaman untuk menguji sistem desain secara utuh: token warna, tipografi, bayangan, serta state hover dan fokus pada empat keluarga kartu.</p>

      <div class="pamer-token" aria-label="Sampel design token warna">
        <div class="token"><span class="token-contoh" style="background-color: var(--warna-utama)"></span><span class="token-nama">--warna-utama</span></div>
        <div class="token"><span class="token-contoh" style="background-color: var(--warna-aksen)"></span><span class="token-nama">--warna-aksen</span></div>
        <div class="token"><span class="token-contoh" style="background-color: var(--warna-sukses)"></span><span class="token-nama">--warna-sukses</span></div>
        <div class="token"><span class="token-contoh" style="background-color: var(--warna-bahaya)"></span><span class="token-nama">--warna-bahaya</span></div>
        <div class="token"><span class="token-contoh" style="background-color: var(--warna-garis)"></span><span class="token-nama">--warna-garis</span></div>
      </div>

      <section aria-labelledby="judul-produk">
        <h2 class="section-judul" id="judul-produk">Kartu produk</h2>
        <div class="galeri-kartu">
          <article class="kartu kartu--produk">
            <span class="kartu-lencana">Terlaris</span>
            <div class="kartu-media" role="img" aria-label="Ilustrasi gradient produk kopi"></div>
            <h3 class="kartu-judul">Kopi Gayo Aceh</h3>
            <p class="kartu-teks">Arabika sangrai medium dengan catatan cokelat dan rempah.</p>
            <div class="kartu-meta"><span>Sangrai medium</span><span>·</span><span>250 g</span></div>
            <div class="kartu-aksi">
              <span class="kartu-harga">Rp85.000</span>
              <button class="tombol" type="button">Tambah</button>
            </div>
          </article>
          <article class="kartu kartu--produk">
            <span class="kartu-lencana">Baru</span>
            <div class="kartu-media" role="img" aria-label="Ilustrasi gradient produk kopi Toraja"></div>
            <h3 class="kartu-judul">Kopi Toraja Sapan</h3>
            <p class="kartu-teks">Profil herbal dengan body tebal dan aroma kayu manis.</p>
            <div class="kartu-meta"><span>Sangrai gelap</span><span>·</span><span>200 g</span></div>
            <div class="kartu-aksi">
              <span class="kartu-harga">Rp92.000</span>
              <button class="tombol" type="button">Tambah</button>
            </div>
          </article>
          <article class="kartu kartu--produk">
            <span class="kartu-lencana">Restock</span>
            <div class="kartu-media" role="img" aria-label="Ilustrasi gradient produk kopi Kintamani"></div>
            <h3 class="kartu-judul">Kopi Kintamani</h3>
            <p class="kartu-teks">Keasaman jeruk yang cerah dengan sentuhan karamel.</p>
            <div class="kartu-meta"><span>Sangrai medium</span><span>·</span><span>250 g</span></div>
            <div class="kartu-aksi">
              <span class="kartu-harga">Rp78.000</span>
              <button class="tombol" type="button">Tambah</button>
            </div>
          </article>
        </div>
      </section>

      <section aria-labelledby="judul-artikel">
        <h2 class="section-judul" id="judul-artikel">Kartu artikel</h2>
        <div class="galeri-kartu">
          <a class="kartu kartu--artikel" href="#">
            <span class="kartu-lencana">Tutorial</span>
            <div class="kartu-media" role="img" aria-label="Ilustrasi gradient artikel"></div>
            <h3 class="kartu-judul">Menyusun design token pertama</h3>
            <p class="kartu-teks">Mulai dari warna semantik, lalu kunci skala spasi dan tipografi.</p>
            <div class="kartu-meta"><span>6 menit baca</span><span>·</span><span>12 Mei</span></div>
          </a>
          <a class="kartu kartu--artikel" href="#">
            <span class="kartu-lencana">Dasar</span>
            <div class="kartu-media" role="img" aria-label="Ilustrasi gradient artikel kedua"></div>
            <h3 class="kartu-judul">Bayangan sebagai sinyal elevasi</h3>
            <p class="kartu-teks">Tiga tingkat bayangan cukup untuk membedakan lapisan antarmuka.</p>
            <div class="kartu-meta"><span>4 menit baca</span><span>·</span><span>28 Mei</span></div>
          </a>
        </div>
      </section>

      <section aria-labelledby="judul-profil">
        <h2 class="section-judul" id="judul-profil">Kartu profil</h2>
        <div class="galeri-kartu">
          <article class="kartu kartu--profil">
            <div class="kartu-avatar" aria-hidden="true">RD</div>
            <h3 class="kartu-judul">Rani Dewi</h3>
            <p class="kartu-peran">Illustrator</p>
            <button class="tombol tombol--sekunder" type="button">Ikuti</button>
          </article>
          <article class="kartu kartu--profil">
            <div class="kartu-avatar" aria-hidden="true">BN</div>
            <h3 class="kartu-judul">Bima Nugraha</h3>
            <p class="kartu-peran">Frontend engineer</p>
            <button class="tombol tombol--sekunder" type="button">Ikuti</button>
          </article>
        </div>
      </section>

      <section aria-labelledby="judul-statistik">
        <h2 class="section-judul" id="judul-statistik">Kartu statistik</h2>
        <div class="galeri-kartu">
          <article class="kartu kartu--statistik">
            <span class="kartu-lencana">Minggu ini</span>
            <span class="kartu-angka">1.284</span>
            <p class="kartu-teks">Kartu dirender dari satu set token yang sama.</p>
            <span class="kartu-tren kartu-tren--naik">Naik 12%</span>
          </article>
          <article class="kartu kartu--statistik">
            <span class="kartu-lencana">Performa</span>
            <span class="kartu-angka">96 ms</span>
            <p class="kartu-teks">Waktu penerapan stylesheet pada halaman galeri.</p>
            <span class="kartu-tren kartu-tren--turun">Turun 4%</span>
          </article>
        </div>
      </section>
    </div>
  </main>

  <footer class="laman-kaki">
    <div class="pembungkus kaki-baris">
      <span>Koleksi Kartu UI — proyek latihan CSS</span>
      <span>Semua kartu dibangun dari satu berkas stylesheet</span>
    </div>
  </footer>
</body>
</html>
js
/* ============================================================
   main.js — toggle tema terang/gelap untuk Koleksi Kartu UI.
   Tema disimpan sebagai atribut [data-tema="gelap"] pada <html>;
   stylesheet tinggal menimpa token warna saat atribut itu ada.
   ============================================================ */
(function () {
  "use strict";

  var akar = document.documentElement;
  var tombol = document.getElementById("tombol-tema");
  var label = document.getElementById("label-tema");

  if (!tombol || !label) {
    return;
  }

  function terapkanTema(nama) {
    var gelap = nama === "gelap";
    if (gelap) {
      akar.setAttribute("data-tema", "gelap");
    } else {
      akar.removeAttribute("data-tema");
    }
    label.textContent = gelap ? "Mode terang" : "Mode gelap";
    tombol.setAttribute("aria-pressed", gelap ? "true" : "false");
  }

  tombol.addEventListener("click", function () {
    var gelapSaatIni = akar.getAttribute("data-tema") === "gelap";
    terapkanTema(gelapSaatIni ? "terang" : "gelap");
  });

  /* Kondisi awal mengikuti setelan sistem pengguna, tanpa penyimpanan. */
  var awalGelap = window.matchMedia &&
    window.matchMedia("(prefers-color-scheme: dark)").matches;
  terapkanTema(awalGelap ? "gelap" : "terang");
})();

Ringkasan #

Proyek ditutup dengan lapisan pengalaman: hover yang bercerita sesuai peran kartu, cincin fokus :focus-visible untuk keyboard, prefers-reduced-motion yang menurunkan gerak, dan tema gelap yang hanya menulis ulang token. Perjalanan lima bagian ini memperlihatkan pola besar sistem desain CSS: token dulu, komponen berikutnya, lalu variasi, kedalaman, ritme, dan akhirnya state — setiap lapis dibangun di atas lapis sebelumnya tanpa membongkar yang lama.