Kontrol form adalah salah satu komponen yang paling sering memakai gaya bawaan browser, dan gaya bawaan itu berbeda antarbrowser serta tidak serasi dengan warna situs. Bagian ketiga proyek ini menulis styles/style.css untuk formulir pendaftaran: jarak antarkelompok yang konsisten, kolom yang lebar penuh namun terbaca, indikator fokus yang jelas bagi pengguna keyboard, serta tombol kirim yang menonjol. Prinsipnya satu: gaya mengikuti struktur dari Bagian 1, bukan menggantinya.

Token Warna dan Tata Letak Dasar #

Seperti pada proyek sebelumnya, seluruh warna dideklarasikan sebagai custom properties pada :root. Struktur halaman memakai main selebar maksimum 34rem — lebar yang pas untuk form agar baris label tidak terlalu panjang. Elemen form sendiri menjadi grid dengan gap: 1.25rem, sehingga jarak antar fieldset seragam tanpa margin manual satu per satu.

Buat folder styles/ pada akar proyek, lalu simpan file berikut sebagai style.css:

css styles/style.css
/* styles/style.css — Seminar Web Komunitas
   Stylesheet bersama proyek Formulir Pendaftaran.
   Ditulis pada Bagian 3, dipakai halaman form dan halaman pemeriksaan. */

/* Token desain: ubah nilai di sini, seluruh halaman ikut menyesuaikan. */
:root {
  --warna-teks: #1f2937;
  --warna-teks-lembut: #5b6472;
  --warna-latar: #f8fafc;
  --warna-aksen: #1a6b54;
  --warna-tepi: #cbd5e1;
  --warna-galat: #b3261e;
  --warna-galat-lembut: #e8b4ae;
  --warna-kartu: #e6f1ec;
}

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

main {
  max-width: 34rem;
  margin-inline: auto;
  padding: 1.5rem 1rem 3rem;
}

form {
  display: grid;
  gap: 1.25rem;
}

fieldset {
  margin: 0;
  border: 1px solid var(--warna-tepi);
  border-radius: 8px;
  padding: 1rem;
  display: grid;
  gap: 0.9rem;
}

legend {
  font-weight: 600;
  padding-inline: 0.4rem;
}

form label {
  display: block;
  margin-bottom: 0.35rem;
  font-weight: 600;
}

.kolom {
  width: 100%;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--warna-tepi);
  border-radius: 6px;
  background-color: #ffffff;
  color: inherit;
  font: inherit;
}

/* Indikator fokus yang terlihat jelas bagi navigasi keyboard. */
.kolom:focus-visible {
  outline: 3px solid var(--warna-aksen);
  outline-offset: 2px;
}

/* Tanda ringan untuk isian yang belum sah — peringatan, bukan hukuman. */
.kolom:invalid {
  border-color: var(--warna-galat-lembut);
}

.pilihan {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0;
  font-weight: normal;
}

.pesan-galat {
  margin: 0;
  color: var(--warna-galat);
  font-size: 0.9rem;
}

button[type="submit"] {
  justify-self: start;
  font: inherit;
  font-weight: 600;
  color: #ffffff;
  background-color: var(--warna-aksen);
  border: 0;
  border-radius: 999px;
  padding: 0.6rem 1.5rem;
  cursor: pointer;
}

button[type="submit"]:focus-visible {
  outline: 3px solid var(--warna-teks);
  outline-offset: 2px;
}

.ringkasan {
  border: 1px solid var(--warna-aksen);
  border-radius: 8px;
  padding: 1rem;
  background-color: var(--warna-kartu);
}

