Materi tentang typography, warna, box model, dan responsive design baru terasa utuh ketika keempatnya dipakai bersama pada satu halaman nyata. Pada proyek ini kita membangun halaman profil pribadi untuk tokoh fiktif bernama Budi Santoso: sebuah kartu yang berisi avatar, nama, peran, bio singkat, daftar keahlian, dan tautan kontak. Proyek dipecah menjadi tiga bagian yang berurutan, dan setiap bagian memperluas satu file CSS yang sama, styles/profil.css, sampai halaman tampil rapi pada layar kecil maupun desktop. Setiap bagian ditutup dengan halaman uji otomatis yang memverifikasi nilai computed CSS dari klaim yang kita tulis.

Tujuan Proyek #

Setelah menyelesaikan ketiga bagian, kita memiliki satu halaman lengkap sekaligus kebiasaan kerja yang sehat. Target teknisnya:

  • Menyatukan token warna dan skala tipografi menjadi fondasi visual yang konsisten di seluruh halaman.
  • Mengatur box model kartu profil — width, padding, border, dan border-radius — dengan box-sizing: border-box.
  • Membentuk ritme spasi antarbagian memakai margin, gap, dan kelipatan 8px.
  • Menjadikan halaman adaptif dengan clamp(), min(), dan satu breakpoint min-width bergaya mobile-first.

Halaman ini sengaja dibuat kecil cakupannya agar fokus kita tertuju pada kualitas keputusan, bukan kuantitas komponen. Satu kartu yang terukur lebih berharga daripada sepuluh kartu yang asal jadi.

Prasyarat #

Proyek ini memakai konsep dari beberapa Section sebelumnya. Kita tidak mengulang teorinya; setiap bagian menautkan chapter yang relevan saat konsep tersebut dipakai. Pastikan kita sudah nyaman dengan:

Struktur Folder Proyek #

Seluruh berkas proyek tinggal di satu folder. Struktur berikut adalah kondisi akhir setelah ketiga bagian selesai:

text
halaman-profil/
├── index.html              markup halaman yang dikembangkan tiap bagian
├── styles/
│   └── profil.css          satu file CSS utuh yang berkembang
├── assets/
│   └── avatar-budi.svg     gambar avatar yang dipakai markup
├── tests/
│   ├── typography-dan-warna.spec.js
│   ├── box-model-dan-spacing.spec.js
│   └── responsive-layout.spec.js
└── bagian/                 halaman pembahasan tiap tahap
    ├── typography-dan-warna/
    ├── box-model-dan-spacing/
    └── responsive-layout/

Ada satu keputusan penting di struktur ini: styles/profil.css adalah satu file yang di-update, bukan diganti, di setiap bagian. Bagian 2 menambah aturan kartu di bawah aturan typography Bagian 1, dan Bagian 3 menambah media query di ujung file. Komentar penanda Bagian N di dalam file membantu kita menelusuri asal setiap kelompok aturan.

Daftar Bagian #

BagianFokus
Bagian 1: Typography dan WarnaToken warna, skala tipografi, dan pewarisan dari body
Bagian 2: Box Model dan SpacingKartu 300px, padding, border, dan ritme spasi 8/16/24px
Bagian 3: Responsive Layoutclamp(), min(), dan breakpoint 640px mobile-first

Menjalankan Halaman Uji Otomatis #

Setiap bagian punya halaman uji mandiri di bawah /tests/css/proyek/halaman-profil/<topic>.html, misalnya uji Bagian 1 . Di dalamnya terdapat elemen probe yang membawa atribut data-expected berisi JSON pasangan properti dan nilai computed yang diharapkan. Harness uji Course membuka halaman tersebut di headless Chromium, membaca getComputedStyle() setiap probe, lalu membandingkannya dengan nilai yang kita janjikan.

Untuk menjalankan seluruh uji proyek ini sekaligus:

bash
node scripts/css-test/run_tests.mjs --filter halaman-profil

