Pembangunan proyek

Proyek terpandu: situs responsif multi-halaman dengan theming sebagai penerapan seluruh materi jalur.

Daftar isi

Topik ini membangun satu proyek utuh bernama Kartu Piket Kelas, yaitu situs responsif multi-halaman dengan tiga halaman, satu stylesheet bersama, dan theming terang gelap. Proyek ini merangkai materi seluruh jalur, mulai dari token warna, navbar dengan flexbox, kisi kartu dengan grid auto-fit, hingga media query untuk layar sempit. Setiap berkas proyek ditampilkan utuh, dan setelah alur pembangunannya selesai, situs yang sama dapat direproduksi serta diuji dengan DevTools.

Struktur berkas dan halaman beranda#

Proyek ini terdiri atas empat berkas yang semuanya ditempatkan dalam satu folder yang sama. Tiga berkas HTML menyusun halamannya, sedangkan satu berkas CSS menata gaya ketiganya. Nama keempat berkas pendek dan tanpa spasi, sehingga jalur relatif antar berkas tetap sederhana.

  • index.html: halaman beranda yang memuat sambutan dan ringkasan pekerjaan piket.
  • daftar-piket.html: halaman daftar piket berupa enam kartu, satu kartu untuk setiap hari.
  • kontak.html: halaman kontak yang memuat formulir pesan.
  • styles.css: stylesheet bersama yang melekat pada ketiga halaman.

Ketiga halaman melekatkan styles.css melalui elemen link, sehingga satu perubahan gaya otomatis berlaku di seluruh situs. Setiap halaman menggunakan kerangka semantik yang sama, yaitu header untuk navbar, main untuk isi, dan footer untuk kaki halaman. Tautan halaman yang sedang dibuka membawa atribut aria-current="page", sehingga browser dan pembaca layar (screen reader) mengetahui posisi pengguna. Meta viewport juga dideklarasikan karena layout responsif memerlukan lebar perangkat yang dilaporkan dengan benar.

Berkas beranda ditampilkan utuh sebagai berikut. Strukturnya disalin ke dua halaman lainnya. Bagian yang berubah pada halaman lain hanyalah isi main dan penanda aria-current pada navbar.

html index.html
<!DOCTYPE html>
<html lang="id">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Kartu Piket Kelas</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <header class="navbar">
    <nav class="navbar-isi" aria-label="Navigasi utama">
      <a class="navbar-merek" href="index.html">Kartu Piket Kelas</a>
      <div class="navbar-tautan">
        <a href="index.html" aria-current="page">Beranda</a>
        <a href="daftar-piket.html">Daftar piket</a>
        <a href="kontak.html">Kontak</a>
      </div>
    </nav>
  </header>

  <main class="konten">
    <section class="sambutan">
      <h1>Jadwal piket kelas 9B</h1>
      <p>
        Situs ini memuat pembagian tugas piket mingguan kelas 9B.
        Setiap kelompok bertugas satu hari dan mengurus tiga pekerjaan,
        yaitu menyapu lantai, merapikan meja, dan mengosongkan tempat sampah.
      </p>
      <a class="tombol" href="daftar-piket.html">Lihat daftar piket</a>
    </section>

    <section>
      <h2>Ringkasan pekerjaan piket</h2>
      <div class="kisi-kartu">
        <article class="kartu">
          <h3>Menyapu lantai</h3>
          <p>Lantai kelas disapu sebelum jam pelajaran pertama dimulai.</p>
        </article>
        <article class="kartu">
          <h3>Merapikan meja</h3>
          <p>Kursi dan meja ditata kembali pada akhir hari pelajaran.</p>
        </article>
        <article class="kartu">
          <h3>Mengosongkan tempat sampah</h3>
          <p>Tempat sampah kelas dikosongkan ke tempat pembuangan sekolah.</p>
        </article>
      </div>
    </section>
  </main>

  <footer class="kaki-halaman">
    <p>Kartu Piket Kelas, kelas 9B.</p>
  </footer>
</body>
</html>

