Setelah puluhan chapter membahas satu properti dalam satu waktu, kini saatnya merangkainya menjadi produk yang benar-benar bisa dipakai. Proyek ini membangun Katalog Tanaman: satu halaman katalog daring dengan bilah filter di sisi kiri, delapan kartu produk pada grid yang menyesuaikan lebar layar, lencana kategori (Indoor, Rimbu, Solar), serta area foto yang digambar penuh dengan lapisan gradasi warna — tanpa satu pun berkas gambar. Kita mulai dari token dan kerangka halaman, lalu membangun kartu, grid produk, dan panel filter, dan menutup dengan perilaku responsif plus penyaringan kategori memakai sedikit JavaScript. Setiap bagian menyimpan snapshot berkasnya sendiri, sehingga stylesheet tumbuh bertahap dari 91 baris menjadi 242 baris dan kalian bisa mengikuti setiap keputusannya.
Perilaku yang Diharapkan #
Halaman akhirnya memiliki header berisi logo dan tagline, bilah filter yang menempel saat digulir, grid berisi delapan kartu tanaman, dan footer. Perilaku yang bisa dicoba langsung:
- Menyusutkan jendela browser: grid kartu menghitung ulang jumlah kolomnya sendiri berkat
repeat(auto-fit, minmax(210px, 1fr)), dan di bawah 880 px bilah filter pindah menumpuk di atas daftar produk. - Mengarahkan kursor ke kartu mana pun mengangkatnya 3 px dan menaikkan bayangannya; tombol dan tombol filter memberi umpan balik warna.
- Menekan tombol kategori menyembunyikan kartu yang tidak cocok lewat class
tersembunyi, memperbarui angka “tanaman ditemukan”, dan menandai tombol aktif; tombol Atur ulang mengembalikan semuanya. - Bila sistem disetel mengurangi gerak, seluruh transisi dimatikan otomatis.
Cuplikan berikut meniru grid katalog final — kecilkan lebar panel demo dan lihat kolomnya menyesuaikan diri:
Struktur Folder #
katalog-tanaman/
├── _index.md
├── src/
│ ├── index.html
│ ├── css/
│ │ └── styles.css
│ ├── js/
│ │ └── main.js
│ └── assets/
│ └── logo.svg
├── part-1-layout-katalog/
│ ├── index.md
│ └── styles.css
├── part-2-product-card/
│ ├── index.md
│ ├── styles.css
│ └── beranda.html
├── part-3-product-grid/
│ ├── index.md
│ ├── styles.css
│ └── beranda.html
├── part-4-filter-panel-layout/
│ ├── index.md
│ ├── styles.css
│ └── beranda.html
└── part-5-responsive-catalog/
├── index.md
├── styles.css
├── beranda.html
└── main.jsTahapan Pengerjaan #
- Bagian 1: Layout Katalog — token hijau, base reset, kerangka header dan footer, serta grid dua kolom yang membagi ruang untuk bilah filter dan katalog.
- Bagian 2: Kartu Produk — anatomi kartu: area media berlapis gradasi, lencana kategori, badan kartu, dan baris beli.
- Bagian 3: Grid Produk
— satu deklarasi
repeat(auto-fit, minmax())untuk semua jumlah kolom, gap, dan baris info hasil. - Bagian 4: Panel Filter — tombol kategori, daftar cek layanan, tampilan rentang harga statis, dan tombol atur ulang.
- Bagian 5: Katalog Responsif
— media query satu kolom, penyaringan kategori dengan
main.js, state fokus, danprefers-reduced-motion.
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:
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Katalog Tanaman</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 Katalog Tanaman" width="40" height="40">
<span class="brand-nama">Katalog Tanaman</span>
</div>
<p class="tagline">Rumah tanam mini untuk semua rumah</p>
</div>
</header>
<main class="laman-utama">
<div class="pembungkus">
<h1 class="judul-halaman">Katalog tanaman hias</h1>
<p class="pengantar">Delapan tanaman pilihan untuk memulai koleksi: dari penghuni sudut ruangan sampai penyuka matahari di teras.</p>
<div class="layout-katalog">
<aside class="bilah-filter" aria-label="Filter katalog">
<section class="panel-filter">
<h2 class="panel-judul">Kategori</h2>
<div class="daftar-filter">
<button class="filter-tombol aktif" type="button" data-kategori="semua" aria-pressed="true">Semua tanaman</button>
<button class="filter-tombol" type="button" data-kategori="indoor" aria-pressed="false">Indoor</button>
<button class="filter-tombol" type="button" data-kategori="rimbu" aria-pressed="false">Rimbu</button>
<button class="filter-tombol" type="button" data-kategori="solar" aria-pressed="false">Solar</button>
</div>
</section>
<section class="panel-filter">
<h2 class="panel-judul">Rentang harga</h2>
<div class="geser-harga" aria-hidden="true">
<div class="geser-track"><span class="geser-isi"></span><span class="geser-thumb"></span></div>
</div>
<p class="geser-label"><span>Rp25.000</span><span>Rp150.000</span></p>
</section>
<section class="panel-filter">
<h2 class="panel-judul">Layanan toko</h2>
<ul class="daftar-cek">
<li><label><input type="checkbox"> Gratis ongkir</label></li>
<li><label><input type="checkbox"> Dikirim hari ini</label></li>
<li><label><input type="checkbox"> Garansi tumbuh</label></li>
</ul>
</section>
<button class="tombol tombol--sekunder" id="tombol-reset" type="button">Atur ulang</button>
</aside>
<section class="isi-katalog" aria-label="Daftar tanaman">
<p class="info-hasil"><span id="hitung-katalog">8</span> tanaman ditemukan</p>
<div class="grid-produk">
<article class="kartu-tanaman" data-kategori="indoor">
<div class="media-tanaman media--monstera" role="img" aria-label="Ilustrasi gradasi monstera">
<span class="lencana lencana--indoor">Indoor</span>
<span class="lencana lencana--promo">Terlaris</span>
</div>
<div class="badan-kartu">
<h2 class="nama-tanaman">Monstera Deliciosa</h2>
<p class="keterangan">Daun lebar bercelah khas; tumbuh rapi di sudut ruangan.</p>
<div class="baris-beli"><span class="harga">Rp95.000</span><button class="tombol" type="button">Tambah</button></div>
</div>
</article>
<article class="kartu-tanaman" data-kategori="indoor">
<div class="media-tanaman media--lidah" role="img" aria-label="Ilustrasi gradasi lidah mertua">
<span class="lencana lencana--indoor">Indoor</span>
</div>
<div class="badan-kartu">
<h2 class="nama-tanaman">Lidah Mertua</h2>
<p class="keterangan">Daun tegak berpola garis; toleran penyiraman jarang.</p>
<div class="baris-beli"><span class="harga">Rp45.000</span><button class="tombol" type="button">Tambah</button></div>
</div>
</article>
<article class="kartu-tanaman" data-kategori="rimbu">
<div class="media-tanaman media--aglaonema" role="img" aria-label="Ilustrasi gradasi aglaonema">
<span class="lencana lencana--rimbu">Rimbu</span>
<span class="lencana lencana--promo">Baru</span>
</div>
<div class="badan-kartu">
<h2 class="nama-tanaman">Aglaonema Merah</h2>
<p class="keterangan">Daun merah marun bermotif; senang tempat teduh.</p>
<div class="baris-beli"><span class="harga">Rp78.000</span><button class="tombol" type="button">Tambah</button></div>
</div>
</article>
<article class="kartu-tanaman" data-kategori="rimbu">
<div class="media-tanaman media--calathea" role="img" aria-label="Ilustrasi gradasi calathea">
<span class="lencana lencana--rimbu">Rimbu</span>
</div>
<div class="badan-kartu">
<h2 class="nama-tanaman">Calathea Orbifolia</h2>
<p class="keterangan">Daun bulat bergaris silver; butuh kelembapan tinggi.</p>
<div class="baris-beli"><span class="harga">Rp110.000</span><button class="tombol" type="button">Tambah</button></div>
</div>
</article>
<article class="kartu-tanaman" data-kategori="rimbu">
<div class="media-tanaman media--pakis" role="img" aria-label="Ilustrasi gradasi pakis hias">
<span class="lencana lencana--rimbu">Rimbu</span>
</div>
<div class="badan-kartu">
<h2 class="nama-tanaman">Pakis Hias</h2>
<p class="keterangan">Rumpun daun rimbun untuk beranda yang teduh.</p>
<div class="baris-beli"><span class="harga">Rp52.000</span><button class="tombol" type="button">Tambah</button></div>
</div>
</article>
<article class="kartu-tanaman" data-kategori="solar">
<div class="media-tanaman media--bougenville" role="img" aria-label="Ilustrasi gradasi bougenville">
<span class="lencana lencana--solar">Solar</span>
<span class="lencana lencana--promo">Terlaris</span>
</div>
<div class="badan-kartu">
<h2 class="nama-tanaman">Bougenville</h2>
<p class="keterangan">Bunga merah muda; mekar deras di matahari penuh.</p>
<div class="baris-beli"><span class="harga">Rp60.000</span><button class="tombol" type="button">Tambah</button></div>
</div>
</article>
<article class="kartu-tanaman" data-kategori="solar">
<div class="media-tanaman media--kamboja" role="img" aria-label="Ilustrasi gradasi kamboja">
<span class="lencana lencana--solar">Solar</span>
</div>
<div class="badan-kartu">
<h2 class="nama-tanaman">Kamboja Jepang</h2>
<p class="keterangan">Bunga kuning lembut; kuat di teras yang panas.</p>
<div class="baris-beli"><span class="harga">Rp85.000</span><button class="tombol" type="button">Tambah</button></div>
</div>
</article>
<article class="kartu-tanaman" data-kategori="solar">
<div class="media-tanaman media--melati" role="img" aria-label="Ilustrasi gradasi melati">
<span class="lencana lencana--solar">Solar</span>
<span class="lencana lencana--promo">Baru</span>
</div>
<div class="badan-kartu">
<h2 class="nama-tanaman">Melati</h2>
<p class="keterangan">Putih wangi sepanjang tahun; primadona pekarangan.</p>
<div class="baris-beli"><span class="harga">Rp38.000</span><button class="tombol" type="button">Tambah</button></div>
</div>
</article>
</div>
</section>
</div>
</div>
</main>
<footer class="laman-kaki">
<div class="pembungkus kaki-baris">
<span>Katalog Tanaman — proyek latihan CSS</span>
<span>Semua foto tanaman digantikan gradasi warna</span>
</div>
</footer>
</body>
</html>/* ============================================================
Katalog Tanaman — stylesheet utama
Urutan: token → base → kerangka halaman → layout katalog
→ tombol → kartu tanaman → grid produk → panel filter
→ state dan gerak → responsive
============================================================ */
/* ---------- 1. Design token ---------- */
:root {
/* Warna semantik tema hijau */
--warna-latar: #f3f7f2; --warna-permukaan: #ffffff;
--warna-teks: #1f2a1d; --warna-teks-lembut: #5b6b57;
--warna-garis: #d9e4d5; --warna-utama: #2f6b3a;
--warna-utama-pekat: #215029; --warna-utama-kontras: #ffffff;
--warna-utama-lembut: #e3f0e0; --warna-aksen: #b3372b;
--warna-indoor: #2e5f8a; --warna-solar: #8a5a12;
--warna-fokus: #1d4ed8;
/* Skala spasi 4/8 px */
--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 dan bayangan */
--radius-kecil: 6px; --radius: 12px; --radius-penuh: 999px;
--bayangan-1: 0 1px 2px rgba(31, 42, 29, 0.08);
--bayangan-2: 0 6px 16px rgba(31, 42, 29, 0.14);
/* Tipografi dan gerak */
--font-dasar: system-ui, "Segoe UI", sans-serif;
--tinggi-baris: 1.55;
--durasi-cepat: 120ms; --durasi: 200ms; --pelunakan: ease-out;
/* Kerangka halaman */
--lebar-isi: 1120px;
--lebar-bilah: 264px;
}
/* ---------- 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: var(--jarak-4); }
.laman-kepala { padding-block: var(--jarak-5); background-color: var(--warna-permukaan); border-bottom: 1px solid var(--warna-garis); }
.kepala-baris { display: flex; align-items: center; justify-content: space-between; gap: var(--jarak-3); flex-wrap: wrap; }
.brand { display: flex; align-items: center; gap: var(--jarak-3); }
.brand img { width: 40px; height: 40px; }
.brand-nama { font-size: 20px; font-weight: 700; }
.tagline { font-size: 14px; color: var(--warna-teks-lembut); }
.laman-utama { padding-block: var(--jarak-7) var(--jarak-8); }
.judul-halaman { font-size: 28px; font-weight: 800; letter-spacing: -0.01em; }
.pengantar { margin-top: var(--jarak-2); max-width: 62ch; color: var(--warna-teks-lembut); }
.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: var(--jarak-2) var(--jarak-4); padding-block: var(--jarak-5); font-size: 13.5px; color: var(--warna-teks-lembut); }
/* ---------- 4. Layout katalog: bilah filter dan isi ---------- */
.layout-katalog {
display: grid;
grid-template-columns: var(--lebar-bilah) minmax(0, 1fr);
gap: var(--jarak-6);
align-items: start;
margin-top: var(--jarak-6);
}
.bilah-filter {
position: sticky; top: var(--jarak-5);
display: flex; flex-direction: column;
gap: var(--jarak-4);
}
.isi-katalog { min-width: 0; }
/* ---------- 5. Tombol ---------- */
.tombol {
padding: 9px 18px; border: none; border-radius: var(--radius-kecil);
background-color: var(--warna-utama);
color: var(--warna-utama-kontras);
font: inherit; font-weight: 600; cursor: pointer;
}
/* ---------- 6. Kartu tanaman ---------- */
.kartu-tanaman {
display: flex; flex-direction: column;
overflow: hidden;
background-color: var(--warna-permukaan);
border: 1px solid var(--warna-garis); border-radius: var(--radius);
box-shadow: var(--bayangan-1);
}
.media-tanaman {
aspect-ratio: 4 / 3;
display: flex; flex-direction: column;
align-items: flex-start; gap: var(--jarak-2);
padding: var(--jarak-3);
background-color: var(--warna-utama-lembut);
}
/* Foto produk digantikan dua lapis gradasi: lapisan pertama
kilau cahaya, lapisan kedua warna khas tiap tanaman. */
.media--monstera { background-image: linear-gradient(160deg, rgba(255, 255, 255, 0.30) 0%, rgba(255, 255, 255, 0) 55%), linear-gradient(135deg, #8fce8f 0%, #2f6b3a 100%); }
.media--lidah { background-image: linear-gradient(160deg, rgba(255, 255, 255, 0.22) 0%, rgba(255, 255, 255, 0) 50%), linear-gradient(135deg, #3d7a5f 0%, #163823 100%); }
.media--aglaonema { background-image: linear-gradient(160deg, rgba(255, 255, 255, 0.30) 0%, rgba(255, 255, 255, 0) 55%), linear-gradient(135deg, #c25b5b 0%, #6b2f4a 100%); }
.media--calathea { background-image: linear-gradient(160deg, rgba(255, 255, 255, 0.28) 0%, rgba(255, 255, 255, 0) 55%), linear-gradient(135deg, #4f8a8b 0%, #274156 100%); }
.media--pakis { background-image: linear-gradient(160deg, rgba(255, 255, 255, 0.28) 0%, rgba(255, 255, 255, 0) 55%), linear-gradient(135deg, #679436 0%, #2c4a1e 100%); }
.media--bougenville { background-image: linear-gradient(160deg, rgba(255, 255, 255, 0.32) 0%, rgba(255, 255, 255, 0) 55%), linear-gradient(135deg, #d16ba5 0%, #7a2f5c 100%); }
.media--kamboja { background-image: linear-gradient(160deg, rgba(255, 255, 255, 0.40) 0%, rgba(255, 255, 255, 0) 60%), linear-gradient(135deg, #f2e3b3 0%, #d9a441 100%); }
.media--melati { background-image: linear-gradient(160deg, rgba(255, 255, 255, 0.45) 0%, rgba(255, 255, 255, 0) 60%), linear-gradient(135deg, #eef7e9 0%, #a8c98a 100%); }
.lencana {
padding: 3px 10px; border-radius: var(--radius-penuh);
font-size: 11px; font-weight: 700;
letter-spacing: 0.06em; text-transform: uppercase;
}
.lencana--indoor { background-color: var(--warna-indoor); color: #ffffff; }
.lencana--rimbu { background-color: var(--warna-utama-lembut); color: var(--warna-utama-pekat); }
.lencana--solar { background-color: #f7e3b8; color: var(--warna-solar); }
.lencana--promo { background-color: var(--warna-aksen); color: #ffffff; }
.badan-kartu { display: flex; flex-direction: column; gap: var(--jarak-2); flex: 1; padding: var(--jarak-4); }
.nama-tanaman { font-size: 17px; font-weight: 700; line-height: 1.3; }
.keterangan { font-size: 13.5px; color: var(--warna-teks-lembut); }
.baris-beli {
display: flex; align-items: center;
justify-content: space-between; gap: var(--jarak-2);
margin-top: auto; padding-top: var(--jarak-2);
}
.harga { font-size: 16px; font-weight: 700; color: var(--warna-utama-pekat); }
/* ---------- 7. Grid produk ---------- */
.info-hasil { margin-bottom: var(--jarak-4); font-size: 14px; color: var(--warna-teks-lembut); }
.grid-produk {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
gap: var(--jarak-5);
}
/* ---------- 8. Panel filter ---------- */
.panel-filter {
padding: var(--jarak-4);
background-color: var(--warna-permukaan);
border: 1px solid var(--warna-garis); border-radius: var(--radius);
}
.panel-judul { font-size: 12.5px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--warna-teks-lembut); }
.daftar-filter { display: grid; gap: var(--jarak-2); margin-top: var(--jarak-3); }
.filter-tombol {
padding: 8px 12px; border: 1px solid var(--warna-garis);
border-radius: var(--radius-kecil);
background-color: var(--warna-permukaan);
font: inherit; font-size: 14px; text-align: left; cursor: pointer;
}
.filter-tombol.aktif { border-color: var(--warna-utama); background-color: var(--warna-utama-lembut); color: var(--warna-utama-pekat); font-weight: 600; }
.daftar-cek { display: grid; gap: var(--jarak-2); margin-top: var(--jarak-3); padding: 0; list-style: none; }
.daftar-cek label { display: flex; align-items: center; gap: var(--jarak-2); font-size: 14px; }
.daftar-cek input { width: 16px; height: 16px; accent-color: var(--warna-utama); }
.geser-harga { margin-top: var(--jarak-4); }
.geser-track { position: relative; height: 6px; border-radius: var(--radius-penuh); background-color: var(--warna-garis); }
.geser-isi { position: absolute; inset: 0 18% 0 6%; border-radius: var(--radius-penuh); background-color: var(--warna-utama); }
.geser-thumb {
position: absolute; top: 50%; left: 30%;
width: 16px; height: 16px;
border: 3px solid var(--warna-utama); border-radius: var(--radius-penuh);
background-color: var(--warna-permukaan);
transform: translate(-50%, -50%);
}
.geser-label { display: flex; justify-content: space-between; margin-top: var(--jarak-2); font-size: 12.5px; color: var(--warna-teks-lembut); }
.tombol--sekunder { background-color: var(--warna-utama-lembut); color: var(--warna-utama-pekat); }
.bilah-filter .tombol--sekunder { margin-top: var(--jarak-2); }
/* ---------- 9. State dan gerak ---------- */
.tersembunyi { display: none; }
.kartu-tanaman {
transition: box-shadow var(--durasi) var(--pelunakan), transform var(--durasi) var(--pelunakan);
}
.kartu-tanaman:hover {
box-shadow: var(--bayangan-2);
transform: translateY(-3px);
}
.tombol { transition: background-color var(--durasi-cepat) var(--pelunakan); }
.tombol:hover { background-color: var(--warna-utama-pekat); }
.tombol:active { transform: translateY(1px); }
.tombol--sekunder:hover { background-color: var(--warna-garis); }
.filter-tombol { transition: border-color var(--durasi-cepat) var(--pelunakan); }
.filter-tombol:hover { border-color: var(--warna-utama); }
:focus-visible {
outline: 2px solid var(--warna-fokus);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
.kartu-tanaman, .tombol, .filter-tombol { transition: none; }
.kartu-tanaman:hover { transform: none; }
}
/* ---------- 10. Responsive ---------- */
@media (max-width: 880px) {
.layout-katalog { grid-template-columns: 1fr; }
.bilah-filter { position: static; }
.daftar-filter { grid-template-columns: repeat(2, 1fr); }
.judul-halaman { font-size: 24px; }
}(function () {
"use strict";
var tombolFilter = document.querySelectorAll(".filter-tombol");
var kartu = document.querySelectorAll(".kartu-tanaman");
var hitungan = document.getElementById("hitung-katalog");
var tombolReset = document.getElementById("tombol-reset");
function terapkanFilter(kategori) {
var jumlah = 0;
kartu.forEach(function (item) {
var cocok = kategori === "semua" ||
item.getAttribute("data-kategori") === kategori;
item.classList.toggle("tersembunyi", !cocok);
if (cocok) {
jumlah += 1;
}
});
if (hitungan) {
hitungan.textContent = String(jumlah);
}
tombolFilter.forEach(function (tombol) {
var aktif = tombol.getAttribute("data-kategori") === kategori;
tombol.classList.toggle("aktif", aktif);
tombol.setAttribute("aria-pressed", aktif ? "true" : "false");
});
}
tombolFilter.forEach(function (tombol) {
tombol.addEventListener("click", function () {
terapkanFilter(tombol.getAttribute("data-kategori"));
});
});
if (tombolReset) {
tombolReset.addEventListener("click", function () {
terapkanFilter("semua");
});
}
terapkanFilter("semua");
})();Menyiapkan token hijau, base reset, kerangka header dan footer, serta grid dua kolom yang membagi ruang untuk bilah filter dan katalog produk.
Buka → # Bagian 2: Kartu ProdukMembangun anatomi kartu tanaman: area media berlapis gradasi, lencana kategori, badan kartu, dan baris beli yang sejajar antar kartu.
Buka → # Bagian 3: Grid ProdukMengubah tumpukan kartu menjadi grid yang menghitung jumlah kolomnya sendiri dengan repeat(auto-fit, minmax()), plus gap dan baris info hasil.
Buka → # Bagian 4: Panel FilterMerapikan bilah kiri dengan panel tombol kategori, daftar cek layanan, tampilan rentang harga statis, dan tombol atur ulang.
Buka → # Bagian 5: Katalog ResponsifMenutup proyek: media query satu kolom untuk layar sempit, penyaringan kategori dengan main.js, cincin fokus, dan prefers-reduced-motion.
Buka →