Proyek ini merakit halaman harga untuk sebuah produk SaaS fiktif bernama LumbungKas, layanan pembukuan awan untuk UMKM. Halaman harga adalah komponen yang hampir selalu kita temui pada situs produk nyata, dan di balik tampilannya yang lugas ada banyak teknik CSS yang bekerja bersama: design token, grid adaptif, varian komponen, tabel perbandingan, saklar yang dibangun dari checkbox, serta media query. Kita membangunnya bertahap dalam lima bagian; setiap bagian menyimpan snapshot berkasnya sendiri, sehingga kalian bisa mengikuti stylesheet tumbuh dari satu ratus baris menjadi lebih dari dua ratus baris lengkap dengan JavaScript penggerak saklarnya.

Perilaku yang Diharapkan #

Halaman akhirnya berisi header dengan logo, seksi hero berisi judul dan saklar tagihan, tiga kartu paket, tabel perbandingan fitur, dan footer. Perilaku yang bisa dicoba langsung:

  • Deretan kartu harga menyusun grid repeat(auto-fit, minmax(250px, 1fr)), sehingga jumlah kolom menyesuaikan lebar layar tanpa satu pun media query.
  • Kartu Bisnis tampil menonjol sebagai paket terlaris: border indigo dua piksel, bayangan lebih dalam, dan lencana yang menempel di tepi atas kartu.
  • Saklar Bulanan/Tahunan menukar seluruh angka harga sekaligus satuannya; angka baru dibaca dari atribut data-harga-* pada tiap kartu, dan status saklar dilaporkan lewat aria-checked.
  • Tabel perbandingan bergaris zebra; pada layar sempit tabel menggulir ke samping di dalam pembungkusnya, bukan merenggangkan halaman.

Cuplikan berikut meniru deretan kartu harga versi final — perhatikan kartu tengah yang lebih menonjol dibanding kedua tetangganya:

Struktur Folder #

text
halaman-harga-layanan/
├── _index.md
├── src/
│   ├── index.html
│   ├── css/
│   │   └── styles.css
│   ├── js/
│   │   └── main.js
│   └── assets/
│       └── logo.svg
├── part-1-layout-halaman/
│   ├── index.md
│   └── styles.css
├── part-2-tabel-harga-dengan-grid/
│   ├── index.md
│   ├── styles.css
│   └── beranda.html
├── part-3-feature-comparison/
│   ├── index.md
│   ├── styles.css
│   └── beranda.html
├── part-4-tombol-dan-call-to-action/
│   ├── index.md
│   ├── styles.css
│   └── beranda.html
└── part-5-responsive-pricing-layout/
    ├── index.md
    ├── styles.css
    ├── beranda.html
    └── main.js

Tahapan Pengerjaan #

  1. Bagian 1: Layout Halaman — token warna indigo, skala spasi, radius, bayangan, base reset, dan kerangka halaman dari pembungkus, header, hero, sampai footer.
  2. Bagian 2: Tabel Harga dengan Grid — grid harga adaptif, anatomi kartu paket, dan varian terlaris dengan lencana di tepi atas.
  3. Bagian 3: Feature Comparison — tabel perbandingan semantik dengan border-collapse, garis zebra, dan pengguliran horizontal yang aman.
  4. Bagian 4: Tombol dan Call to Action — tombol utama dan varian garis, saklar tagihan dari checkbox, beserta label aksesibilitasnya.
  5. Bagian 5: Responsive Pricing Layout — media query penumpuk kartu, JavaScript penukar harga, dan penghormatan prefers-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:

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>
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; }
}
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);
})();