Bagian sambutan memuat satu ajakan menuju halaman daftar melalui kelas tombol. Ringkasan pekerjaan menggunakan kelas kisi-kartu, sehingga perilaku kisi cukup ditulis satu kali pada styles.css. Ketiga kartu ringkasannya hanya berisi satu judul dan satu paragraf pendek.

Token warna dan tema gelap#

Semua warna dan ukuran utama proyek dinyatakan sebagai custom property pada selector :root, kemudian dibaca kembali melalui fungsi var(). Penamaannya diberi awalan --warna, --radius, --jarak, dan --lebar-maks supaya maksud setiap token terbaca dari namanya. Berkas styles.css berikut ditampilkan utuh, dan setiap bagiannya dibahas pada bagian-bagian berikutnya.

css styles.css
/* ============================================================
   Kartu Piket Kelas
   Stylesheet bersama untuk index.html, daftar-piket.html,
   dan kontak.html. Diurutkan dari token, reset, komponen,
   hingga media query.
   ============================================================ */

/* Token warna dan ukuran untuk tema terang */
:root {
  --warna-latar: #f4f6f8;
  --warna-kartu: #ffffff;
  --warna-teks: #1d2733;
  --warna-teks-lembut: #5b6774;
  --warna-aksen: #2f6fed;
  --warna-aksen-teks: #ffffff;
  --warna-aksen-lembut: #dbe7ff;
  --warna-garis: #d8dee5;
  --radius-kartu: 12px;
  --jarak: 16px;
  --lebar-maks: 960px;
}

/* Token yang sama ditimpa saat sistem meminta tema gelap */
@media (prefers-color-scheme: dark) {
  :root {
    --warna-latar: #14181f;
    --warna-kartu: #1e242e;
    --warna-teks: #e7ecf2;
    --warna-teks-lembut: #9aa7b4;
    --warna-aksen: #7ba4f7;
    --warna-aksen-teks: #101623;
    --warna-aksen-lembut: #2c3b58;
    --warna-garis: #2b333d;
  }
}

/* Reset ringan */
* {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: system-ui, sans-serif;
  line-height: 1.6;
  background-color: var(--warna-latar);
  color: var(--warna-teks);
}

/* Navbar: dua kontainer flex yang bersarang */
.navbar {
  background-color: var(--warna-kartu);
  border-bottom: 1px solid var(--warna-garis);
}

.navbar-isi {
  max-width: var(--lebar-maks);
  margin: 0 auto;
  padding: 12px var(--jarak);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--jarak);
}

.navbar-merek {
  color: var(--warna-teks);
  font-size: 18px;
  font-weight: 700;
  text-decoration: none;
}

.navbar-tautan {
  display: flex;
  gap: 6px;
}

.navbar-tautan a {
  color: var(--warna-teks-lembut);
  text-decoration: none;
  padding: 6px 12px;
  border-radius: 999px;
}

.navbar-tautan a:hover {
  color: var(--warna-teks);
  background-color: var(--warna-aksen-lembut);
}

/* Tautan halaman aktif membawa aria-current="page" di HTML */
.navbar-tautan a[aria-current="page"] {
  color: var(--warna-aksen);
  background-color: var(--warna-aksen-lembut);
  font-weight: 600;
}

/* Kerangka isi di bawah navbar */
.konten {
  max-width: var(--lebar-maks);
  margin: 0 auto;
  padding: 32px var(--jarak) 56px;
}

.sambutan {
  max-width: 560px;
}

.tombol {
  display: inline-block;
  background-color: var(--warna-aksen);
  color: var(--warna-aksen-teks);
  font-weight: 600;
  text-decoration: none;
  padding: 10px 20px;
  border-radius: 999px;
}

.tombol:hover {
  filter: brightness(1.15);
}

/* Kisi kartu yang jumlah kolomnya menyesuaikan lebar kontainer */
.kisi-kartu {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: var(--jarak);
}

.kartu {
  background-color: var(--warna-kartu);
  border: 1px solid var(--warna-garis);
  border-radius: var(--radius-kartu);
  padding: 16px 20px;
}

.kartu h2,
.kartu h3 {
  margin-top: 0;
}