Spesifikasi asersi per bagian juga tersedia sebagai berkas tests/<topic>.spec.js yang dapat dibaca dan diunduh dari halaman pembahasannya. Kita dapat pula memeriksa manual: buka halaman uji di browser, tekan F12, lalu bandingkan panel Computed dengan nilai pada tabel asersi di akhir setiap bagian.

Berkas Sumber

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

styles/profil.css

Unduh berkas
css
/* =========================================================
   Proyek: Halaman Profil — styles/profil.css
   File utuh proyek yang berkembang mengikuti tiga bagian:
   Bagian 1 (Typography dan Warna), Bagian 2 (Box Model dan
   Spacing), Bagian 3 (Responsive Layout). Komentar "Bagian N"
   menandai kelompok aturan yang dibahas pada setiap bagian,
   dan komentar kecil mencatat perubahan antarbagian.
   ========================================================= */

/* Bagian 1: Typography dan Warna -------------------------- */

:root {
  --warna-teks: #0f172a;
  --warna-teks-lembut: #475569;
  --warna-aksen: #0284c7;
  --warna-latar: #f1f5f9;
  --warna-kartu: #ffffff;
  --warna-garis: #e2e8f0;
}

* {
  margin: 0;
  box-sizing: border-box; /* ditambahkan pada Bagian 2 */
}

body {
  font-family: system-ui, sans-serif;
  font-size: 16px;
  line-height: 1.5;
  color: var(--warna-teks);
  background-color: var(--warna-latar);
}

.halaman {
  max-width: 760px;
  margin: 0 auto;
  padding: 32px 16px; /* Bagian 3: padding dasar diturunkan dari 48px 16px */
}

.profil-nama {
  /* Bagian 3: nilai tetap 36px diganti clamp() agar fluid */
  font-size: clamp(30px, 24px + 1vw, 40px);
  font-weight: 700;
  line-height: 1.2;
  color: var(--warna-teks);
}

.profil-peran {
  font-size: 18px;
  font-weight: 500;
  color: var(--warna-aksen);
}

.profil-bio {
  color: var(--warna-teks-lembut);
}

.profil-bagian-judul {
  font-size: 20px;
  font-weight: 600;
  letter-spacing: 0.5px;
  color: var(--warna-teks);
}

.tautan-kontak {
  color: var(--warna-aksen);
  font-weight: 500;
  text-decoration: underline;
}

.tautan-kontak:hover {
  text-decoration-thickness: 2px;
}

/* Bagian 2: Box Model dan Spacing ------------------------- */

.kartu-profil {
  width: min(100%, 300px); /* Bagian 3: mulanya 300px tetap */
  margin: 0 auto;
  padding: 24px;
  background-color: var(--warna-kartu);
  border: 1px solid var(--warna-garis);
  border-radius: 12px;
  box-shadow: 0 4px 12px rgb(15 23 42 / 8%);
}

.profil-kepala {
  display: flex;
  flex-direction: column; /* Bagian 3: kembali ke baris pada breakpoint */
  align-items: center;
  gap: 16px;
  margin-bottom: 16px;
  text-align: center;
}

.avatar {
  flex: none; /* jaga ukuran: avatar tidak ikut menyusut */
  width: 96px;
  height: 96px;
  border-radius: 50%;
  border: 2px solid var(--warna-garis);
}

.profil-bagian {
  margin-top: 24px;
}

.daftar-keahlian {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  list-style: none;
}

.keahlian {
  padding: 8px 12px;
  background-color: var(--warna-latar);
  border-radius: 8px;
  font-size: 14px;
  color: var(--warna-teks-lembut);
}

/* Bagian 3: Responsive Layout ----------------------------- */

@media (min-width: 640px) {
  .halaman {
    padding: 64px 32px;
  }

  .profil-kepala {
    flex-direction: row;
    text-align: left;
    margin-bottom: 24px;
  }
}

tests/box-model-dan-spacing.spec.js