Kelas .pesan-galat dan .ringkasan akan dipakai oleh JavaScript pada Bagian 4; menyiapkannya sekarang membuat stylesheet berhenti berubah setelah tahap ini. Mari kita bedah keputusan-keputusannya:

  1. Aturan .kolom: satu class untuk semua kontrol teks, email, password, tanggal, dan select. Deklarasi font: inherit penting — tanpa itu, browser memakai jenis huruf berbeda untuk kontrol form dan formulir terlihat tidak serasi.
  2. Aturan .kolom:focus-visible: outline hijau setebal 3 piksel yang muncul ketika kontrol menerima fokus melalui keyboard. Pseudo-class :focus-visible dipilih daripada :focus karena browser hanya menampilkannya bila fokus memang datang dari navigasi keyboard, sehingga klik tetap terasa bersih.
  3. Aturan .kolom:invalid: tepi merah lembut pada kolom yang isian belum memenuhi aturan Bagian 2 — tanda visual ringan, bukan hukuman berupa latar merah menyala.
  4. Aturan .pilihan: untuk label radio dan checkbox yang berdampingan dengan kontrolnya; arah flex dan font-weight: normal membuatnya berbeda dari label kolom.
  5. Aturan button[type="submit"]: tombol aksen dengan sudut membulat penuh; justify-self: start pada grid membuat lebar tombol mengikuti isinya, bukan membentang selebar kolom.
  6. Aturan .ringkasan: kartu hijau muda untuk pesan keberhasilan yang muncul setelah seluruh isian sah.

Menghubungkan Stylesheet ke Halaman #

File style.css berada di folder styles/ pada akar proyek, sedangkan halaman form berada satu tingkat di bawahnya. Tambahkan baris berikut ke head formulir-validasi.html:

html Potongan head formulir-validasi.html
<link rel="stylesheet" href="../styles/style.css">

Awalan ../ menaikkan rujukan satu folder menuju akar proyek — pola path relatif yang sama dengan rujukan logo pada Bagian 1. Class kolom dan pilihan kemudian ditambahkan pada markup: setiap input teks, select, dan kontrol tanggal menerima class="kolom", sedangkan label radio dan checkbox menerima class="pilihan".

Langkah Pemeriksaan #

Tuntaskan Bagian ini dengan urutan berikut:

  1. Tambahkan link dan atribut class ke formulir-validasi.html, buka di browser, lalu pastikan fieldset berbingkai rapi, kolom selebar penuh, dan tombol hijau menonjol.
  2. Tekan tombol Tab beberapa kali dari kolom nama dan amati cincin hijau berpindah antarkolom — indikator fokus harus terlihat pada setiap kontrol, termasuk select dan checkbox.
  3. Jalankan ../tests/cek-struktur.html dan centang butir-butir kelompok File pendukung yang terkait stylesheet serta pemeriksaan fokus.

Formulir kini rapi dan responsif terhadap keyboard. Bagian 4 melengkapi pengalaman pengunjung: pesan kesalahan kustom per kontrol yang kalimatnya kita tentukan sendiri, disusul ringkasan keberhasilan.

Referensi resmi untuk pendalaman:

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

styles/style.css

Unduh berkas
css
/* styles/style.css — Seminar Web Komunitas
   Stylesheet bersama proyek Formulir Pendaftaran.
   Ditulis pada Bagian 3, dipakai halaman form dan halaman pemeriksaan. */

/* Token desain: ubah nilai di sini, seluruh halaman ikut menyesuaikan. */
:root {
  --warna-teks: #1f2937;
  --warna-teks-lembut: #5b6472;
  --warna-latar: #f8fafc;
  --warna-aksen: #1a6b54;
  --warna-tepi: #cbd5e1;
  --warna-galat: #b3261e;
  --warna-galat-lembut: #e8b4ae;
  --warna-kartu: #e6f1ec;
}

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

main {
  max-width: 34rem;
  margin-inline: auto;
  padding: 1.5rem 1rem 3rem;
}

form {
  display: grid;
  gap: 1.25rem;
}

fieldset {
  margin: 0;
  border: 1px solid var(--warna-tepi);
  border-radius: 8px;
  padding: 1rem;
  display: grid;
  gap: 0.9rem;
}

legend {
  font-weight: 600;
  padding-inline: 0.4rem;
}

form label {
  display: block;
  margin-bottom: 0.35rem;
  font-weight: 600;
}

.kolom {
  width: 100%;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--warna-tepi);
  border-radius: 6px;
  background-color: #ffffff;
  color: inherit;
  font: inherit;
}

/* Indikator fokus yang terlihat jelas bagi navigasi keyboard. */
.kolom:focus-visible {
  outline: 3px solid var(--warna-aksen);
  outline-offset: 2px;
}