.kartu p {
  color: var(--warna-teks-lembut);
}

.daftar-anggota {
  margin: 0;
  padding-left: 20px;
  color: var(--warna-teks-lembut);
}

/* Formulir kontak */
.formulir {
  display: grid;
  gap: 14px;
  max-width: 420px;
}

.formulir label {
  font-weight: 600;
}

.formulir input,
.formulir textarea {
  font: inherit;
  width: 100%;
  color: inherit;
  background-color: var(--warna-kartu);
  border: 1px solid var(--warna-garis);
  border-radius: 8px;
  padding: 10px 12px;
}

.formulir input:focus,
.formulir textarea:focus {
  outline: 2px solid var(--warna-aksen);
  outline-offset: 2px;
}

.formulir button {
  font: inherit;
  font-weight: 600;
  color: var(--warna-aksen-teks);
  background-color: var(--warna-aksen);
  border: none;
  border-radius: 999px;
  padding: 10px 20px;
  justify-self: start;
  cursor: pointer;
}

/* Kaki halaman */
.kaki-halaman {
  max-width: var(--lebar-maks);
  margin: 0 auto;
  padding: 0 var(--jarak) 32px;
  color: var(--warna-teks-lembut);
  font-size: 14px;
}

/* Navbar menumpuk pada layar sempit */
@media (max-width: 560px) {
  .navbar-isi {
    flex-direction: column;
    align-items: stretch;
    text-align: center;
  }

  .navbar-tautan {
    justify-content: center;
    flex-wrap: wrap;
  }
}

Urutan aturan di dalam berkas tersebut mengikuti tingkat cakupannya, dari token, reset ringan, kerangka navbar, komponen kartu dan formulir, kaki halaman, hingga dua blok media query. Resetnya hanya terdiri atas box-sizing: border-box untuk semua elemen beserta pengaturan awal pada body. Seluruh aturan hanya menggunakan kelas dan selector elemen sederhana tanpa ID, sehingga bobot specificity tetap rendah dan konflik cascade mudah dihindari.

Tema gelap bekerja tanpa menyentuh satu aturan komponen pun. Blok @media (prefers-color-scheme: dark) hanya menimpa nilai token yang sama pada :root, sedangkan seluruh komponen terus membaca nama token yang sama sehingga warnanya ikut berganti. Token --warna-aksen-teks disiapkan terpisah karena teks yang berada di atas warna aksen membutuhkan nilai kontras yang berbeda antara tema terang dan gelap.

Navbar tersusun atas dua kontainer flex yang bersarang. Kontainer luar .navbar-isi menjejerkan merek dan kelompok tautan dengan justify-content: space-between, sehingga keduanya menempel pada ujung yang berlawanan. Kontainer dalam .navbar-tautan menjejerkan tiga tautan menu dengan gap kecil, sedangkan align-items: center pada kontainer luar menyamakan kedudukan vertikal merek dan kelompok tautan. Deklarasi display: flex hanya mengatur anak langsungnya, sehingga setiap kelompok tautan memerlukan pembungkus yang menjadi kontainer sendiri.

Keadaan tautan diberi umpan balik visual melalui dua aturan. Aturan :hover mengubah warna teks dan latar saat kursor berada di atasnya, sedangkan selector atribut a[aria-current="page"] menetapkan tampilan permanen bagi halaman yang sedang dibuka. Bentuk pil pada kedua keadaan itu berasal dari border-radius: 999px, dan warnanya menggunakan token --warna-aksen-lembut sehingga ikut menyesuaikan tema gelap.

Perubahan navbar pada layar sempit sengaja tidak ditulis di bagian ini. Aturan penumpukannya berada di dalam media query tersendiri pada akhir berkas, sehingga gaya dasar dan pengecualiannya terpisah dengan jelas. Pemisahan semacam ini memudahkan penelusuran karena setiap aturan hanya muncul pada satu tempat.

Kisi kartu pada daftar piket#

Halaman daftar piket menampilkan enam kartu, satu untuk setiap hari, dan seluruhnya diatur oleh kelas kisi-kartu yang sama dengan beranda. Isi kartunya berupa judul hari dan daftar nama anggota, sedangkan urutan kartunya mengikuti urutan penulisan di HTML. Berkasnya ditampilkan utuh sebagai berikut.

