Bagian penutup ini mengubah dasbor desktop menjadi dasbor yang nyaman di ponsel, sekaligus memasang penggeraknya: main.js yang selama ini disiapkan. Peta grid dari Bagian 1 kita susun ulang dalam satu media query, sidebar berubah menjadi panel geser bertirai, tombol Menu mendapatkan pekerjaannya, dan seluruh gerak tunduk pada preferensi pengguna. Snapshot pada bagian ini adalah kondisi akhir proyek — identik dengan berkas di folder src/.

Media Query: dari Sidebar ke Bilah Atas #

Ambang batasnya max-width: 860px, lebar di mana kolom samping 248 px mulai terasa memakan ruang. Di dalamnya, peta area disusun ulang dan sidebar berubah watak:

css
@media (max-width: 860px) {
  .cangkang-app {
    grid-template-areas: "kepala" "utama";
    grid-template-columns: 1fr;
    grid-template-rows: auto 1fr;
  }
  .sidebar {
    position: fixed; top: 0; bottom: 0; left: 0;
    width: min(280px, 80vw);
    transform: translateX(-100%);
  }
  .sidebar.is-terbuka { transform: translateX(0); box-shadow: var(--bayangan-2); }
}

Inilah hadiah pola grid-template-areas: mengubah tata letak hanya berarti menulis ulang petanya. Karena samping tidak lagi ada di peta, .sidebar keluar dari arus dokumen dengan position: fixed dan bersembunyi di luar tepi kiri lewat translateX(-100%). Class is-terbuka — yang akan dipasang JavaScript — menggesernya kembali. Tirai .tirai-samping menutupi layar dengan latar gelap semi transparan saat panel terbuka; properti visibility dan pointer-events memastikan ia tidak bisa disentuh ketika tertutup, sekaligus menjauhkannya dari urutan fokus keyboard. Tombol Menu tampil di bilah kepala lewat display: inline-flex, menggantikan tempat chip periode yang disembunyikan.

JavaScript Penggerak Sidebar #

main.js tetap pendek dan tanpa pustaka. Fungsi intinya satu, dengan status ARIA ikut diperbarui:

js
function aturSidebar(terbuka) {
  sidebar.classList.toggle("is-terbuka", terbuka);
  tirai.classList.toggle("is-tampak", terbuka);
  tombolMenu.setAttribute("aria-expanded", terbuka ? "true" : "false");
}

Tiga sumber kejadian memanggilnya: klik tombol Menu membalik keadaan saat ini, klik tirai menutup panel, dan memilih tautan menu juga menutupnya agar pengguna langsung melihat konten. Fungsi pilihMenu memindahkan class is-aktif beserta atribut aria-current ke tautan yang diklik — memastikan tanda halaman aktif yang kita bangun di Bagian 2 tetap benar setiap saat. Skrip tidak menyimpan apa pun di sisi browser, sehingga aman dijalankan di lingkungan pratinjau apa pun.

Cincin Fokus dan Preferensi Gerak #

Penutup ini soal kenyamanan dua kelompok pengguna. Pengguna keyboard mendapat cincin fokus yang terlihat di atas latar gelap:

