Bagian penutup ini mengangkat halaman dari “rapi di desktop” menjadi benar-benar responsif dan interaktif. Dua hal sekaligus dikerjakan: media query yang menata ulang kartu dan tabel di layar sempit, lalu js/main.js yang menggerakkan saklar tagihan. Snapshot pada bagian ini — styles.css, beranda.html, dan main.js — kini identik dengan kondisi akhir di folder src/, sehingga menyelesaikan bagian ini berarti menyelesaikan seluruh proyek.

Grid auto-fit dari Bagian 2 sudah menangani perubahan jumlah kolom, tetapi beberapa detail tetap perlu disetel lewat media query:

css
@media (max-width: 720px) {
  .galeri-harga { grid-template-columns: 1fr; }

  .kartu-harga--unggulan { order: -1; }
}

Menjadi satu kolom membuat urutan kartu mengikuti urutan HTML: Pemula, Bisnis, Enterprise. Urutan itu kurang pas di ponsel, sebab paket yang paling sering dipilih justru berada di tengah. Deklarasi order: -1 pada kartu unggulan memindahkannya ke posisi pertama saat kartu menumpuk, tanpa mengubah urutan HTML di desktop — hierarki penjualan tetap terjaga di kedua wadah. Aturan pada max-width: 480px merapatkan padding kartu dan sel tabel, sedangkan tabel dari Bagian 3 sudah aman berkat pembungkusnya yang menggulir.

Sekarang bagian interaksinya. Tag <script src="js/main.js" defer> di <head> memuat skrip setelah HTML siap dibaca. Setiap angka harga di HTML menyimpan dua nilai lewat atribut data:

html
<span class="kartu-angka"
  data-harga-bulanan="Rp149.000"
  data-harga-tahunan="Rp1.490.000">Rp149.000</span>

Skripnya mengumpulkan seluruh angka dan satuan dengan querySelectorAll, lalu menukar textContent sesuai keadaan saklar:

js
function terapkanMode(tahunan) {
  angkaHarga.forEach(function (el) {
    var atribut = tahunan ? "data-harga-tahunan" : "data-harga-bulanan";
    el.textContent = el.getAttribute(atribut);
  });

  saklar.setAttribute("aria-checked", tahunan ? "true" : "false");
}

Karena selector membidik atribut data-harga-bulanan, kartu Enterprise yang tidak punya atribut itu otomatis diabaikan — harga “Hubungi kami” tidak perlu ikut ditukar. Baris terakhir fungsi menyelaraskan aria-checked dengan keadaan sebenarnya; elemen ber-role="switch" melaporkan statusnya lewat atribut tersebut, bukan lewat properti checked milik checkbox. Perhatikan pula pemanggilan terapkanMode(saklar.checked) di akhir skrip: keadaan awal HTML dan tampilan selalu sinkron sejak halaman dibuka. Keadaan ini sengaja tidak disimpan ke mana pun (tanpa localStorage), sehingga halaman kembali ke tagihan bulanan setiap kali dimuat ulang.

Menghormati prefers-reduced-motion #

Transisi tombol dan saklar nyaman bagi kebanyakan orang, tetapi bagi pengguna yang menyetel sistemnya mengurangi gerak, animasi justru mengganggu. Aturan terakhir stylesheet menanggapinya:

css
@media (prefers-reduced-motion: reduce) {
  .kartu-harga, .tombol, .saklar-lintasan, .saklar-kenop { transition: none; }

  .kartu-harga:hover { transform: none; }

  .tombol:active { transform: none; }
}

Seluruh transisi — kartu, tombol, dan saklar — dimatikan, angkat kartu saat dihover dan pergeseran tombol saat diklik dihilangkan, sementara fungsionalitas tetap berjalan normal: saklar tetap menukar harga, hanya tanpa animasi. Coba pula navigasi keyboard: tekan Tab dan ring fokus biru muncul pada tautan navigasi, kartu aksi, dan lintasan saklar berkat :focus-visible yang kita pasang di seluruh elemen interaktif.