html daftar-piket.html
<!DOCTYPE html>
<html lang="id">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Daftar piket, Kartu Piket Kelas</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <header class="navbar">
    <nav class="navbar-isi" aria-label="Navigasi utama">
      <a class="navbar-merek" href="index.html">Kartu Piket Kelas</a>
      <div class="navbar-tautan">
        <a href="index.html">Beranda</a>
        <a href="daftar-piket.html" aria-current="page">Daftar piket</a>
        <a href="kontak.html">Kontak</a>
      </div>
    </nav>
  </header>

  <main class="konten">
    <section class="sambutan">
      <h1>Daftar piket minggu ini</h1>
      <p>
        Setiap kelompok bertugas pada hari yang telah ditetapkan.
        Pertukaran hari tugas antar anggota disepakati terlebih dahulu,
        kemudian dilaporkan melalui halaman kontak.
      </p>
    </section>

    <div class="kisi-kartu">
      <article class="kartu">
        <h2>Senin</h2>
        <ul class="daftar-anggota">
          <li>Ali</li>
          <li>Budi</li>
        </ul>
      </article>
      <article class="kartu">
        <h2>Selasa</h2>
        <ul class="daftar-anggota">
          <li>Citra</li>
          <li>Dewi</li>
        </ul>
      </article>
      <article class="kartu">
        <h2>Rabu</h2>
        <ul class="daftar-anggota">
          <li>Eka</li>
          <li>Farhan</li>
        </ul>
      </article>
      <article class="kartu">
        <h2>Kamis</h2>
        <ul class="daftar-anggota">
          <li>Gita</li>
          <li>Hendra</li>
        </ul>
      </article>
      <article class="kartu">
        <h2>Jumat</h2>
        <ul class="daftar-anggota">
          <li>Intan</li>
          <li>Joko</li>
        </ul>
      </article>
      <article class="kartu">
        <h2>Sabtu</h2>
        <ul class="daftar-anggota">
          <li>Kiki</li>
          <li>Lutfi</li>
        </ul>
      </article>
    </div>
  </main>

  <footer class="kaki-halaman">
    <p>Kartu Piket Kelas, kelas 9B.</p>
  </footer>
</body>
</html>

Kunci perilaku kisi terletak pada satu deklarasi grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)). Browser membentuk kolom sebanyak mungkin selama lebar minimum 200 piksel masih muat, kemudian sisa ruang dibagi sama melalui satuan fr. Ketika kontainer menyempit dan kolom berikutnya tidak muat lagi, jumlah kolom berkurang secara otomatis hingga tersisa satu kolom penuh.

Konten di dalam kartu juga ikut ditata. Selector .kartu h2 dan .kartu h3 menghapus margin atas judul agar teks tidak terlalu turun dari tepi kartu, sedangkan .daftar-anggota menata ulang daftar nama dengan warna teks lembut. Jarak antarkartu menggunakan token --jarak, sehingga perubahan kerapatan kisi cukup dilakukan pada satu tempat.

Formulir kontak#

Halaman ketiga memuat formulir untuk menyampaikan pertukaran hari tugas dan pertanyaan lainnya. Formulir ini statis dan tidak ada server yang memproses kiriman, sehingga tombol kirim hanya melengkapi tampilan antarmuka. Pasangan label dan kolom isian dihubungkan melalui atribut for dan id, sehingga pembaca layar menyebut nama kolom dengan benar. Tipe email memberikan pemeriksaan dasar dari browser, sedangkan textarea menampung pesan yang lebih panjang.