css
:focus-visible { outline: 2px solid var(--warna-fokus); outline-offset: 2px; }
.menu-tautan:focus-visible { outline-color: #5eead4; }

Sementara pengguna yang mengaktifkan prefers-reduced-motion di sistemnya tidak disugu animasi geser:

css
@media (prefers-reduced-motion: reduce) {
  .menu-tautan, .sidebar, .tirai-samping { transition: none; }
}

Panel tetap terbuka dan tertutup — hanya tanpa perpindahan halus. Terakhir, hierarki visual dasbor dipoles dengan memangkas padding area utama dan sel tabel di layar sempit, serta menyembunyikan kolom Kategori yang paling opsional. Hierarkinya kini tegas: angka Rupiah paling besar dan tebal, label dan metadata tampil kecil redup, lencana status memberi aksen warna — mata pengguna berhenti dulu di angka sebelum membaca sisanya.

css
/* ============================================================
   Dashboard Keuangan — stylesheet utama
   Urutan: token → base → cangkang aplikasi → sidebar →
   kepala aplikasi → kartu ringkasan → tabel transaksi →
   lencana status → responsif → fokus dan gerak
   ============================================================ */

/* ---------- 1. Design token ---------- */
:root {
  /* Warna semantik dasbor */
  --warna-latar: #f1f5f9;       --warna-permukaan: #ffffff;
  --warna-teks: #0f172a;        --warna-teks-lembut: #64748b;
  --warna-garis: #e2e8f0;       --warna-utama: #0f766e;
  --warna-utama-pekat: #115e59; --warna-utama-lembut: #ccfbf1;
  --warna-utama-tinta: #134e4a; --warna-fokus: #0d9488;
  --warna-samping: #0f172a;     --warna-samping-teks: #cbd5e1;
  --warna-samping-hover: rgba(255, 255, 255, 0.08);
  --warna-masuk: #15803d;       --warna-masuk-latar: #dcfce7;
  --warna-keluar: #b91c1c;      --warna-keluar-latar: #fee2e2;

  /* 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;

  /* Radius sudut dan bayangan */
  --radius-kecil: 8px; --radius: 12px; --radius-penuh: 999px;
  --bayangan-1: 0 1px 2px rgba(15, 23, 42, 0.06);
  --bayangan-2: 0 12px 28px rgba(15, 23, 42, 0.22);

  /* Tipografi, gerak, dan kerangka */
  --font-dasar: system-ui, "Segoe UI", sans-serif;
  --durasi: 180ms; --pelunakan: ease-out;
  --lebar-samping: 248px;
}

/* ---------- 2. Base ---------- */
*, *::before, *::after { box-sizing: border-box; }

body { margin: 0; font-family: var(--font-dasar); line-height: 1.6; background-color: var(--warna-latar); color: var(--warna-teks); }

h1, h2, p, ul { margin: 0; }

ul { padding: 0; list-style: none; }

button { font: inherit; }

.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }

/* ---------- 3. Cangkang aplikasi ---------- */
.cangkang-app {
  display: grid;
  grid-template-areas: "samping kepala" "samping utama";
  grid-template-columns: var(--lebar-samping) 1fr;
  grid-template-rows: auto 1fr;
  height: 100dvh;
}

.sidebar { grid-area: samping; }

.kepala-app { grid-area: kepala; }

.isi-utama { grid-area: utama; overflow-y: auto; display: grid; gap: var(--jarak-5); align-content: start; padding: var(--jarak-5); }

.isi-kaki { font-size: 12.5px; color: var(--warna-teks-lembut); text-align: center; }

/* ---------- 4. Sidebar dan navigasi ---------- */
.sidebar {
  display: flex; flex-direction: column; gap: var(--jarak-5); overflow-y: auto;
  padding: var(--jarak-5) var(--jarak-4);
  background-color: var(--warna-samping); color: var(--warna-samping-teks);
}

.brand { display: flex; align-items: center; gap: var(--jarak-3); }