css
/* ============================================================
   Halaman Harga Layanan — stylesheet utama
   Urutan: token → base → kerangka halaman → galeri harga →
   kartu harga → tabel banding → tombol → saklar tagihan →
   responsif → gerak dan fokus
   ============================================================ */

/* ---------- 1. Design token ---------- */
:root {
  /* Warna semantik, tema terang */
  --warna-latar: #f8fafc;        --warna-permukaan: #ffffff;
  --warna-teks: #0f172a;         --warna-teks-lembut: #64748b;
  --warna-garis: #e2e8f0;        --warna-utama: #4f46e5;
  --warna-utama-pekat: #4338ca;  --warna-utama-kontras: #ffffff;
  --warna-utama-lembut: #e0e7ff; --warna-utama-tinta: #312e81;
  --warna-fokus: #1d4ed8;        --warna-sukses: #15803d;
  --warna-hemat-latar: #ccfbf1;  --warna-hemat-teks: #0f766e;
  --warna-lintasan: #cbd5e1;

  /* 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: 8px; --radius: 12px;
  --radius-penuh: 999px;

  /* Elevasi bayangan, tiga tingkat */
  --bayangan-1: 0 1px 2px rgba(15, 23, 42, 0.06);
  --bayangan-2: 0 6px 16px rgba(79, 70, 229, 0.14);
  --bayangan-3: 0 16px 32px rgba(79, 70, 229, 0.18);

  /* Tipografi dan gerak */
  --font-dasar: system-ui, "Segoe UI", sans-serif;
  --tinggi-baris: 1.6;
  --durasi: 180ms; --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, ul { 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-4);
  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-4); flex-wrap: wrap;
}

.brand { display: flex; align-items: center; gap: var(--jarak-2); }
.brand img { width: 36px; height: 36px; }
.brand-nama { font-size: 19px; font-weight: 800; }

.nav-kepala { display: flex; gap: var(--jarak-4); }
.nav-kepala a { color: var(--warna-teks-lembut); font-size: 15px; font-weight: 600; text-decoration: none; }
.nav-kepala a:hover { color: var(--warna-utama); }

.hero { padding-block: var(--jarak-7) var(--jarak-5); text-align: center; }
.judul-halaman { font-size: clamp(1.8rem, 4vw, 2.6rem); line-height: 1.2; letter-spacing: -0.02em; }
.pengantar { max-width: 56ch; margin: var(--jarak-3) auto 0; color: var(--warna-teks-lembut); }

.pilih-tagihan { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: var(--jarak-3); margin-top: var(--jarak-5); }

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

.lencana-hemat { padding: 3px 12px; border-radius: var(--radius-penuh); background-color: var(--warna-hemat-latar); color: var(--warna-hemat-teks); font-size: 12.5px; font-weight: 700; }

.laman-kaki { margin-top: var(--jarak-8); padding-block: var(--jarak-5); 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); font-size: 13.5px; color: var(--warna-teks-lembut); }

/* ---------- 4. Galeri harga ---------- */
.galeri-harga { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: var(--jarak-5); align-items: stretch; margin-top: var(--jarak-6); }

/* ---------- 5. Kartu harga ---------- */
.kartu-harga {
  position: relative;
  display: flex; flex-direction: column;
  gap: var(--jarak-3);
  padding: var(--jarak-5) var(--jarak-4);
  background-color: var(--warna-permukaan);
  border: 1px solid var(--warna-garis);
  border-radius: var(--radius);
  box-shadow: var(--bayangan-1);
  transition: box-shadow var(--durasi) var(--pelunakan), transform var(--durasi) var(--pelunakan);
}

.kartu-nama { font-size: 19px; font-weight: 800; }
.kartu-deskripsi { font-size: 14px; color: var(--warna-teks-lembut); }
.kartu-harga-baris { display: flex; align-items: baseline; gap: 6px; margin-top: var(--jarak-1); }
.kartu-angka { font-size: 2.1rem; font-weight: 800; letter-spacing: -0.02em; }
.kartu-angka--custom { font-size: 1.5rem; color: var(--warna-utama); }
.kartu-satuan { font-size: 14px; color: var(--warna-teks-lembut); }

