Semua bahan sudah terkumpul; Bagian 5 memberi galeri gerak, responsivitas penuh, dan otak kecilnya. Tiga kelompok perubahan masuk bersamaan: transisi dan efek hover pada foto, breakpoint dua kolom untuk layar menengah, serta main.js yang menyalakan empat tombol filter dan penghitungnya. Snapshot pada bagian ini adalah kondisi akhir proyek — ketiga berkasnya identik dengan folder src/, dan salinan di akar bundle berasal dari sini.
Hover dan Fokus dengan Transisi #
Gerak halaman lahir dari empat aturan berpasangan:
.galeri-foto { transition: transform var(--durasi-gerak) ease; }
.galeri-tautan:hover .galeri-foto { transform: scale(1.06); }
.galeri-keterangan {
opacity: 0;
transform: translateY(10px);
transition: opacity var(--durasi-gerak) ease, transform var(--durasi-gerak) ease;
}
.galeri-tautan:hover + .galeri-keterangan,
.galeri-item:focus-within .galeri-keterangan { opacity: 1; transform: translateY(0); }Mengarahkan kursor ke tautan memperbesar foto 6% selama 220 ms; pembesaran itu terpotong rapi karena overflow: hidden yang dipasang sejak Bagian 2. Keterangan memakai pola berbeda: opacity dan translateY digerakkan bersamaan sehingga ia muncul dari bawah sambil memudar masuk. Selector + bekerja karena figcaption memang adik langsung dari <a>, dan .galeri-item:focus-within membuka keterangan yang sama saat pengguna navigasi keyboard — hover bukan satu-satunya jalan. Tiga pelengkap menjaga kewarasan gerak ini: @media (hover: none) menampilkan keterangan permanen pada peranti sentuh, :focus-visible memberi cincin kuning pada tautan foto dan tombol filter, dan prefers-reduced-motion: reduce mematikan seluruh transisi beserta scroll-behavior. Tombol filter ikut bergerak: aria-pressed="true" diberi latar kuning penuh lewat selector atribut.
Arahkan kursor ke foto pada demo — atau tekan Tab hingga tautannya mendapat fokus — untuk memunculkan keterangan yang selama ini tersembunyi:
Breakpoint Dua Kolom #
Media query pertama proyek muncul di 940 px:
@media (max-width: 940px) {
.galeri { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.galeri-item--panorama { grid-column: span 2; }
.galeri-item--lebar { grid-column: span 2; grid-row: span 3; }
.galeri-item--besar { grid-column: span 2; grid-row: span 4; }
.galeri-item--tinggi { grid-row: span 5; }
}Aturan .galeri-item--panorama adalah kunci halus di sini: span tiga kolom tidak bisa dipenuhi grid dua kolom, dan browser akan membuat kolom implisit ketiga untuk menampungnya — merusak lebar semua kolom lain. Menurunkannya ke span 2 menjaga grid tetap dua kolom bersih. Item lain kebanyakan sudah memakai span dua kolom, sehingga cukup ditegaskan kembali; foto tinggi tetap memanjang lima baris dan aspect-ratio dari Bagian 3 menunggu giliran di bawah 620 px.
Penyaring Kategori dengan JavaScript #
Skripnya tiga puluh baris vanilla dan seluruhnya berputar pada satu fungsi:
function saringGaleri(kategori) {
let jumlahTampil = 0;
itemGaleri.forEach(function (item) {
const cocok = kategori === "semua" || item.dataset.kategori === kategori;
item.classList.toggle("tersembunyi", !cocok);
if (cocok) {
jumlahTampil += 1;
}
});
perbaruiHitungan(jumlahTampil);
}Alurnya: setiap .galeri-item membawa data-kategori yang cocok dengan data-kategori tombol; foto yang tidak lolos diberi class tersembunyi — yang didefinisikan CSS sebagai display: none — dan penghitung #hitung-galeri diperbarui lewat textContent. Dua fungsi pendamping menjaga sisanya: tandaiTombolAktif memindahkan aria-pressed sehingga pembaca layar tahu tombol mana yang aktif, dan satu pendengar klik per tombol menjalankan keduanya. Kondisi awal halaman langsung memanggil saringGaleri("semua"), dan skrip aman dijalankan di halaman yang kehilangan bilah filternya karena setiap elemen diperiksa sebelum dipakai. Cukup DOM API bawaan; tidak ada pustaka dan tidak ada penyimpanan luar.
Demo berikut membekukan keadaan sesudah tombol Makro diklik — perhatikan selector atribut [aria-pressed="true"] yang mewarnainya:
Snapshot akhir proyek:
/* ============================================================
Galeri Sudut Cahaya — stylesheet utama
Urutan: token → base → kerangka halaman → bilah filter
→ grid galeri → media foto dan object-fit → keterangan overlay
→ hover, fokus, dan transisi → responsive
============================================================ */
/* ---------- 1. Design token ---------- */
:root {
color-scheme: dark;
--warna-latar: #10151a;
--warna-permukaan: #1b232b;
--warna-teks: #f1f4f6;
--warna-teks-lembut: #9db0bd;
--warna-garis: #2b3742;
--warna-utama: #e8a13d;
--warna-utama-teks: #241703;
--warna-fokus: #ffd479;
--jarak-1: 4px;
--jarak-2: 8px;
--jarak-3: 12px;
--jarak-4: 16px;
--jarak-5: 24px;
--jarak-6: 32px;
--jarak-7: 48px;
--radius-sedang: 12px;
--radius-penuh: 999px;
--lebar-halaman: 1160px;
--unit-lintasan: 96px;
--celah-galeri: 14px;
--rasio-lanskap: 16 / 9;
--rasio-panorama: 21 / 9;
--rasio-potret: 2 / 3;
--rasio-besar: 7 / 4;
--durasi-gerak: 220ms;
}
/* ---------- 2. Base ---------- */
*,
*::before,
*::after { box-sizing: border-box; }
body {
margin: 0;
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
line-height: 1.6;
background: var(--warna-latar);
color: var(--warna-teks);
}
h1, h2, p { margin: 0; }
img { display: block; max-width: 100%; }
html { scroll-behavior: smooth; }
::selection { background: var(--warna-utama); color: var(--warna-utama-teks); }
/* ---------- 3. Kerangka halaman ---------- */
.pembungkus {
max-width: var(--lebar-halaman);
margin-inline: auto;
padding-inline: var(--jarak-4);
}
.kepala-galeri {
display: flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: space-between;
gap: var(--jarak-2);
padding-block: var(--jarak-5);
border-bottom: 1px solid var(--warna-garis);
}
.merek { font-size: 1.35rem; font-weight: 700; letter-spacing: 0.02em; color: var(--warna-utama); }
.tagline { color: var(--warna-teks-lembut); font-size: 0.95rem; }
.pengantar { max-width: 66ch; padding-block: var(--jarak-6) var(--jarak-4); }
.pengantar h2 { font-size: clamp(1.5rem, 1rem + 2.2vw, 2.1rem); line-height: 1.25; margin-bottom: var(--jarak-3); }
.pengantar p { color: var(--warna-teks-lembut); }
.kaki-galeri { margin-top: var(--jarak-7); border-top: 1px solid var(--warna-garis); }
.kaki-isi {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: var(--jarak-2);
padding-block: var(--jarak-5);
color: var(--warna-teks-lembut);
font-size: 0.9rem;
}
/* ---------- 4. Bilah filter ---------- */
.bilah-filter {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--jarak-2);
padding-block: var(--jarak-4);
}
.tombol-filter {
font: inherit;
font-size: 0.92rem;
padding: var(--jarak-1) var(--jarak-4);
border: 1px solid var(--warna-garis);
border-radius: var(--radius-penuh);
background: var(--warna-permukaan);
color: var(--warna-teks);
cursor: pointer;
}
.hitung-galeri { margin-left: auto; color: var(--warna-teks-lembut); font-size: 0.9rem; }
.hitung-galeri span { font-weight: 700; color: var(--warna-teks); }
/* ---------- 5. Grid galeri ---------- */
.galeri {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-auto-rows: var(--unit-lintasan);
grid-auto-flow: dense;
gap: var(--celah-galeri);
padding-block: var(--jarak-2) var(--jarak-5);
}
.galeri-item {
position: relative;
margin: 0;
display: flex;
flex-direction: column;
grid-row: span 2;
}
.galeri-item--lebar { grid-column: span 2; grid-row: span 3; }
.galeri-item--tinggi { grid-row: span 5; }
.galeri-item--besar { grid-column: span 2; grid-row: span 4; }
.galeri-item--panorama { grid-column: span 3; grid-row: span 3; }
/* ---------- 6. Media foto dan object-fit ---------- */
.galeri-tautan { display: block; flex: 1; min-height: 0; border-radius: var(--radius-sedang); }
.galeri-media {
height: 100%;
overflow: hidden;
border-radius: var(--radius-sedang);
}
.galeri-foto {
width: 100%;
height: 100%;
object-fit: cover;
}
.galeri-media--pemandangan { background: linear-gradient(165deg, #2e6f5e 0%, #17453c 70%); }
.galeri-media--urban { background: linear-gradient(165deg, #3d4a5c 0%, #1f2733 70%); }
.galeri-media--makro { background: linear-gradient(165deg, #6d4a2f 0%, #2f1f14 70%); }
/* ---------- 7. Keterangan dan overlay ---------- */
.galeri-keterangan {
position: absolute;
inset: auto 0 0 0;
display: flex;
flex-direction: column;
gap: 2px;
padding: var(--jarak-6) var(--jarak-4) var(--jarak-3);
border-radius: 0 0 var(--radius-sedang) var(--radius-sedang);
background: linear-gradient(to top, rgba(9, 13, 17, 0.92) 0%, rgba(9, 13, 17, 0.55) 55%, rgba(9, 13, 17, 0) 100%);
color: var(--warna-teks);
}
.galeri-judul { font-size: 0.98rem; font-weight: 600; line-height: 1.35; }
.galeri-meta { font-size: 0.78rem; letter-spacing: 0.04em; color: var(--warna-teks-lembut); }
/* ---------- 8. Hover, fokus, dan transisi ---------- */
.galeri-foto { transition: transform var(--durasi-gerak) ease; }
.galeri-tautan:hover .galeri-foto { transform: scale(1.06); }
.galeri-keterangan {
opacity: 0;
transform: translateY(10px);
transition: opacity var(--durasi-gerak) ease, transform var(--durasi-gerak) ease;
}
.galeri-tautan:hover + .galeri-keterangan,
.galeri-item:focus-within .galeri-keterangan { opacity: 1; transform: translateY(0); }
@media (hover: none) {
.galeri-keterangan { opacity: 1; transform: none; transition: none; }
}
.tombol-filter { transition: background var(--durasi-gerak) ease, color var(--durasi-gerak) ease, border-color var(--durasi-gerak) ease; }
.tombol-filter:hover { border-color: var(--warna-utama); color: var(--warna-utama); }
.tombol-filter[aria-pressed="true"] { background: var(--warna-utama); border-color: var(--warna-utama); color: var(--warna-utama-teks); font-weight: 600; }
.galeri-tautan:focus-visible { outline: 3px solid var(--warna-fokus); outline-offset: 3px; }
.tombol-filter:focus-visible { outline: 3px solid var(--warna-fokus); outline-offset: 2px; }
.galeri-item.tersembunyi { display: none; }
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
.galeri-foto, .galeri-keterangan, .tombol-filter { transition: none; }
.galeri-tautan:hover .galeri-foto { transform: none; }
.galeri-keterangan { transform: none; }
}
/* ---------- 9. Responsive ---------- */
@media (max-width: 940px) {
.galeri { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.galeri-item--panorama { grid-column: span 2; }
.galeri-item--lebar { grid-column: span 2; grid-row: span 3; }
.galeri-item--besar { grid-column: span 2; grid-row: span 4; }
.galeri-item--tinggi { grid-row: span 5; }
}
@media (max-width: 620px) {
.galeri { grid-template-columns: minmax(0, 1fr); grid-auto-rows: auto; }
.galeri-item, .galeri-item--lebar, .galeri-item--besar, .galeri-item--panorama, .galeri-item--tinggi { grid-column: auto; grid-row: auto; }
.galeri-media { height: auto; aspect-ratio: var(--rasio-lanskap); }
.galeri-item--tinggi .galeri-media { aspect-ratio: var(--rasio-potret); }
.galeri-item--lebar .galeri-media, .galeri-item--panorama .galeri-media { aspect-ratio: var(--rasio-panorama); }
.galeri-item--besar .galeri-media { aspect-ratio: var(--rasio-besar); }
.hitung-galeri { margin-left: 0; width: 100%; }
.kaki-isi { flex-direction: column; }
}<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Galeri Sudut Cahaya</title>
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<header class="kepala-galeri pembungkus">
<h1 class="merek">Galeri Sudut Cahaya</h1>
<p class="tagline">Koleksi foto pemandangan, urban, dan makro</p>
</header>
<main class="pembungkus">
<section class="pengantar">
<h2>Sepuluh momen pilihan</h2>
<p>Sepuluh foto dikurasi dari tiga kategori dan disusun pada grid yang ritmenya mengikuti rasio masing-masing bidang. Gunakan bilah filter untuk menyaring koleksi, dan arahkan kursor ke foto untuk melihat keterangannya.</p>
</section>
<div class="bilah-filter" id="bilah-filter" role="group" aria-label="Saring foto menurut kategori">
<button type="button" class="tombol-filter" data-kategori="semua" aria-pressed="true">Semua</button>
<button type="button" class="tombol-filter" data-kategori="pemandangan" aria-pressed="false">Pemandangan</button>
<button type="button" class="tombol-filter" data-kategori="urban" aria-pressed="false">Urban</button>
<button type="button" class="tombol-filter" data-kategori="makro" aria-pressed="false">Makro</button>
<p class="hitung-galeri">Menampilkan <span id="hitung-galeri">10</span> foto</p>
</div>
<div class="galeri">
<figure class="galeri-item galeri-item--lebar" data-kategori="pemandangan">
<a class="galeri-tautan" href="#senja-di-deraung"><span class="galeri-media galeri-media--pemandangan">
<img class="galeri-foto" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 840 360'%3E%3Cdefs%3E%3ClinearGradient id='a' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%23f0a35e'/%3E%3Cstop offset='1' stop-color='%23a8432c'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='840' height='360' fill='url(%23a)'/%3E%3Ccircle cx='610' cy='150' r='42' fill='%23ffe0a3'/%3E%3Cpolygon points='0,360 250,170 520,360' fill='%23602a38'/%3E%3Cpolygon points='330,360 590,215 840,360' fill='%23401c2b'/%3E%3C/svg%3E" alt="Siluet perbukitan bertingkat dengan matahari terbenam di Deraung" width="840" height="360" loading="lazy">
</span></a>
<figcaption class="galeri-keterangan"><strong class="galeri-judul">Senja di Deraung</strong> <span class="galeri-meta">Pemandangan · 35 mm · f/8 · 1/125 dtk</span></figcaption>
</figure>
<figure class="galeri-item" data-kategori="pemandangan">
<a class="galeri-tautan" href="#kabut-telaga-raya"><span class="galeri-media galeri-media--pemandangan">
<img class="galeri-foto" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 800 450'%3E%3Cdefs%3E%3ClinearGradient id='b' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%239fc4bd'/%3E%3Cstop offset='1' stop-color='%2357837c'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='800' height='450' fill='url(%23b)'/%3E%3Ccircle cx='190' cy='110' r='34' fill='%23e8f2ee'/%3E%3Cpolygon points='0,340 240,190 470,340' fill='%23314b4a'/%3E%3Cpolygon points='260,340 520,215 800,340' fill='%23243738'/%3E%3Crect y='340' width='800' height='110' fill='%232c4344'/%3E%3C/svg%3E" alt="Kabut pagi menyelimuti telaga dan dua bukit berlapis" width="800" height="450" loading="lazy">
</span></a>
<figcaption class="galeri-keterangan"><strong class="galeri-judul">Kabut Telaga Raya</strong> <span class="galeri-meta">Pemandangan · 24 mm · f/11 · 1/60 dtk</span></figcaption>
</figure>
<figure class="galeri-item" data-kategori="urban">
<a class="galeri-tautan" href="#gang-lampu-neon"><span class="galeri-media galeri-media--urban">
<img class="galeri-foto" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 800 450'%3E%3Crect width='800' height='450' fill='%23141821'/%3E%3Crect x='40' y='60' width='180' height='390' fill='%231d2430'/%3E%3Crect x='250' y='20' width='220' height='430' fill='%23232c3b'/%3E%3Crect x='500' y='90' width='260' height='360' fill='%231d2430'/%3E%3Crect x='250' y='20' width='220' height='16' fill='%23e84d8a'/%3E%3Crect x='40' y='60' width='180' height='10' fill='%2340c4b5'/%3E%3Crect x='500' y='90' width='260' height='10' fill='%23e8a13d'/%3E%3Crect x='285' y='80' width='24' height='36' fill='%2340c4b5'/%3E%3Crect x='330' y='80' width='24' height='36' fill='%23e84d8a'/%3E%3Crect x='375' y='80' width='24' height='36' fill='%2340c4b5'/%3E%3Crect x='540' y='140' width='20' height='28' fill='%23e8a13d'/%3E%3Crect x='580' y='140' width='20' height='28' fill='%2340c4b5'/%3E%3Crect x='620' y='140' width='20' height='28' fill='%23e84d8a'/%3E%3C/svg%3E" alt="Dinding gedung bertumpuk dengan jalur lampu neon merah, hijau, dan kuning" width="800" height="450" loading="lazy">
</span></a>
<figcaption class="galeri-keterangan"><strong class="galeri-judul">Gang Lampu Neon</strong> <span class="galeri-meta">Urban · 50 mm · f/2 · 1/30 dtk</span></figcaption>
</figure>
<figure class="galeri-item galeri-item--tinggi" data-kategori="makro">
<a class="galeri-tautan" href="#tetes-embun-pakis"><span class="galeri-media galeri-media--makro">
<img class="galeri-foto" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 600 900'%3E%3Cdefs%3E%3ClinearGradient id='c' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%232f6b3a'/%3E%3Cstop offset='1' stop-color='%2312301c'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='600' height='900' fill='url(%23c)'/%3E%3Cellipse cx='300' cy='480' rx='210' ry='380' fill='%233d7d47'/%3E%3Cpath d='M300 120 L300 860' stroke='%23245530' stroke-width='10'/%3E%3Ccircle cx='300' cy='430' r='58' fill='%23cfe8d5'/%3E%3Ccircle cx='282' cy='410' r='14' fill='%23ffffff'/%3E%3C/svg%3E" alt="Tetes embun besar di permukaan daun pakis berwarna hijau pekat" width="600" height="900" loading="lazy">
</span></a>
<figcaption class="galeri-keterangan"><strong class="galeri-judul">Tetes Embun Pakis</strong> <span class="galeri-meta">Makro · 90 mm · f/4 · 1/200 dtk</span></figcaption>
</figure>
<figure class="galeri-item galeri-item--panorama" data-kategori="urban">
<a class="galeri-tautan" href="#jembatan-kota-batavia"><span class="galeri-media galeri-media--urban">
<img class="galeri-foto" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1050 300'%3E%3Cdefs%3E%3ClinearGradient id='d' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%232c3d55'/%3E%3Cstop offset='1' stop-color='%231a2433'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='1050' height='300' fill='url(%23d)'/%3E%3Crect x='140' y='70' width='16' height='180' fill='%230e131c'/%3E%3Crect x='510' y='40' width='16' height='210' fill='%230e131c'/%3E%3Crect x='880' y='70' width='16' height='180' fill='%230e131c'/%3E%3Cpolyline points='0,140 148,120 300,150 518,90 740,150 888,120 1050,140' fill='none' stroke='%23566b8a' stroke-width='4'/%3E%3Crect y='250' width='1050' height='50' fill='%230e131c'/%3E%3Ccircle cx='100' cy='240' r='8' fill='%23ffd479'/%3E%3Ccircle cx='350' cy='240' r='8' fill='%23ffd479'/%3E%3Ccircle cx='620' cy='240' r='8' fill='%23ffd479'/%3E%3Ccircle cx='900' cy='240' r='8' fill='%23ffd479'/%3E%3C/svg%3E" alt="Bentang jembatan kabel dengan tiga menara dan lampu kuning saat petang" width="1050" height="300" loading="lazy">
</span></a>
<figcaption class="galeri-keterangan"><strong class="galeri-judul">Jembatan Kota Batavia</strong> <span class="galeri-meta">Urban · 70 mm · f/9 · 2 dtk · tripod</span></figcaption>
</figure>
<figure class="galeri-item" data-kategori="pemandangan">
<a class="galeri-tautan" href="#bukit-lavanya"><span class="galeri-media galeri-media--pemandangan">
<img class="galeri-foto" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 800 450'%3E%3Cdefs%3E%3ClinearGradient id='e' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%23cfa9d9'/%3E%3Cstop offset='1' stop-color='%238a5f9e'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='800' height='450' fill='url(%23e)'/%3E%3Ccircle cx='640' cy='95' r='30' fill='%23f2e6f5'/%3E%3Cpolygon points='0,450 190,260 420,450' fill='%235d3a75'/%3E%3Cpolygon points='250,450 480,290 720,450' fill='%233f2657'/%3E%3Cpolygon points='0,450 640,340 800,450 0,450' fill='%232a183c'/%3E%3C/svg%3E" alt="Rangkaian bukit ungu berlapis di bawah langit senja lavender" width="800" height="450" loading="lazy">
</span></a>
<figcaption class="galeri-keterangan"><strong class="galeri-judul">Bukit Lavanya</strong> <span class="galeri-meta">Pemandangan · 85 mm · f/5.6 · 1/250 dtk</span></figcaption>
</figure>
<figure class="galeri-item galeri-item--besar" data-kategori="urban">
<a class="galeri-tautan" href="#fasad-kaca-merdeka"><span class="galeri-media galeri-media--urban">
<img class="galeri-foto" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 700 400'%3E%3Crect width='700' height='400' fill='%2322303f'/%3E%3Crect x='30' y='30' width='150' height='100' fill='%233a556e'/%3E%3Crect x='210' y='30' width='150' height='100' fill='%23517893'/%3E%3Crect x='390' y='30' width='150' height='100' fill='%233a556e'/%3E%3Crect x='570' y='30' width='100' height='100' fill='%23517893'/%3E%3Crect x='30' y='160' width='150' height='100' fill='%23517893'/%3E%3Crect x='210' y='160' width='150' height='100' fill='%233a556e'/%3E%3Crect x='390' y='160' width='150' height='100' fill='%23517893'/%3E%3Crect x='570' y='160' width='100' height='100' fill='%233a556e'/%3E%3Crect x='30' y='290' width='640' height='80' fill='%231a2531'/%3E%3C/svg%3E" alt="Fasad kaca gedung perkantoran berpola jendela biru keabu-abuan" width="700" height="400" loading="lazy">
</span></a>
<figcaption class="galeri-keterangan"><strong class="galeri-judul">Fasad Kaca Merdeka</strong> <span class="galeri-meta">Urban · 24 mm · f/8 · 1/160 dtk</span></figcaption>
</figure>
<figure class="galeri-item" data-kategori="makro">
<a class="galeri-tautan" href="#mata-kupu-kupu"><span class="galeri-media galeri-media--makro">
<img class="galeri-foto" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 800 450'%3E%3Cdefs%3E%3ClinearGradient id='f' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%23274023'/%3E%3Cstop offset='1' stop-color='%230f1d10'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='800' height='450' fill='url(%23f)'/%3E%3Cellipse cx='330' cy='225' rx='150' ry='95' fill='%23d98e32' transform='rotate(-18 330 225)'/%3E%3Cellipse cx='470' cy='225' rx='150' ry='95' fill='%23d98e32' transform='rotate(18 470 225)'/%3E%3Cellipse cx='400' cy='225' rx='18' ry='95' fill='%231d1509'/%3E%3Ccircle cx='290' cy='200' r='26' fill='%23f2e2b6'/%3E%3Ccircle cx='510' cy='200' r='26' fill='%23f2e2b6'/%3E%3C/svg%3E" alt="Sayap kupu-kupu jingga dengan bercak krem terlihat dari dekat" width="800" height="450" loading="lazy">
</span></a>
<figcaption class="galeri-keterangan"><strong class="galeri-judul">Mata Kupu-Kupu</strong> <span class="galeri-meta">Makro · 100 mm · f/2.8 · 1/320 dtk</span></figcaption>
</figure>
<figure class="galeri-item galeri-item--tinggi" data-kategori="makro">
<a class="galeri-tautan" href="#heliconia-merah"><span class="galeri-media galeri-media--makro">
<img class="galeri-foto" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 600 900'%3E%3Crect width='600' height='900' fill='%23101c14'/%3E%3Cpath d='M320 900 L300 260' stroke='%23245530' stroke-width='14' fill='none'/%3E%3Cpolygon points='300,260 180,320 260,380' fill='%23c23b3b'/%3E%3Cpolygon points='300,340 420,390 330,450' fill='%23c23b3b'/%3E%3Cpolygon points='300,420 170,470 270,530' fill='%23e86a5a'/%3E%3Cpolygon points='300,500 430,550 330,610' fill='%23c23b3b'/%3E%3Cpolygon points='300,580 180,630 280,690' fill='%23e86a5a'/%3E%3C/svg%3E" alt="Juntungan bunga heliconia merah bertingkat di latar hijau gelap" width="600" height="900" loading="lazy">
</span></a>
<figcaption class="galeri-keterangan"><strong class="galeri-judul">Heliconia Merah</strong> <span class="galeri-meta">Makro · 60 mm · f/5.6 · 1/125 dtk</span></figcaption>
</figure>
<figure class="galeri-item galeri-item--lebar" data-kategori="pemandangan">
<a class="galeri-tautan" href="#danau-warna-fajar"><span class="galeri-media galeri-media--pemandangan">
<img class="galeri-foto" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 840 360'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%23f2c9a0'/%3E%3Cstop offset='1' stop-color='%23b86a4e'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='840' height='360' fill='url(%23g)'/%3E%3Ccircle cx='420' cy='160' r='38' fill='%23ffe9c9'/%3E%3Cpolygon points='0,250 220,170 430,250' fill='%234f3450'/%3E%3Cpolygon points='360,250 600,160 840,250' fill='%233a2440'/%3E%3Crect y='250' width='840' height='110' fill='%23302a44'/%3E%3Cpolygon points='380,250 420,290 460,250' fill='%23ffe9c9'/%3E%3C/svg%3E" alt="Danau tenang saat fajar dengan pantulan matahari di antara dua bukit" width="840" height="360" loading="lazy">
</span></a>
<figcaption class="galeri-keterangan"><strong class="galeri-judul">Danau Warna Fajar</strong> <span class="galeri-meta">Pemandangan · 28 mm · f/13 · 1/45 dtk</span></figcaption>
</figure>
</div>
</main>
<footer class="kaki-galeri">
<div class="pembungkus kaki-isi">
<p>Galeri Sudut Cahaya — koleksi latihan CSS Grid dan object-fit</p>
<p>Semua foto digambar sebagai SVG data URI</p>
</div>
</footer>
<script src="js/main.js"></script>
</body>
</html>// ============================================================
// Galeri Sudut Cahaya — penyaring kategori galeri
// id yang dirujuk: bilah-filter dan hitung-galeri (beranda.html)
// ============================================================
"use strict";
const grupFilter = document.getElementById("bilah-filter");
const tombolFilter = grupFilter ? Array.from(grupFilter.querySelectorAll(".tombol-filter")) : [];
const itemGaleri = Array.from(document.querySelectorAll(".galeri-item"));
const hitungGaleri = document.getElementById("hitung-galeri");
// Menuliskan jumlah foto yang lolos saringan ke elemen penghitung.
function perbaruiHitungan(jumlah) {
if (hitungGaleri) {
hitungGaleri.textContent = String(jumlah);
}
}
// Menandai satu tombol sebagai aktif lewat atribut aria-pressed.
function tandaiTombolAktif(tombolAktif) {
tombolFilter.forEach(function (tombol) {
const aktif = tombol === tombolAktif;
tombol.setAttribute("aria-pressed", aktif ? "true" : "false");
});
}
// Menyembunyikan foto yang kategorinya tidak cocok memakai class tersembunyi.
function saringGaleri(kategori) {
let jumlahTampil = 0;
itemGaleri.forEach(function (item) {
const cocok = kategori === "semua" || item.dataset.kategori === kategori;
item.classList.toggle("tersembunyi", !cocok);
if (cocok) {
jumlahTampil += 1;
}
});
perbaruiHitungan(jumlahTampil);
}
// Satu pendengar klik untuk setiap tombol filter.
tombolFilter.forEach(function (tombol) {
tombol.addEventListener("click", function () {
tandaiTombolAktif(tombol);
saringGaleri(tombol.dataset.kategori);
});
});
// Kondisi awal: tombol Semua aktif, seluruh koleksi tampil, hitungan 10.
if (tombolFilter.length > 0) {
saringGaleri("semua");
}Ringkasan #
Proyek Galeri Fotografi selesai: grid berirama dari Bagian 1, pemangkasan object-fit dari Bagian 2, aspect-ratio dari Bagian 3, overlay keterangan dari Bagian 4, dan kini gerak, dua breakpoint, serta penyaring kategori dari bagian ini. Ketiga berkasnya ada di src/ dan tersalin utuh di atas. Latihan lanjutan yang murah: ubah --unit-lintasan menjadi 80 px dan amati seluruh ritme bergeser, tambah kategori kelima lengkap dengan tombolnya, atau ganti palet gelap menjadi terang hanya dengan menyunting blok token.