.brand-tanda { flex: none; width: 38px; height: 38px; border-radius: 10px; background-image: linear-gradient(135deg, #2dd4bf, #0f766e); }

.brand-nama { color: #ffffff; font-size: 17px; font-weight: 800; line-height: 1.3; }

.brand-sub { display: block; font-size: 11.5px; font-weight: 500; color: var(--warna-samping-teks); }

.menu-judul { padding-inline: var(--jarak-3); font-size: 11.5px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; color: #64748b; }

.menu-app { display: grid; gap: 4px; margin-top: var(--jarak-2); }

.menu-tautan {
  display: flex; align-items: center; gap: var(--jarak-3); padding: 10px var(--jarak-3);
  border-radius: var(--radius-kecil); text-decoration: none;
  color: var(--warna-samping-teks); font-size: 14.5px; font-weight: 600;
  transition: background-color var(--durasi) var(--pelunakan), color var(--durasi) var(--pelunakan);
}

.menu-tautan:hover { background-color: var(--warna-samping-hover); color: #ffffff; }

.menu-tautan.is-aktif { background-color: var(--warna-utama); color: #ffffff; }

.sidebar-kaki { margin-top: auto; padding: var(--jarak-3); border-top: 1px solid rgba(255, 255, 255, 0.12); font-size: 12.5px; }

/* ---------- 5. Kepala aplikasi ---------- */
.kepala-app {
  display: flex; align-items: center; gap: var(--jarak-4); padding: var(--jarak-4) var(--jarak-5);
  background-color: var(--warna-permukaan); border-bottom: 1px solid var(--warna-garis);
}

.kepala-sapaan { font-size: 12.5px; color: var(--warna-teks-lembut); }

.kepala-judul { font-size: 20px; font-weight: 800; letter-spacing: -0.01em; line-height: 1.3; }

.kepala-periode { margin-left: auto; padding: 6px 14px; border-radius: var(--radius-penuh); background-color: var(--warna-utama-lembut); color: var(--warna-utama-tinta); font-size: 13px; font-weight: 700; }

.tombol-samping { display: none; }

/* ---------- 6. Kartu ringkasan ---------- */
.dasbor-ringkasan {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: var(--jarak-4);
}

.kartu-ringkasan {
  display: grid; gap: 2px; padding: var(--jarak-4) var(--jarak-5);
  background-color: var(--warna-permukaan); border-radius: var(--radius);
  border: 1px solid var(--warna-garis); border-left: 4px solid var(--warna-utama);
  box-shadow: var(--bayangan-1);
}

.kartu-ringkasan--masuk { border-left-color: var(--warna-masuk); }

.kartu-ringkasan--keluar { border-left-color: var(--warna-keluar); }

.kartu-label { font-size: 13px; font-weight: 600; color: var(--warna-teks-lembut); }

.kartu-angka { font-size: clamp(1.5rem, 2.5vw, 1.85rem); font-weight: 800; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }

.kartu-tren { font-size: 13px; font-weight: 600; color: var(--warna-teks-lembut); }

.tren--naik::before { content: "▲ "; font-size: 0.8em; }

.tren--turun::before { content: "▼ "; font-size: 0.8em; }

.tren--positif { color: var(--warna-masuk); }

.tren--negatif { color: var(--warna-keluar); }

/* ---------- 7. Tabel transaksi ---------- */
.seksi-transaksi {
  background-color: var(--warna-permukaan); border-radius: var(--radius); overflow: hidden;
  border: 1px solid var(--warna-garis); box-shadow: var(--bayangan-1);
}

.seksi-kepala {
  display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap;
  gap: var(--jarak-3); padding: var(--jarak-4) var(--jarak-5);
  border-bottom: 1px solid var(--warna-garis);
}

.seksi-judul { font-size: 17px; font-weight: 800; }

.tautan-lain { font-size: 13.5px; font-weight: 600; color: var(--warna-utama); text-decoration: none; }

.tautan-lain:hover { text-decoration: underline; }

.tabel-gulir { max-height: 380px; overflow-y: auto; }

.tabel-transaksi { width: 100%; border-collapse: collapse; font-size: 14px; }

.tabel-transaksi th, .tabel-transaksi td {
  padding: 12px var(--jarak-5); text-align: left;
  border-bottom: 1px solid var(--warna-garis);
}

.tabel-transaksi thead th {
  position: sticky; top: 0; z-index: 1;
  background-color: var(--warna-utama-lembut); color: var(--warna-utama-tinta);
  font-size: 12.5px; letter-spacing: 0.05em; text-transform: uppercase;
}

.tabel-transaksi tbody tr:nth-child(even) { background-color: #f8fafc; }

.tabel-transaksi tbody tr:last-child td { border-bottom: none; }

.kolom-tanggal { white-space: nowrap; color: var(--warna-teks-lembut); }

.kolom-kategori { color: var(--warna-teks-lembut); }

.kolom-jumlah { text-align: right; font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }

.sel-masuk { color: var(--warna-masuk); }

.sel-keluar { color: var(--warna-keluar); }

/* ---------- 8. Lencana status ---------- */
.lencana-status { display: inline-block; padding: 3px 12px; border-radius: var(--radius-penuh); font-size: 12px; font-weight: 700; }

.lencana-status--masuk { background-color: var(--warna-masuk-latar); color: var(--warna-masuk); }

.lencana-status--keluar { background-color: var(--warna-keluar-latar); color: var(--warna-keluar); }

/* ---------- 9. Responsif ---------- */
@media (max-width: 860px) {
  .cangkang-app {
    grid-template-areas: "kepala" "utama";
    grid-template-columns: 1fr;
    grid-template-rows: auto 1fr;
  }

  .kepala-app { padding: var(--jarak-3) var(--jarak-4); }

  .tombol-samping {
    display: inline-flex; align-items: center; gap: var(--jarak-2); padding: 8px 14px;
    border: 1px solid var(--warna-garis); border-radius: var(--radius-kecil);
    background-color: var(--warna-permukaan); font-size: 14px; font-weight: 700; cursor: pointer;
  }

  .kepala-periode { display: none; }

  .sidebar {
    position: fixed; top: 0; bottom: 0; left: 0; z-index: 30; width: min(280px, 80vw);
    transform: translateX(-100%);
    transition: transform var(--durasi) var(--pelunakan), box-shadow var(--durasi) var(--pelunakan);
  }

  .sidebar.is-terbuka { transform: translateX(0); box-shadow: var(--bayangan-2); }

  .tirai-samping {
    position: fixed; inset: 0; z-index: 20; border: none; padding: 0; cursor: pointer;
    background-color: rgba(15, 23, 42, 0.45);
    opacity: 0; visibility: hidden; pointer-events: none;
    transition: opacity var(--durasi) var(--pelunakan), visibility var(--durasi) var(--pelunakan);
  }

  .tirai-samping.is-tampak { opacity: 1; visibility: visible; pointer-events: auto; }

  .isi-utama { padding: var(--jarak-4); }

  .tabel-transaksi th, .tabel-transaksi td { padding: 10px var(--jarak-3); }

  .kolom-kategori { display: none; }
}

/* ---------- 10. Fokus dan gerak ---------- */
:focus-visible { outline: 2px solid var(--warna-fokus); outline-offset: 2px; }

.menu-tautan:focus-visible { outline-color: #5eead4; }

@media (prefers-reduced-motion: reduce) {
  .menu-tautan, .sidebar, .tirai-samping { transition: none; }
}
html
<!DOCTYPE html>
<html lang="id">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Dashboard Keuangan — DompetKu</title>
  <link rel="stylesheet" href="css/styles.css">
  <script src="js/main.js" defer></script>
</head>
<body>
  <div class="cangkang-app">
    <aside class="sidebar" id="sidebar-nav" aria-label="Panel navigasi dasbor">
      <div class="brand">
        <span class="brand-tanda" aria-hidden="true"></span>
        <span class="brand-nama">DompetKu<span class="brand-sub">Catatan keuangan pribadi</span></span>
      </div>
      <nav aria-label="Menu utama">
        <p class="menu-judul" id="judul-menu">Menu</p>
        <ul class="menu-app" aria-labelledby="judul-menu">
          <li><a href="#ringkasan" class="menu-tautan is-aktif" aria-current="page">Ringkasan</a></li>
          <li><a href="#transaksi" class="menu-tautan">Transaksi</a></li>
          <li><a href="#" class="menu-tautan">Anggaran</a></li>
          <li><a href="#" class="menu-tautan">Tabungan</a></li>
          <li><a href="#" class="menu-tautan">Laporan</a></li>
        </ul>
        <p class="menu-judul" id="judul-menu-lain">Lainnya</p>
        <ul class="menu-app" aria-labelledby="judul-menu-lain">
          <li><a href="#" class="menu-tautan">Pengaturan</a></li>
          <li><a href="#" class="menu-tautan">Bantuan</a></li>
        </ul>
      </nav>
      <p class="sidebar-kaki">Versi latihan 1.0</p>
    </aside>

    <button type="button" class="tirai-samping" id="tirai-samping" aria-label="Tutup panel navigasi"></button>

    <header class="kepala-app">
      <button type="button" class="tombol-samping" id="tombol-samping" aria-expanded="false" aria-controls="sidebar-nav">☰ Menu</button>
      <div class="kepala-teks">
        <p class="kepala-sapaan">Selamat datang kembali, Rani</p>
        <h1 class="kepala-judul">Ringkasan Keuangan</h1>
      </div>
      <p class="kepala-periode">November 2026</p>
    </header>

    <main class="isi-utama" id="ringkasan">
      <section class="dasbor-ringkasan" aria-label="Kartu ringkasan keuangan">
        <article class="kartu-ringkasan">
          <p class="kartu-label">Saldo saat ini</p>
          <p class="kartu-angka">Rp9.485.000</p>
          <p class="kartu-tren tren tren--turun tren--negatif">0,8% dari bulan lalu</p>
        </article>
        <article class="kartu-ringkasan kartu-ringkasan--masuk">
          <p class="kartu-label">Pemasukan bulan ini</p>
          <p class="kartu-angka">Rp11.670.000</p>
          <p class="kartu-tren tren tren--naik tren--positif">12,4% dari bulan lalu</p>
        </article>
        <article class="kartu-ringkasan kartu-ringkasan--keluar">
          <p class="kartu-label">Pengeluaran bulan ini</p>
          <p class="kartu-angka">Rp2.185.000</p>
          <p class="kartu-tren tren tren--naik tren--negatif">5,2% dari bulan lalu</p>
        </article>
      </section>

      <section class="seksi-transaksi" id="transaksi" aria-labelledby="judul-transaksi">
        <div class="seksi-kepala">
          <h2 class="seksi-judul" id="judul-transaksi">Transaksi Terakhir</h2>
          <a class="tautan-lain" href="#">Lihat semua transaksi</a>
        </div>
        <div class="tabel-gulir">
          <table class="tabel-transaksi">
            <caption class="sr-only">Delapan transaksi terakhir pada November 2026</caption>
            <thead>
              <tr>
                <th scope="col">Tanggal</th>
                <th scope="col">Keterangan</th>
                <th scope="col" class="kolom-kategori">Kategori</th>
                <th scope="col" class="kolom-jumlah">Jumlah</th>
                <th scope="col">Status</th>
              </tr>
            </thead>
            <tbody>
              <tr><td class="kolom-tanggal">18 Nov</td><td>Penjualan barang bekas</td><td class="kolom-kategori">Lainnya</td><td class="kolom-jumlah sel-masuk">+Rp420.000</td><td><span class="lencana-status lencana-status--masuk">Masuk</span></td></tr>
              <tr><td class="kolom-tanggal">15 Nov</td><td>Ongkir belanja online</td><td class="kolom-kategori">Kebutuhan</td><td class="kolom-jumlah sel-keluar">−Rp35.000</td><td><span class="lencana-status lencana-status--keluar">Keluar</span></td></tr>
              <tr><td class="kolom-tanggal">14 Nov</td><td>Makan siang kantor</td><td class="kolom-kategori">Makanan</td><td class="kolom-jumlah sel-keluar">−Rp275.000</td><td><span class="lencana-status lencana-status--keluar">Keluar</span></td></tr>
              <tr><td class="kolom-tanggal">11 Nov</td><td>Tagihan listrik dan air</td><td class="kolom-kategori">Tagihan</td><td class="kolom-jumlah sel-keluar">−Rp486.000</td><td><span class="lencana-status lencana-status--keluar">Keluar</span></td></tr>
              <tr><td class="kolom-tanggal">08 Nov</td><td>Proyek desain lepas</td><td class="kolom-kategori">Penghasilan</td><td class="kolom-jumlah sel-masuk">+Rp2.750.000</td><td><span class="lencana-status lencana-status--masuk">Masuk</span></td></tr>
              <tr><td class="kolom-tanggal">05 Nov</td><td>Langganan streaming</td><td class="kolom-kategori">Hiburan</td><td class="kolom-jumlah sel-keluar">−Rp149.000</td><td><span class="lencana-status lencana-status--keluar">Keluar</span></td></tr>
              <tr><td class="kolom-tanggal">03 Nov</td><td>Belanja bulanan supermarket</td><td class="kolom-kategori">Kebutuhan</td><td class="kolom-jumlah sel-keluar">−Rp1.240.000</td><td><span class="lencana-status lencana-status--keluar">Keluar</span></td></tr>
              <tr><td class="kolom-tanggal">02 Nov</td><td>Gaji bulanan</td><td class="kolom-kategori">Penghasilan</td><td class="kolom-jumlah sel-masuk">+Rp8.500.000</td><td><span class="lencana-status lencana-status--masuk">Masuk</span></td></tr>
            </tbody>
          </table>
        </div>
      </section>

      <p class="isi-kaki">Angka pada dasbor ini adalah contoh latihan; total pemasukan dikurangi pengeluaran cocok dengan saldo yang ditampilkan.</p>
    </main>
  </div>
</body>
</html>
js
/* ============================================================
   main.js — interaksi Dashboard Keuangan.
   1) Status aktif menu: klik tautan menu memindahkan class
      is-aktif beserta atribut aria-current ke tautan itu.
   2) Pada layar kecil, tombol menu membuka/menutup sidebar
      dan menyelaraskan aria-expanded; tirai gelap menutupnya.
   Tidak ada penyimpanan di sisi browser.
   ============================================================ */
(function () {
  "use strict";

  var sidebar = document.getElementById("sidebar-nav");
  var tombolMenu = document.getElementById("tombol-samping");
  var tirai = document.getElementById("tirai-samping");
  var daftarTautan = document.querySelectorAll(".menu-tautan");

  /* Buka atau tutup sidebar lalu selaraskan status ARIA. */
  function aturSidebar(terbuka) {
    sidebar.classList.toggle("is-terbuka", terbuka);
    tirai.classList.toggle("is-tampak", terbuka);
    tombolMenu.setAttribute("aria-expanded", terbuka ? "true" : "false");
  }

  /* Pindahkan tanda menu aktif ke tautan yang dipilih. */
  function pilihMenu(tautanDipilih) {
    daftarTautan.forEach(function (tautan) {
      var aktif = tautan === tautanDipilih;
      tautan.classList.toggle("is-aktif", aktif);
      if (aktif) {
        tautan.setAttribute("aria-current", "page");
      } else {
        tautan.removeAttribute("aria-current");
      }
    });
    aturSidebar(false);
  }

  tombolMenu.addEventListener("click", function () {
    aturSidebar(!sidebar.classList.contains("is-terbuka"));
  });

  tirai.addEventListener("click", function () {
    aturSidebar(false);
  });

  daftarTautan.forEach(function (tautan) {
    tautan.addEventListener("click", function (event) {
      event.preventDefault(); /* tautan berupa "#" dalam latihan */
      pilihMenu(tautan);
    });
  });
})();

Ringkasan #

Proyek DompetKu tuntas: cangkang grid 100dvh dari Bagian 1 kini ikut meruntuhkan diri menjadi bilah atas, sidebar geser digerakkan JavaScript yang menjaga aria-expanded dan aria-current, serta seluruh gerak tunduk pada prefers-reduced-motion. Snapshot bagian ini identik dengan folder src/, sehingga bisa dipakai sebagai acuan akhir. Sebagai latihan lanjutan, coba tambahkan kartu ringkasan keempat atau naikkan ambang media query dan amati bagian mana yang perlu menyesuaikan.