Unduh berkas
javascript
// ============================================================
// Spesifikasi uji — Proyek: Halaman Profil
// Bagian 2: Box Model dan Spacing
// ============================================================
// Berkas ini merangkum asersi yang diverifikasi oleh harness
// pada halaman uji:
//   /tests/css/proyek/halaman-profil/box-model-dan-spacing.html
// Setiap entri memasangkan elemen probe dengan properti CSS
// dan nilai computed yang diharapkan.

const asersi = [
  // Kartu profil selebar 300px berkat box-sizing: border-box,
  // padding dan border tidak menambah lebar totalnya.
  { elemen: ".kartu-profil", properti: "width", diharapkan: "300px" },

  // Padding 24px di semua sisi memberi ruang lega bagi konten.
  { elemen: ".kartu-profil", properti: "padding", diharapkan: "24px" },

  // Border tipis 1px dengan warna token --warna-garis (#e2e8f0).
  { elemen: ".kartu-profil", properti: "border-top-width", diharapkan: "1px" },
  { elemen: ".kartu-profil", properti: "border-top-color", diharapkan: "rgb(226, 232, 240)" },

  // Sudut kartu membulat 12px, latar kartu putih.
  { elemen: ".kartu-profil", properti: "border-radius", diharapkan: "12px" },
  { elemen: ".kartu-profil", properti: "background-color", diharapkan: "rgb(255, 255, 255)" },

  // .profil-kepala adalah flex container dengan jarak antaritem 16px.
  { elemen: ".profil-kepala", properti: "display", diharapkan: "flex" },
  { elemen: ".profil-kepala", properti: "gap", diharapkan: "16px" },

  // Ritme vertikal: kepala kartu diberi margin bawah 16px.
  { elemen: ".profil-kepala", properti: "margin-bottom", diharapkan: "16px" },

  // Avatar berukuran 96px tetap (flex: none agar tidak menyusut
  // di dalam flex container) dengan sudut penuh (lingkaran).
  { elemen: ".avatar", properti: "width", diharapkan: "96px" },
  { elemen: ".avatar", properti: "height", diharapkan: "96px" },
  { elemen: ".avatar", properti: "border-radius", diharapkan: "50%" },

  // Setiap subbagian kartu berjarak 24px dari blok di atasnya.
  { elemen: ".profil-bagian", properti: "margin-top", diharapkan: "24px" },

  // Daftar keahlian adalah flex yang meliputi baris dengan gap 8px.
  { elemen: ".daftar-keahlian", properti: "display", diharapkan: "flex" },
  { elemen: ".daftar-keahlian", properti: "flex-wrap", diharapkan: "wrap" },
  { elemen: ".daftar-keahlian", properti: "gap", diharapkan: "8px" },

  // Chip keahlian berpadding 8px vertikal dan 12px horizontal.
  { elemen: ".keahlian", properti: "padding-top", diharapkan: "8px" },
  { elemen: ".keahlian", properti: "padding-left", diharapkan: "12px" },

  // Chip membulat 8px dengan teks 14px.
  { elemen: ".keahlian", properti: "border-radius", diharapkan: "8px" },
  { elemen: ".keahlian", properti: "font-size", diharapkan: "14px" },
];

tests/responsive-layout.spec.js

Unduh berkas
javascript
// ============================================================
// Spesifikasi uji — Proyek: Halaman Profil
// Bagian 3: Responsive Layout
// ============================================================
// Berkas ini merangkum asersi yang diverifikasi oleh harness
// pada halaman uji (viewport 1280x720, sehingga breakpoint
// min-width: 640px berada pada keadaan aktif):
//   /tests/css/proyek/halaman-profil/responsive-layout.html
// Setiap entri memasangkan elemen probe dengan properti CSS
// dan nilai computed yang diharapkan.