html kontak.html
<!DOCTYPE html>
<html lang="id">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Kontak, Kartu Piket Kelas</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <header class="navbar">
    <nav class="navbar-isi" aria-label="Navigasi utama">
      <a class="navbar-merek" href="index.html">Kartu Piket Kelas</a>
      <div class="navbar-tautan">
        <a href="index.html">Beranda</a>
        <a href="daftar-piket.html">Daftar piket</a>
        <a href="kontak.html" aria-current="page">Kontak</a>
      </div>
    </nav>
  </header>

  <main class="konten">
    <section class="sambutan">
      <h1>Kontak</h1>
      <p>
        Pertukaran hari tugas dan pertanyaan seputar piket disampaikan
        melalui formulir berikut. Pesan diteruskan kepada ketua kelompok
        piket pada hari yang bersangkutan.
      </p>
    </section>

    <form class="formulir">
      <label for="nama">Nama</label>
      <input id="nama" name="nama" type="text" autocomplete="name">

      <label for="email">Email</label>
      <input id="email" name="email" type="email" autocomplete="email">

      <label for="pesan">Pesan</label>
      <textarea id="pesan" name="pesan" rows="5"></textarea>

      <button type="submit">Kirim pesan</button>
    </form>
  </main>

  <footer class="kaki-halaman">
    <p>Kartu Piket Kelas, kelas 9B.</p>
  </footer>
</body>
</html>

Penataan gaya formulir bertumpu pada kelas formulir. Deklarasi display: grid beserta gap menata label dan kolom satu per satu menurun tanpa margin manual, sedangkan max-width menjaga lebar kolom tetap nyaman dibaca. Deklarasi font: inherit pada input, textarea, dan button diperlukan karena kontrol formulir tidak mewarisi huruf halaman secara default. Lebar setiap kolom isian mengikuti kontainernya melalui width: 100%, sedangkan tombol menyusut mengikuti isinya berkat justify-self: start. Gaya :focus dengan outline tebal sengaja dipertahankan agar posisi fokus keyboard tetap terlihat, dan tombol menggunakan token aksen beserta --warna-aksen-teks sebagaimana kelas tombol.

Media query untuk layar sempit#

Dua blok @media pada styles.css mengerjakan tugas yang berbeda. Blok prefers-color-scheme merespons preferensi sistem, sedangkan blok max-width: 560px merespons lebar viewport. Di dalamnya, .navbar-isi berpindah menjadi kolom melalui flex-direction: column, merek menumpuk di atas tautan, dan text-align: center memusatkan keduanya. Kelompok tautan diberi flex-wrap: wrap agar tiga tautannya tetap sebaris bila muat atau melipat dengan rapi bila tidak. Blok ini hanya menyentuh navbar karena satu-satunya komponen yang sesak pada layar sempit adalah baris menunya, sedangkan kisi kartu sudah menyesuaikan diri tanpa bantuan media query.

Angka 560 piksel dipilih dengan mengamati kontennya, bukan dengan menyalin daftar perangkat. Tiga tautan menu beserta merek masih sebaris dengan nyaman pada lebar itu, sedangkan di bawahnya mulai sesak. Penulisan max-width digunakan karena gaya dasar sudah bekerja untuk semua ukuran dan pengecualiannya hanya menambah kondisi sempit. Bila proyek tumbuh, breakpoint yang sama cukup digunakan kembali untuk penyesuaian komponen lain tanpa menambah blok baru. Pada lebar di atas 560 piksel, navbar tetap sebaris tanpa perubahan.

Pemeriksaan akhir dengan DevTools#

Pemeriksaan akhir memanfaatkan DevTools yang tersedia pada setiap browser modern. Mode perangkat (device toolbar) meniru berbagai lebar viewport, sehingga perpindahan navbar ke mode menumpuk dan berkurangnya kolom kartu dapat diamati tanpa menyusutkan jendela sungguhan. Emulasi prefers-color-scheme pada panel Rendering memaksa tema gelap aktif, berguna ketika sistem operasi tidak sedang berada dalam mode gelap. Panel Computed menunjukkan nilai akhir setiap properti, termasuk hasil var() yang sudah diuraikan, sehingga token yang keliru langsung terlihat.

Pratinjau berikut merender index.html di dalam iframe. Persempit lebar panel pratinjau untuk melihat navbar menumpuk dan jumlah kolom kartu berkurang. Keduanya berpindah pada lebar yang berbeda, sehingga perilakunya mudah dibedakan.

