Materi CSS selama ini baru terasa utuh bila dirangkai menjadi satu produk nyata. Proyek ini membangun Koleksi Kartu UI: satu halaman galeri yang memamerkan empat keluarga kartu — produk, artikel, profil, dan statistik — di atas sistem desain kecil yang kita bangun sendiri, mulai dari design token, varian komponen, bayangan, tipografi dan spasi, sampai state hover, fokus keyboard, dan tema gelap. Setiap bagian mengerjakan satu lapis dan menyimpan snapshot berkasnya sendiri, sehingga kalian bisa mengikuti stylesheet tumbuh dari 71 baris menjadi lebih dari tiga ratus baris.

Perilaku yang Diharapkan #

Halaman akhirnya memiliki header berisi logo dan tombol tema, deretan sampel token warna, empat seksi galeri kartu, dan footer. Perilaku yang bisa dicoba langsung:

  • Mengarahkan kursor ke kartu mana pun mengangkatnya 2 px dan menaikkan bayangannya; kartu artikel justru memberi garis bawah pada judulnya.
  • Menekan Tab memunculkan cincin fokus biru pada tombol dan kartu tautan, dan bila sistem disetel mengurangi gerak, seluruh transisi mati sendiri.
  • Tombol Mode gelap memasang atribut data-tema="gelap" pada <html>; karena semua komponen membaca token, satu atribut itu menggelapkan seluruh halaman tanpa menyentuh CSS komponen.

Cuplikan berikut meniru keluarga kartu produk dari galeri final — arahkan kursor, lalu tekan Tab untuk merasakan cincin fokusnya:

Struktur Folder #

text
koleksi-kartu-ui/
├── _index.md
├── src/
│   ├── index.html
│   ├── css/
│   │   └── styles.css
│   ├── js/
│   │   └── main.js
│   └── assets/
│       └── logo.svg
├── part-1-design-tokens/
│   ├── index.md
│   └── styles.css
├── part-2-variasi-kartu/
│   ├── index.md
│   └── styles.css
├── part-3-shadows-dan-borders/
│   ├── index.md
│   └── styles.css
├── part-4-typography-dan-spacing/
│   ├── index.md
│   └── styles.css
└── part-5-hover-dan-focus-states/
    ├── index.md
    ├── styles.css
    ├── index.html
    └── main.js

Tahapan Pengerjaan #

  1. Bagian 1: Design Token — fondasi berupa token warna semantik, skala spasi 4/8 px, radius, bayangan, tipografi, gerak, dan base reset.
  2. Bagian 2: Variasi Kartu — kerangka halaman, tombol, kartu dasar, dan empat varian: produk, artikel, profil, statistik.
  3. Bagian 3: Bayangan dan Border — tiga tingkat elevasi dari token serta aksen border pada kartu statistik dan avatar.
  4. Bagian 4: Tipografi dan Spasi — nilai px diikat ke token, ritme antarseksi, lencana kapital, dan huruf monospace untuk nama token.
  5. Bagian 5: Hover dan Fokus — transisi, state hover per peran kartu, cincin fokus :focus-visible, prefers-reduced-motion, dan tema gelap dari penimpaan token.

Ikuti urutannya, karena setiap bagian membangun di atas snapshot bagian sebelumnya.

Berkas Sumber Lengkap #

Tiga berkas berikut adalah kondisi akhir proyek, identik dengan snapshot Bagian 5. Gunakan tombol unduh pada header tiap code block untuk menyimpannya, lalu susun folder sesuai struktur di atas. Snapshot halaman ditampilkan dengan label beranda.html; saat dipraktikkan, simpan isinya sebagai index.html:

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>
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);
}
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");
})();