/* Tanda ringan untuk isian yang belum sah — peringatan, bukan hukuman. */
.kolom:invalid {
  border-color: var(--warna-galat-lembut);
}

.pilihan {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0;
  font-weight: normal;
}

.pesan-galat {
  margin: 0;
  color: var(--warna-galat);
  font-size: 0.9rem;
}

button[type="submit"] {
  justify-self: start;
  font: inherit;
  font-weight: 600;
  color: #ffffff;
  background-color: var(--warna-aksen);
  border: 0;
  border-radius: 999px;
  padding: 0.6rem 1.5rem;
  cursor: pointer;
}

button[type="submit"]:focus-visible {
  outline: 3px solid var(--warna-teks);
  outline-offset: 2px;
}

.ringkasan {
  border: 1px solid var(--warna-aksen);
  border-radius: 8px;
  padding: 1rem;
  background-color: var(--warna-kartu);
}

tests/cek-struktur.html

Unduh berkas
html
<!doctype html>
<html lang="id">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Cek Struktur — Proyek Formulir Pendaftaran</title>
  <link rel="stylesheet" href="../styles/style.css">
  <script src="cek-struktur.js" defer></script>
  <style>
    .cek {
      list-style: none;
      padding-left: 0;
    }

    .cek li {
      margin-bottom: 0.5rem;
    }

    .cek input {
      margin-right: 0.5rem;
    }

    #progres {
      font-weight: 700;
    }
  </style>
</head>
<body>
  <main>
    <header>
      <h1>Cek Struktur Proyek Formulir Pendaftaran</h1>
      <p>Halaman ini membantu memeriksa hasil setiap Bagian proyek Seminar Web Komunitas secara mandiri. Centang butir yang terpenuhi; progres tersimpan otomatis di browser dan tidak perlu dicentang ulang.</p>
      <p id="progres">0 dari 16 butir terpenuhi</p>
    </header>

    <fieldset>
      <legend>Struktur form (Bagian 1)</legend>
      <ul class="cek">
        <li><input type="checkbox" id="cek-s1" data-cek="s1"><label for="cek-s1">Form memakai <code>action</code> dan <code>method="post"</code></label></li>
        <li><input type="checkbox" id="cek-s2" data-cek="s2"><label for="cek-s2">Tiga kelompok <code>fieldset</code> masing-masing berjudul lewat <code>legend</code>: Data diri, Kategori peserta, Akun</label></li>
        <li><input type="checkbox" id="cek-s3" data-cek="s3"><label for="cek-s3">Terdapat input <code>text</code>, <code>date</code>, <code>email</code>, <code>password</code>, satu <code>select</code> kota, tiga radio satu grup, satu checkbox, dan tombol submit</label></li>
        <li><input type="checkbox" id="cek-s4" data-cek="s4"><label for="cek-s4">Ketiga radio berbagi <code>name="kategori"</code> dengan <code>value</code> berbeda</label></li>
        <li><input type="checkbox" id="cek-s5" data-cek="s5"><label for="cek-s5">Opsi pertama <code>select</code> bernilai kosong sebagai penanda belum memilih</label></li>
      </ul>
    </fieldset>

    <fieldset>
      <legend>Label dan fieldset (Bagian 1)</legend>
      <ul class="cek">
        <li><input type="checkbox" id="cek-l1" data-cek="l1"><label for="cek-l1">Setiap kontrol punya <code>label</code> dengan pasangan <code>for</code>/<code>id</code> yang cocok</label></li>
        <li><input type="checkbox" id="cek-l2" data-cek="l2"><label for="cek-l2">Mengklik teks label memindahkan fokus ke kolomnya</label></li>
        <li><input type="checkbox" id="cek-l3" data-cek="l3"><label for="cek-l3">Logo <code>assets/logo-acara.svg</code> tampil dengan <code>alt</code> deskriptif</label></li>
      </ul>
    </fieldset>

    <fieldset>
      <legend>Validasi atribut (Bagian 2)</legend>
      <ul class="cek">
        <li><input type="checkbox" id="cek-v1" data-cek="v1"><label for="cek-v1"><code>required</code> terpasang pada semua kolom wajib, termasuk radio pertama dan checkbox persetujuan</label></li>
        <li><input type="checkbox" id="cek-v2" data-cek="v2"><label for="cek-v2"><code>minlength="8"</code> pada kata sandi dan <code>minlength="3"</code> pada nama</label></li>
        <li><input type="checkbox" id="cek-v3" data-cek="v3"><label for="cek-v3"><code>pattern</code> nama menolak angka dan atribut <code>title</code> memberi saran</label></li>
        <li><input type="checkbox" id="cek-v4" data-cek="v4"><label for="cek-v4"><code>min</code> dan <code>max</code> pada tanggal lahir menolak isian di luar rentang</label></li>
        <li><input type="checkbox" id="cek-v5" data-cek="v5"><label for="cek-v5">Token <code>autocomplete</code> terisi: name, bday, address-level2, email, new-password</label></li>
      </ul>
    </fieldset>

    <fieldset>
      <legend>Umpan balik JavaScript (Bagian 3-4)</legend>
      <ul class="cek">
        <li><input type="checkbox" id="cek-j1" data-cek="j1"><label for="cek-j1">Stylesheet <code>../styles/style.css</code> terhubung; cincin fokus <code>:focus-visible</code> terlihat saat menekan Tab</label></li>
        <li><input type="checkbox" id="cek-j2" data-cek="j2"><label for="cek-j2">Form memakai <code>novalidate</code> dan tombol kirim tidak lagi menampilkan gelembung browser</label></li>
        <li><input type="checkbox" id="cek-j3" data-cek="j3"><label for="cek-j3">Kirim formulir kosong: pesan kustom tampil di bawah setiap kolom bermasalah</label></li>
        <li><input type="checkbox" id="cek-j4" data-cek="j4"><label for="cek-j4">Kontrol tidak sah memperoleh <code>aria-invalid="true"</code> di panel inspector</label></li>
        <li><input type="checkbox" id="cek-j5" data-cek="j5"><label for="cek-j5">Isian lengkap dan sah menampilkan kartu ringkasan keberhasilan</label></li>
      </ul>
    </fieldset>

    <p><img src="../assets/logo-acara.svg" alt="" width="72" height="72"></p>
    <p>Halaman pemeriksaan ini juga memakai stylesheet proyek dan gambar aset melalui path relatif <code>../</code>, sekaligus menjadi contoh rujukan lintas folder.</p>
  </main>