.kartu-fitur { display: grid; gap: var(--jarak-2); margin-top: var(--jarak-1); padding: 0; list-style: none; font-size: 14.5px; }

.kartu-fitur li { position: relative; padding-left: 26px; }
.kartu-fitur li::before { content: "✓"; position: absolute; left: 0; color: var(--warna-sukses); font-weight: 700; }
.kartu-harga .tombol { margin-top: auto; text-align: center; }

.kartu-lencana { position: absolute; top: -12px; left: 50%; transform: translateX(-50%); padding: 4px 14px; border-radius: var(--radius-penuh); background-color: var(--warna-utama); color: var(--warna-utama-kontras); font-size: 12px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; }

.kartu-harga--unggulan {
  border: 2px solid var(--warna-utama);
  box-shadow: var(--bayangan-2);
}

.kartu-harga--unggulan .kartu-angka { color: var(--warna-utama); }

.kartu-harga:hover {
  box-shadow: var(--bayangan-2);
  transform: translateY(-2px);
}

.kartu-harga--unggulan:hover { box-shadow: var(--bayangan-3); }

/* ---------- 6. Tabel perbandingan ---------- */
.seksi-banding { margin-top: var(--jarak-7); }
.section-judul { font-size: 24px; font-weight: 800; letter-spacing: -0.01em; }

.tabel-gulir { margin-top: var(--jarak-4); overflow-x: auto; background-color: var(--warna-permukaan); border: 1px solid var(--warna-garis); border-radius: var(--radius); }

.tabel-banding { width: 100%; min-width: 560px; border-collapse: collapse; font-size: 14.5px; }

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

.tabel-banding thead th { background-color: var(--warna-utama-lembut); color: var(--warna-utama-tinta); font-size: 13px; letter-spacing: 0.05em; text-transform: uppercase; }

.tabel-banding tbody th { font-weight: 600; }
.tabel-banding tbody tr:nth-child(even) { background-color: var(--warna-latar); }

.tabel-banding tbody tr:last-child th, .tabel-banding tbody tr:last-child td { border-bottom: none; }

/* ---------- 7. Tombol ---------- */
.tombol {
  display: inline-block;
  padding: 10px 20px;
  border: none; border-radius: var(--radius-kecil);
  background-color: var(--warna-utama); color: var(--warna-utama-kontras);
  font: inherit; font-size: 15px; font-weight: 600;
  text-decoration: none; cursor: pointer;
  transition: background-color var(--durasi) var(--pelunakan), transform var(--durasi) var(--pelunakan);
}

.tombol:hover { background-color: var(--warna-utama-pekat); }
.tombol:active { transform: translateY(1px); }

.tombol--garis {
  border: 1px solid var(--warna-utama);
  background-color: transparent;
  color: var(--warna-utama);
}

.tombol--garis:hover { background-color: var(--warna-utama-lembut); }

/* ---------- 8. Saklar tagihan ---------- */
.saklar { position: relative; display: inline-flex; cursor: pointer; }

.saklar input { position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }

.saklar-lintasan { display: block; width: 46px; height: 26px; border-radius: var(--radius-penuh); background-color: var(--warna-lintasan); transition: background-color var(--durasi) var(--pelunakan); }

.saklar-kenop { position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; border-radius: 50%; background-color: var(--warna-permukaan); box-shadow: 0 1px 3px rgba(15, 23, 42, 0.3); transition: transform var(--durasi) var(--pelunakan); }

.saklar input:checked + .saklar-lintasan { background-color: var(--warna-utama); }
.saklar input:checked + .saklar-lintasan .saklar-kenop { transform: translateX(20px); }

.saklar input:focus-visible + .saklar-lintasan { outline: 2px solid var(--warna-fokus); outline-offset: 2px; }

.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; }

/* ---------- 9. Responsif ---------- */
@media (max-width: 720px) {
  .hero { padding-block: var(--jarak-6) var(--jarak-4); }
  .galeri-harga { grid-template-columns: 1fr; }

  /* Paket terlaris naik ke urutan pertama saat kartu menumpuk */
  .kartu-harga--unggulan { order: -1; }
  .kaki-baris { justify-content: center; text-align: center; }
}

