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, danborder-radius— denganbox-sizing: border-box. - Membentuk ritme spasi antarbagian memakai
margin,gap, dan kelipatan 8px. - Menjadikan halaman adaptif dengan
clamp(),min(), dan satu breakpointmin-widthbergaya 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:
- Typography: Font Family , Font Size dan Weight , serta Line Height .
- Warna: Color Syntax dan Background Color .
- Box model: Content, Padding, Border, dan Margin serta Box Sizing .
- Responsive: Media Queries dan Mobile First .
- Pendukung: Custom Properties untuk token warna.
Struktur Folder Proyek #
Seluruh berkas proyek tinggal di satu folder. Struktur berikut adalah kondisi akhir setelah ketiga bagian selesai:
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 #
| Bagian | Fokus |
|---|---|
| Bagian 1: Typography dan Warna | Token warna, skala tipografi, dan pewarisan dari body |
| Bagian 2: Box Model dan Spacing | Kartu 300px, padding, border, dan ritme spasi 8/16/24px |
| Bagian 3: Responsive Layout | clamp(), 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:
node scripts/css-test/run_tests.mjs --filter halaman-profilSpesifikasi 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.
Bangun fondasi visual halaman profil dengan token warna, skala tipografi, dan pewarisan dari body yang konsisten.
Buka → # Bagian 2: Box Model dan SpacingBungkus profil dalam kartu 300px dengan box model terukur: box-sizing, padding, border, dan ritme spasi 8/16/24px.
Buka → # Bagian 3: Responsive LayoutJadikan halaman profil adaptif dengan clamp() untuk judul fluid, min() untuk kartu, dan satu breakpoint min-width 640px.
Buka →Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
styles/profil.css
Unduh berkas/* =========================================================
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// ============================================================
// 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// ============================================================
// 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// ============================================================
// 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" },
];