Kode sumber demo
<!DOCTYPE html>
<html lang="id">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Kartu Piket Kelas</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <header class="navbar">
    <nav class="navbar-isi" aria-label="Navigasi utama">
      <a class="navbar-merek" href="index.html">Kartu Piket Kelas</a>
      <div class="navbar-tautan">
        <a href="index.html" aria-current="page">Beranda</a>
        <a href="daftar-piket.html">Daftar piket</a>
        <a href="kontak.html">Kontak</a>
      </div>
    </nav>
  </header>

  <main class="konten">
    <section class="sambutan">
      <h1>Jadwal piket kelas 9B</h1>
      <p>
        Situs ini memuat pembagian tugas piket mingguan kelas 9B.
        Setiap kelompok bertugas satu hari dan mengurus tiga pekerjaan,
        yaitu menyapu lantai, merapikan meja, dan mengosongkan tempat sampah.
      </p>
      <a class="tombol" href="daftar-piket.html">Lihat daftar piket</a>
    </section>

    <section>
      <h2>Ringkasan pekerjaan piket</h2>
      <div class="kisi-kartu">
        <article class="kartu">
          <h3>Menyapu lantai</h3>
          <p>Lantai kelas disapu sebelum jam pelajaran pertama dimulai.</p>
        </article>
        <article class="kartu">
          <h3>Merapikan meja</h3>
          <p>Kursi dan meja ditata kembali pada akhir hari pelajaran.</p>
        </article>
        <article class="kartu">
          <h3>Mengosongkan tempat sampah</h3>
          <p>Tempat sampah kelas dikosongkan ke tempat pembuangan sekolah.</p>
        </article>
      </div>
    </section>
  </main>

  <footer class="kaki-halaman">
    <p>Kartu Piket Kelas, kelas 9B.</p>
  </footer>
</body>
</html>

Tiga halaman diperiksa berurutan karena ketiganya menggunakan kelas yang sama. Kesalahan yang paling sering ditemukan pada tahap ini adalah halaman yang dapat digulir ke samping karena ada elemen yang lebih lebar dari viewport. Panel Elements membantu menemukan elemen penyebabnya, dan perbaikannya biasanya berupa penggantian lebar tetap dengan max-width.

Pengembangan lanjutan#

Proyek ini sengaja dijaga tetap ramping, dan beberapa arah pengembangan wajar untuk ditempuh selanjutnya. Kisi kartu saat ini merespons lebar viewport, sedangkan container query membuat setiap kartu merespons lebar wadahnya sendiri melalui deklarasi container-type: inline-size beserta aturan @container. Dengan arah tersebut, kartu yang sama tampil tepat baik pada kisi lebar maupun di dalam kolom sempit.

Gerak halus merupakan arah kedua. Transisi singkat pada :hover kartu dan tombol memberi umpan balik yang lebih jelas, tetapi setiap animasi sebaiknya dibungkus @media (prefers-reduced-motion: reduce) yang mematikan transisi bagi pengguna yang memilih mengurangi gerak. Pola ini menjaga situs tetap nyaman tanpa menghilangkan efek visual sepenuhnya.

Penataan yang lebih rapi dapat dilanjutkan dengan mengelompokkan aturan ke dalam cascade layer melalui @layer, misalnya lapisan token, komponen, dan utilitas. Formulir juga dapat diperdalam dengan logical property agar tata letaknya aman untuk arah teks lain. Semua arah tersebut bekerja di atas struktur berkas yang sama, sehingga tidak ada perombakan HTML yang diperlukan.

Ringkasan#

  • Tiga halaman saling berbagi satu berkas styles.css, sehingga perubahan gaya cukup dilakukan pada satu tempat.
  • Token warna dinyatakan pada :root dan ditimpa oleh @media (prefers-color-scheme: dark), sehingga tema gelap aktif tanpa menyentuh aturan komponen.
  • Navbar terdiri atas dua kontainer flex bersarang dan menumpuk melalui satu media query max-width.
  • Deklarasi repeat(auto-fit, minmax(200px, 1fr)) membuat jumlah kolom kartu menyesuaikan lebar kontainer otomatis.

Referensi#