@media (max-width: 480px) {
  .kartu-harga { padding: var(--jarak-4) var(--jarak-3); }
  .tabel-banding { font-size: 13.5px; }

  .tabel-banding th, .tabel-banding td { padding: 10px 12px; }
}

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

.tombol:focus-visible { outline-offset: 3px; }

@media (prefers-reduced-motion: reduce) {
  .kartu-harga, .tombol, .saklar-lintasan, .saklar-kenop { transition: none; }

  .kartu-harga:hover { transform: none; }

  .tombol:active { transform: none; }
}
html
<!DOCTYPE html>
<html lang="id">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Halaman Harga Layanan — LumbungKas</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 LumbungKas" width="36" height="36">
        <span class="brand-nama">LumbungKas</span>
      </div>
      <nav class="nav-kepala" aria-label="Navigasi utama">
        <a href="#paket">Paket</a>
        <a href="#perbandingan">Perbandingan</a>
      </nav>
    </div>
  </header>

  <main>
    <div class="pembungkus">
      <div class="hero">
        <h1 class="judul-halaman">Pilih paket yang pas untuk bisnismu</h1>
        <p class="pengantar">Pembukuan awan untuk UMKM: catat penjualan, kirim faktur, dan pantau laporan dari satu tempat. Semua paket bisa dicoba gratis 14 hari.</p>
        <div class="pilih-tagihan">
          <span class="pilih-label">Bulanan</span>
          <label class="saklar">
            <input type="checkbox" id="saklar-tagihan" role="switch" aria-checked="false">
            <span class="saklar-lintasan" aria-hidden="true"><span class="saklar-kenop"></span></span>
            <span class="sr-only">Ganti ke tagihan tahunan</span>
          </label>
          <span class="pilih-label">Tahunan</span>
          <span class="lencana-hemat">Hemat 2 bulan</span>
        </div>
      </div>

      <section class="galeri-harga" id="paket" aria-label="Daftar paket harga">
        <article class="kartu-harga">
          <h2 class="kartu-nama">Pemula</h2>
          <p class="kartu-deskripsi">Untuk usaha yang baru mulai mencatat transaksi.</p>
          <p class="kartu-harga-baris">
            <span class="kartu-angka" data-harga-bulanan="Rp49.000" data-harga-tahunan="Rp490.000">Rp49.000</span>
            <span class="kartu-satuan" data-satuan-bulanan="/bulan" data-satuan-tahunan="/tahun">/bulan</span>
          </p>
          <ul class="kartu-fitur">
            <li>1 pengguna</li>
            <li>30 faktur per bulan</li>
            <li>Laporan penjualan dasar</li>
            <li>Dukungan lewat surel</li>
          </ul>
          <a class="tombol tombol--garis" href="#">Mulai gratis 14 hari</a>
        </article>

        <article class="kartu-harga kartu-harga--unggulan">
          <span class="kartu-lencana">Terlaris</span>
          <h2 class="kartu-nama">Bisnis</h2>
          <p class="kartu-deskripsi">Untuk tim penjualan yang butuh laporan lengkap.</p>
          <p class="kartu-harga-baris">
            <span class="kartu-angka" data-harga-bulanan="Rp149.000" data-harga-tahunan="Rp1.490.000">Rp149.000</span>
            <span class="kartu-satuan" data-satuan-bulanan="/bulan" data-satuan-tahunan="/tahun">/bulan</span>
          </p>
          <ul class="kartu-fitur">
            <li>5 pengguna</li>
            <li>500 faktur per bulan</li>
            <li>Laporan keuangan lengkap</li>
            <li>Ekspor data akuntansi</li>
          </ul>
          <a class="tombol" href="#">Coba paket Bisnis</a>
        </article>

        <article class="kartu-harga">
          <h2 class="kartu-nama">Enterprise</h2>
          <p class="kartu-deskripsi">Untuk jaringan cabang dengan kebutuhan khusus.</p>
          <p class="kartu-harga-baris">
            <span class="kartu-angka kartu-angka--custom">Hubungi kami</span>
          </p>
          <ul class="kartu-fitur">
            <li>Pengguna tanpa batas</li>
            <li>Multi gudang dan multi cabang</li>
            <li>Integrasi khusus</li>
            <li>Dukungan prioritas</li>
          </ul>
          <a class="tombol tombol--garis" href="#">Jadwalkan demo</a>
        </article>
      </section>

      <section class="seksi-banding" id="perbandingan" aria-labelledby="judul-banding">
        <h2 class="section-judul" id="judul-banding">Perbandingan fitur</h2>
        <div class="tabel-gulir">
          <table class="tabel-banding">
            <caption class="sr-only">Perbandingan fitur antar paket LumbungKas</caption>
            <thead>
              <tr><th scope="col">Fitur</th><th scope="col">Pemula</th><th scope="col">Bisnis</th><th scope="col">Enterprise</th></tr>
            </thead>
            <tbody>
              <tr><th scope="row">Pengguna</th><td>1</td><td>5</td><td>Tanpa batas</td></tr>
              <tr><th scope="row">Faktur per bulan</th><td>30</td><td>500</td><td>Tanpa batas</td></tr>
              <tr><th scope="row">Laporan keuangan</th><td>Dasar</td><td>Lengkap</td><td>Lengkap</td></tr>
              <tr><th scope="row">Multi gudang</th><td>—</td><td>2 lokasi</td><td>Tanpa batas</td></tr>
              <tr><th scope="row">Ekspor data akuntansi</th><td>—</td><td>✓</td><td>✓</td></tr>
              <tr><th scope="row">Dukungan prioritas</th><td>—</td><td>✓</td><td>✓</td></tr>
            </tbody>
          </table>
        </div>
      </section>
    </div>
  </main>

  <footer class="laman-kaki">
    <div class="pembungkus kaki-baris">
      <span>LumbungKas — proyek latihan CSS</span>
      <span>Harga dalam Rupiah dan belum termasuk pajak</span>
    </div>
  </footer>
