Bagian penutup ini mengerjakan dua hal yang membuat katalog terasa selesai. Pertama, layout belajar menata dirinya di layar sempit: bilah filter pindah menumpuk di atas daftar produk dan tombol kategori dirapatkan menjadi dua kolom. Kedua, filter mulai bekerja: main.js sekitar 50 baris menghubungkan tombol kategori dengan kartu lewat atribut data-kategori, menyembunyikan kartu yang tidak cocok, dan memperbarui angka hasil. Snapshot pada bagian ini adalah kondisi akhir proyek dan identik dengan berkas di src/ — HTML pun mendapat dua perubahan kecil: atribut data-kategori pada setiap artikel dan tag <script> di <head>.
Menata Ulang untuk Layar Sempit #
Grid produk dari Bagian 3 tidak butuh apa-apa: auto-fit sudah menghitung ulang kolomnya sendiri. Yang perlu media query hanyalah hubungan bilah dan katalog:
@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; }
}Di bawah 880 px, grid-template-columns berubah menjadi satu kolom, dan karena <aside> ditulis sebelum <section> di HTML, bilah filter otomatis muncul di atas katalog tanpa order apa pun. position: static melepas sticky yang justru mengganggu saat bilah setinggi layar penuh, repeat(2, 1fr) merapatkan empat tombol kategori menjadi dua baris ringkas, dan judul halaman diturunkan ke 24 px. Perhatikan pola responsif proyek ini: keputusan berbasis konten (lebar kolom kartu) diserahkan ke auto-fit, dan media query hanya menyentuh keputusan struktur halaman — satu breakpoint sudah cukup.
Filter Kategori dengan JavaScript #
main.js berpola IIFE dengan "use strict" dan tiga pegangan: document.querySelectorAll(".filter-tombol"), document.querySelectorAll(".kartu-tanaman"), serta elemen #hitung-katalog dan #tombol-reset. Jantungnya fungsi terapkanFilter(kategori):
kartu.forEach(function (item) {
var cocok = kategori === "semua" ||
item.getAttribute("data-kategori") === kategori;
item.classList.toggle("tersembunyi", !cocok);
if (cocok) {
jumlah += 1;
}
});Setiap kartu dibandingkan dengan kategori terpilih; class tersembunyi dipasang atau dilepas lewat classList.toggle, dan aturan .tersembunyi { display: none; } pada stylesheet mengeluarkannya dari grid — karena itu auto-fit langsung mengisi ulang baris. Jumlah kartu yang terlihat ditulis ke #hitung-katalog lewat textContent. Fungsi yang sama menyinkronkan tombol: class aktif (yang gayanya sudah disiapkan Bagian 4) dan atribut aria-pressed pindah ke tombol terpilih. Setiap tombol filter memasang pendengar klik yang memanggil terapkanFilter dengan data-kategori-nya, tombol reset memanggilnya dengan "semua", dan pemanggilan awal terapkanFilter("semua") menyamakan tampilan dengan HTML sejak halaman terbuka. Sengaja tidak ada localStorage: keadaan filter hidup hanya di memori halaman, sehingga kode tetap kecil dan aman di lingkungan pratinjau.
Fokus, Gerak, dan Sentuhan Akhir #
Tiga aturan state menutup stylesheet. :focus-visible global memberi cincin biru 2 px hanya untuk fokus keyboard, sehingga klik tetap bersih sementara pengguna Tab selalu tahu posisinya. Hover kartu mengangkatnya 3 px dengan translateY(-3px) dan menaikkan bayangan ke --bayangan-2, tombol merespons lewat perubahan latar 120 ms plus :active yang menurunkannya 1 px. Terakhir, @media (prefers-reduced-motion: reduce) mematikan seluruh transisi dan menghilangkan angkatan hover bagi pengguna yang memintanya. Simpan ketiga snapshot berikut, jalankan di browser, dan coba setiap perilaku yang dijanjikan overview: menyaring kategori, mengatur ulang, menyusutkan jendela, dan menekan Tab.
/* ============================================================
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; }
}<!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>(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");
})();Ringkasan #
Proyek Katalog Tanaman tuntas: kerangka halaman dan grid dua kolom dari Bagian 1, kartu berlapis gradasi dari Bagian 2, grid auto-fit dari Bagian 3, panel filter dari Bagian 4, kini bertemu media query satu kolom, penyaringan JavaScript tanpa penyimpanan, cincin :focus-visible, dan prefers-reduced-motion. Stylesheet tumbuh dari 91 menjadi 242 baris yang seluruhnya membaca token, dan setiap snapshot bagian membentuk rantai superset yang berakhir identik dengan src/. Kalian bisa memperluasnya dengan mudah: tambah kategori baru berarti satu tombol, satu modifier media, dan satu nilai data-kategori.