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.
/* ============================================================
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);
}<!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>/* ============================================================
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.