</body>
</html>
js
/* ============================================================
   main.js — saklar tagihan bulanan/tahunan Halaman Harga Layanan.
   Setiap kartu menyimpan dua versi harga pada atribut
   data-harga-bulanan dan data-harga-tahunan; skrip hanya
   menukar teksnya lalu menyelaraskan aria-checked pada saklar.
   Tidak ada penyimpanan di sisi browser.
   ============================================================ */
(function () {
  "use strict";

  var saklar = document.getElementById("saklar-tagihan");

  if (!saklar) {
    return;
  }

  var angkaHarga = document.querySelectorAll("[data-harga-bulanan]");
  var satuanHarga = document.querySelectorAll("[data-satuan-bulanan]");

  /* Tukar teks harga dan satuan sesuai mode tagihan aktif. */
  function terapkanMode(tahunan) {
    angkaHarga.forEach(function (el) {
      var atribut = tahunan ? "data-harga-tahunan" : "data-harga-bulanan";
      el.textContent = el.getAttribute(atribut);
    });

    satuanHarga.forEach(function (el) {
      var atribut = tahunan ? "data-satuan-tahunan" : "data-satuan-bulanan";
      el.textContent = el.getAttribute(atribut);
    });

    /* role="switch" melaporkan status lewat aria-checked. */
    saklar.setAttribute("aria-checked", tahunan ? "true" : "false");
  }

  saklar.addEventListener("change", function () {
    terapkanMode(saklar.checked);
  });

  /* Selaraskan tampilan awal dengan kondisi centang pada HTML. */
  terapkanMode(saklar.checked);
})();

Ringkasan #

Proyek selesai. Halaman harga LumbungKas kini punya kerangka bertoken, grid harga adaptif dengan kartu terlaris yang menonjol, tabel perbandingan zebra yang menggulir dengan aman, tombol berdua varian dengan state lengkap, saklar tagihan yang menukar harga dan melaporkan statusnya lewat aria-checked, serta perhatian pada fokus keyboard dan pengurangan gerak. Sebagai latihan lanjutan, coba tambahkan baris “Penyimpanan awan” pada tabel perbandingan, atau varian kartu tahunan yang menampilkan harga rata-rata per bulan.