const asersi = [
  // Wadah halaman dibatasi 760px dan, pada layar >= 640px,
  // berpadding 64px vertikal serta 32px horizontal.
  { elemen: ".halaman", properti: "max-width", diharapkan: "760px" },
  { elemen: ".halaman", properti: "padding-top", diharapkan: "64px" },

  // Kartu tetap 300px dan terpusat: margin auto terhitung browser
  // menjadi 198px pada konten selebar 696px (760 - 2 x 32).
  { elemen: ".kartu-profil", properti: "width", diharapkan: "300px" },
  { elemen: ".kartu-profil", properti: "margin-left", diharapkan: "198px" },

  // Judul fluid: clamp(30px, 24px + 1vw, 40px) pada viewport 1280px
  // menghasilkan 24 + 12.8 = 36.8px, masih di dalam rentang.
  { elemen: ".profil-nama", properti: "font-size", diharapkan: "36.8px" },

  // Pada layar >= 640px kepala profil kembali tersusun horizontal.
  { elemen: ".profil-kepala", properti: "flex-direction", diharapkan: "row" },
  { elemen: ".profil-kepala", properti: "gap", diharapkan: "16px" },

  // Jarak kepala ke bio diperbesar menjadi 24px pada breakpoint.
  { elemen: ".profil-kepala", properti: "margin-bottom", diharapkan: "24px" },

  // Daftar keahlian tetap berjajar dengan gap 8px.
  { elemen: ".daftar-keahlian", properti: "flex-direction", diharapkan: "row" },
  { elemen: ".daftar-keahlian", properti: "gap", diharapkan: "8px" },
];

tests/typography-dan-warna.spec.js

Unduh berkas
javascript
// ============================================================
// Spesifikasi uji — Proyek: Halaman Profil
// Bagian 1: Typography dan Warna
// ============================================================
// Berkas ini merangkum asersi yang diverifikasi oleh harness
// pada halaman uji:
//   /tests/css/proyek/halaman-profil/typography-dan-warna.html
// Setiap entri memasangkan elemen probe dengan properti CSS
// dan nilai computed yang diharapkan.

const asersi = [
  // body membawa warna teks utama dari token --warna-teks (#0f172a
  // dikonversi browser menjadi rgb(15, 23, 42)).
  { elemen: "body", properti: "color", diharapkan: "rgb(15, 23, 42)" },

  // Latar halaman memakai token --warna-latar (#f1f5f9).
  { elemen: "body", properti: "background-color", diharapkan: "rgb(241, 245, 249)" },

  // Ukuran teks dasar 16px dengan line-height 1.5 (16 x 1.5 = 24px).
  { elemen: "body", properti: "font-size", diharapkan: "16px" },
  { elemen: "body", properti: "line-height", diharapkan: "24px" },

  // .profil-nama berada di puncak skala tipografi: 36px, tebal 700.
  { elemen: ".profil-nama", properti: "font-size", diharapkan: "36px" },
  { elemen: ".profil-nama", properti: "font-weight", diharapkan: "700" },

  // line-height 1.2 pada font 36px dihitung browser menjadi 43.2px.
  { elemen: ".profil-nama", properti: "line-height", diharapkan: "43.2px" },

  // .profil-peran memakai warna aksen (#0284c7) dan bobot medium.
  { elemen: ".profil-peran", properti: "color", diharapkan: "rgb(2, 132, 199)" },
  { elemen: ".profil-peran", properti: "font-size", diharapkan: "18px" },
  { elemen: ".profil-peran", properti: "font-weight", diharapkan: "500" },

  // .profil-bio memakai warna teks lembut (#475569).
  { elemen: ".profil-bio", properti: "color", diharapkan: "rgb(71, 85, 105)" },

  // Judul bagian 20px, bobot 600, dengan letter-spacing 0.5px.
  { elemen: ".profil-bagian-judul", properti: "font-size", diharapkan: "20px" },
  { elemen: ".profil-bagian-judul", properti: "font-weight", diharapkan: "600" },
  { elemen: ".profil-bagian-judul", properti: "letter-spacing", diharapkan: "0.5px" },

  // Tautan kontak berwarna aksen dan digarisbawahi.
  { elemen: ".tautan-kontak", properti: "color", diharapkan: "rgb(2, 132, 199)" },
  { elemen: ".tautan-kontak", properti: "text-decoration-line", diharapkan: "underline" },
];