</body>
</html>

tests/cek-struktur.js

Unduh berkas
javascript
// cek-struktur.js — checklist mandiri proyek dengan progres tersimpan.
// Digunakan bersama cek-struktur.html pada folder tests/ proyek.

const KUNCI_SIMPAN = "cek:formulir-pendaftaran";

function bacaTersimpan() {
  try {
    return JSON.parse(localStorage.getItem(KUNCI_SIMPAN)) || {};
  } catch (galat) {
    return {};
  }
}

function tulisTersimpan(nilai) {
  try {
    localStorage.setItem(KUNCI_SIMPAN, JSON.stringify(nilai));
  } catch (galat) {
    // Penyimpanan tidak tersedia; checklist tetap berfungsi tanpa persist.
  }
}

function perbaruiProgres() {
  const kotak = Array.from(document.querySelectorAll("[data-cek]"));
  const terpenuhi = kotak.filter(function (item) {
    return item.checked;
  }).length;
  const progres = document.querySelector("#progres");
  if (progres) {
    progres.textContent = terpenuhi + " dari " + kotak.length + " butir terpenuhi";
  }
}

function pasang() {
  const tersimpan = bacaTersimpan();
  const kotak = Array.from(document.querySelectorAll("[data-cek]"));
  kotak.forEach(function (item) {
    item.checked = tersimpan[item.dataset.cek] === true;
    item.addEventListener("change", function () {
      const nilai = bacaTersimpan();
      nilai[item.dataset.cek] = item.checked;
      tulisTersimpan(nilai);
      perbaruiProgres();
    });
  });
  perbaruiProgres();
}

if (document.readyState === "loading") {
  document.addEventListener("DOMContentLoaded", pasang);
} else {
  pasang();
}