Semakin besar sebuah stylesheet, semakin mahal harganya bila nilai warna dan jarak ditulis lepas-lapas. Satu biru yang sedikit berbeda di tiga tempat sudah cukup membuat halaman terlihat tidak terawat. Bagian pertama proyek portofolio ini karena itu tidak langsung menyusun layout, melainkan menanam fondasi: design tokens. Kita mendefinisikan warna semantik, skala spasi, radius, dan ukuran teks sebagai custom properties di :root, lalu membangun komponen pertama — tombol dan badge — sepenuhnya dari token tersebut.
Tujuan Bagian dan Prasyarat #
Di akhir bagian ini kita memiliki file styles/portofolio.css berisi blok token lengkap dan dua komponen kecil yang membuktikan token bekerja:
- Tujuh token warna semantik: primer, primer tua, latar, permukaan, teks, teks lembut, dan teks balik.
- Skala spasi tujuh tingkat (4px sampai 64px) dan dua token radius (10px dan 999px).
- Komponen
.tomboldengan varian.tombol-primerdan.tombol-kedua, serta.badge.
Teori di balik pola ini dibahas pada chapter Design Tokens dan Custom Properties ; penulisan nilai warna diulas pada Color Syntax . Kita tidak mengulangnya di sini, hanya memakainya dalam bentuk paling praktis.
Menyiapkan File dan Blok Token #
Buat folder proyek website-portofolio/ dengan index.html, lalu subfolder styles/ dan assets/. Tulis blok token berikut sebagai bagian pertama styles/portofolio.css. Penamaan memakai pola --warna-*, --spasi-*, dan --radius-* agar maksud setiap token terbaca tanpa membuka dokumentasi.
:root {
/* Warna semantik */
--warna-primer: #2563eb;
--warna-primer-tua: #1e40af;
--warna-latar: #f8fafc;
--warna-permukaan: #ffffff;
--warna-teks: #0f172a;
--warna-teks-lembut: #475569;
--warna-teks-balik: #ffffff;
--warna-garis: #e2e8f0;
/* Skala spasi */
--spasi-1: 4px;
--spasi-2: 8px;
--spasi-3: 12px;
--spasi-4: 16px;
--spasi-6: 24px;
--spasi-8: 32px;
--spasi-16: 64px;
/* Bentuk */
--radius-sedang: 10px;
--radius-penuh: 999px;
/* Tipografi */
--font-utama: system-ui, sans-serif;
--teks-dasar: 16px;
--teks-lg: 20px;
}
* {
margin: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-utama);
font-size: var(--teks-dasar);
line-height: 1.6;
color: var(--warna-teks);
background-color: var(--warna-latar);
}
.tombol {
display: inline-block;
padding: var(--spasi-3) var(--spasi-4);
border-radius: var(--radius-sedang);
font-size: var(--teks-dasar);
font-weight: 500;
text-decoration: none;
}
.tombol-primer {
background-color: var(--warna-primer);
color: var(--warna-teks-balik);
}
.tombol-kedua {
background-color: var(--warna-permukaan);
color: var(--warna-primer);
border: 1px solid var(--warna-primer);
}
.badge {
display: inline-block;
padding: var(--spasi-1) var(--spasi-2);
margin-bottom: var(--spasi-4);
border-radius: var(--radius-penuh);
background-color: rgb(37 99 235 / 12%);
color: var(--warna-primer-tua);
font-size: 14px;
font-weight: 500;
}Markup pembuktinya hanya dua tautan dan satu badge; bagian berikutnya akan menempatkannya di hero.
<div class="wadah">
<span class="badge">Terbuka untuk proyek</span>
<p>
<a class="tombol tombol-primer" href="#proyek">Lihat Proyek</a>
<a class="tombol tombol-kedua" href="mailto:[email protected]">Hubungi Saya</a>
</p>
</div>Hasil di Browser:
Badge tampil berlatar rgba(37, 99, 235, 0.12) — primer dengan
alpha 12% — berbentuk pil (radius 999px) dan teks biru tua.
Tombol primer berlatar rgb(37, 99, 235) dengan teks putih,
padding 12px 16px, dan sudut membulat 10px. Tombol kedua putih
dengan teks dan border rgb(37, 99, 235).Bedah Token dan Komponen Pertama #
- Blok token warna: nama token menyatakan peran (
--warna-primer), bukan warnanya (--biru-500). Saat tema berubah, nilai yang diganti — bukan seluruh komponen. Token--warna-teks-balikmenunjuk warna teks di atas latar primer, sengaja terpisah dari--warna-teks. - Skala spasi: angka pada nama token adalah kelipatan 4px (
--spasi-6= 24px). Kelompok nilai 4/8/12/16/24/32/64 memberi ritme konsisten dan mencegah jarak asal-asalan seperti 13px atau 21px. - Token bentuk dan tipografi:
--radius-penuh: 999pxadalah trik klasik pil — radius melebihi setengah tinggi elemen otomatis dibatasi browser menjadi bentuk pil sempurna. - Aturan
.tombol: paddingvar(--spasi-3) var(--spasi-4)dibaca “12px vertikal, 16px horizontal”. Varian.tombol-primerdan.tombol-keduahanya menambah warna; geometri tetap milik kelas dasar. - Aturan
.badge:rgb(37 99 235 / 12%)memakai notasi warna modern dengan alpha — hasilnya latar biru transparan 12% yang tetap terbaca di atas latar terang.
Perhatikan bahwa tidak satu pun komponen menulis nilai piksel sendiri. Itulah ciri stylesheet berbasis token: nilai hidup di :root, komponen hanya merujuk.
Uji Bagian Ini #
Halaman uji bagian ini tersedia di /tests/css/proyek/website-portofolio/design-tokens.html
, dengan spesifikasi lengkap pada tests/design-tokens.spec.js. Halaman uji juga membuktikan bahwa token dapat dibaca langsung dari computed style elemen html:
| Elemen | Properti | Diharapkan |
|---|---|---|
html | --warna-primer | #2563eb |
html | --spasi-6 | 24px |
html | --radius-sedang | 10px |
body | background-color | rgb(248, 250, 252) |
.tombol-primer | background-color | rgb(37, 99, 235) |
.tombol-primer | padding-left | 16px |
.tombol-kedua | border-top-color | rgb(37, 99, 235) |
.badge | background-color | rgba(37, 99, 235, 0.12) |
.badge | border-radius | 999px |
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
styles/portofolio.css
Unduh berkas/* =========================================================
Proyek: Website Portofolio — styles/portofolio.css
File utuh proyek yang berkembang mengikuti empat bagian:
Bagian 1 (Design Tokens), Bagian 2 (Navigation dan Hero),
Bagian 3 (Project Grid), Bagian 4 (Responsive Design).
Komentar "Bagian N" menandai kelompok aturan yang dibahas
pada setiap bagian.
========================================================= */
/* Bagian 1: Design Tokens --------------------------------- */
:root {
/* Warna semantik */
--warna-primer: #2563eb;
--warna-primer-tua: #1e40af;
--warna-latar: #f8fafc;
--warna-permukaan: #ffffff;
--warna-teks: #0f172a;
--warna-teks-lembut: #475569;
--warna-teks-balik: #ffffff;
--warna-garis: #e2e8f0;
/* Skala spasi */
--spasi-1: 4px;
--spasi-2: 8px;
--spasi-3: 12px;
--spasi-4: 16px;
--spasi-6: 24px;
--spasi-8: 32px;
--spasi-16: 64px;
/* Bentuk */
--radius-sedang: 10px;
--radius-penuh: 999px;
/* Tipografi */
--font-utama: system-ui, sans-serif;
--teks-dasar: 16px;
--teks-lg: 20px;
}
* {
margin: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-utama);
font-size: var(--teks-dasar);
line-height: 1.6;
color: var(--warna-teks);
background-color: var(--warna-latar);
}
.tombol {
display: inline-block;
padding: var(--spasi-3) var(--spasi-4);
border-radius: var(--radius-sedang);
font-size: var(--teks-dasar);
font-weight: 500;
text-decoration: none;
}
.tombol-primer {
background-color: var(--warna-primer);
color: var(--warna-teks-balik);
}
.tombol-kedua {
background-color: var(--warna-permukaan);
color: var(--warna-primer);
border: 1px solid var(--warna-primer);
}
.badge {
display: inline-block;
padding: var(--spasi-1) var(--spasi-2);
margin-bottom: var(--spasi-4);
border-radius: var(--radius-penuh);
background-color: rgb(37 99 235 / 12%);
color: var(--warna-primer-tua);
font-size: 14px;
font-weight: 500;
}
/* Bagian 2: Navigation dan Hero --------------------------- */
.navigasi {
position: sticky;
top: 0;
z-index: 10;
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spasi-4);
padding: var(--spasi-4) var(--spasi-6);
background-color: rgb(255 255 255 / 85%);
backdrop-filter: blur(8px);
border-bottom: 1px solid var(--warna-garis);
}
.logo {
font-size: var(--teks-lg);
font-weight: 700;
color: var(--warna-teks);
text-decoration: none;
}
.navigasi-menu {
display: flex;
gap: var(--spasi-6);
list-style: none;
}
.navigasi-tautan {
color: var(--warna-teks-lembut);
font-weight: 500;
text-decoration: none;
}
.navigasi-tautan:hover {
color: var(--warna-primer);
}
.wadah {
max-width: 1120px;
margin: 0 auto;
padding: 0 var(--spasi-6); /* Bagian 4: 16px pada layar <= 767px */
}
.bagian-hero {
display: grid;
grid-template-columns: 1.1fr 0.9fr; /* Bagian 4: satu kolom pada layar <= 1023px */
gap: var(--spasi-8);
align-items: center;
padding: var(--spasi-16) 0;
}
.judul-hero {
font-size: clamp(30px, 28px + 1.25vw, 44px);
line-height: 1.15;
color: var(--warna-teks);
}
.teks-aksen {
color: var(--warna-primer);
}
.deskripsi-hero {
margin-top: var(--spasi-4);
font-size: 18px;
line-height: 1.6;
color: var(--warna-teks-lembut);
}
.hero-aksi {
display: flex;
gap: var(--spasi-4);
margin-top: var(--spasi-6);
}
.foto-hero {
display: block;
width: 100%;
height: auto;
border-radius: 16px;
border: 1px solid var(--warna-garis);
}
/* Bagian 3: Project Grid ---------------------------------- */
.judul-bagian {
font-size: 28px;
font-weight: 700;
}
.deskripsi-bagian {
margin-top: var(--spasi-2);
max-width: 560px;
color: var(--warna-teks-lembut);
}
.grid-proyek {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); /* Bagian 4: minmax(240px, 1fr) pada layar <= 767px */
gap: var(--spasi-6);
margin-top: var(--spasi-8);
}
.kartu-proyek {
display: flex;
flex-direction: column;
background-color: var(--warna-permukaan);
border: 1px solid var(--warna-garis);
border-radius: var(--radius-sedang);
overflow: hidden;
}
.kartu-proyek-img {
display: block;
width: 100%;
aspect-ratio: 16 / 10;
object-fit: cover;
}
.kartu-proyek-isi {
display: grid;
gap: var(--spasi-2);
padding: var(--spasi-6);
}
.kartu-proyek-judul {
font-size: var(--teks-lg);
font-weight: 600;
}
.kartu-proyek-deskripsi {
font-size: 14px;
color: var(--warna-teks-lembut);
}
.tautan-kartu {
color: var(--warna-primer);
font-weight: 500;
text-decoration: none;
}
/* Bagian 4: Responsive Design ----------------------------- */
@media (max-width: 767px) {
.wadah {
padding: 0 var(--spasi-4);
}
.navigasi-menu {
flex-direction: column;
gap: var(--spasi-2);
}
.grid-proyek {
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}
}
@media (max-width: 1023px) {
.bagian-hero {
grid-template-columns: 1fr;
}
}
tests/design-tokens.spec.js
Unduh berkas// ============================================================
// Spesifikasi uji — Proyek: Website Portofolio
// Bagian 1: Design Tokens
// ============================================================
// Berkas ini merangkum asersi yang diverifikasi oleh harness
// pada halaman uji:
// /tests/css/proyek/website-portofolio/design-tokens.html
// Setiap entri memasangkan elemen probe dengan properti CSS
// dan nilai computed yang diharapkan.
const asersi = [
// Token tersimpan di :root (elemen html) dan terbaca apa adanya.
{ elemen: "html", properti: "--warna-primer", diharapkan: "#2563eb" },
{ elemen: "html", properti: "--spasi-6", diharapkan: "24px" },
{ elemen: "html", properti: "--radius-sedang", diharapkan: "10px" },
// body mengonsumsi token: latar slate-50 dan teks slate-900.
{ elemen: "body", properti: "background-color", diharapkan: "rgb(248, 250, 252)" },
{ elemen: "body", properti: "color", diharapkan: "rgb(15, 23, 42)" },
{ elemen: "body", properti: "font-size", diharapkan: "16px" },
// .tombol-primer: padding dari token spasi, radius dari token bentuk.
{ elemen: ".tombol-primer", properti: "background-color", diharapkan: "rgb(37, 99, 235)" },
{ elemen: ".tombol-primer", properti: "color", diharapkan: "rgb(255, 255, 255)" },
{ elemen: ".tombol-primer", properti: "padding-top", diharapkan: "12px" },
{ elemen: ".tombol-primer", properti: "padding-left", diharapkan: "16px" },
{ elemen: ".tombol-primer", properti: "border-radius", diharapkan: "10px" },
// .tombol-kedua: permukaan putih dengan teks dan border primer.
{ elemen: ".tombol-kedua", properti: "background-color", diharapkan: "rgb(255, 255, 255)" },
{ elemen: ".tombol-kedua", properti: "color", diharapkan: "rgb(37, 99, 235)" },
{ elemen: ".tombol-kedua", properti: "border-top-color", diharapkan: "rgb(37, 99, 235)" },
// .badge memakai warna primer dengan alpha 12% dan radius penuh.
{ elemen: ".badge", properti: "background-color", diharapkan: "rgba(37, 99, 235, 0.12)" },
{ elemen: ".badge", properti: "color", diharapkan: "rgb(30, 64, 175)" },
{ elemen: ".badge", properti: "border-radius", diharapkan: "999px" },
{ elemen: ".badge", properti: "font-size", diharapkan: "14px" },
];
tests/navigation-dan-hero.spec.js
Unduh berkas// ============================================================
// Spesifikasi uji — Proyek: Website Portofolio
// Bagian 2: Navigation dan Hero
// ============================================================
// Berkas ini merangkum asersi yang diverifikasi oleh harness
// pada halaman uji (viewport 1280x720):
// /tests/css/proyek/website-portofolio/navigation-dan-hero.html
// Setiap entri memasangkan elemen probe dengan properti CSS
// dan nilai computed yang diharapkan.
const asersi = [
// Navigation menempel di atas: sticky dengan jarak 0 dari tepi.
{ elemen: ".navigasi", properti: "position", diharapkan: "sticky" },
{ elemen: ".navigasi", properti: "top", diharapkan: "0px" },
{ elemen: ".navigasi", properti: "z-index", diharapkan: "10" },
// Latar navigasi putih transparan 85% dengan efek blur di belakangnya.
{ elemen: ".navigasi", properti: "background-color", diharapkan: "rgba(255, 255, 255, 0.85)" },
{ elemen: ".navigasi", properti: "backdrop-filter", diharapkan: "blur(8px)" },
// Menu navigasi tersusun horizontal dengan jarak antartautan 24px.
{ elemen: ".navigasi-menu", properti: "display", diharapkan: "flex" },
{ elemen: ".navigasi-menu", properti: "flex-direction", diharapkan: "row" },
{ elemen: ".navigasi-menu", properti: "gap", diharapkan: "24px" },
// Judul hero fluid: clamp mencapai batas atas 44px pada viewport 1280px.
{ elemen: ".judul-hero", properti: "font-size", diharapkan: "44px" },
{ elemen: ".judul-hero", properti: "line-height", diharapkan: "50.6px" },
// Deskripsi hero 18px dengan warna teks lembut.
{ elemen: ".deskripsi-hero", properti: "font-size", diharapkan: "18px" },
{ elemen: ".deskripsi-hero", properti: "color", diharapkan: "rgb(71, 85, 105)" },
// Baris tombol aksi adalah flex dengan gap 16px.
{ elemen: ".hero-aksi", properti: "display", diharapkan: "flex" },
{ elemen: ".hero-aksi", properti: "gap", diharapkan: "16px" },
// Wadah konten dibatasi 1120px dan terpusat di viewport 1280px.
{ elemen: ".wadah", properti: "max-width", diharapkan: "1120px" },
{ elemen: ".wadah", properti: "margin-left", diharapkan: "80px" },
];
tests/project-grid.spec.js
Unduh berkas// ============================================================
// Spesifikasi uji — Proyek: Website Portofolio
// Bagian 3: Project Grid
// ============================================================
// Berkas ini merangkum asersi yang diverifikasi oleh harness
// pada halaman uji (viewport 1280x720, wadah 1120px dengan
// padding 24px sehingga konten selebar 1072px):
// /tests/css/proyek/website-portofolio/project-grid.html
// Setiap entri memasangkan elemen probe dengan properti CSS
// dan nilai computed yang diharapkan.
const asersi = [
// Grid proyek memakai auto-fill minmax(280px, 1fr): pada konten
// 1072px muat 3 track berukuran sekitar 341px dengan gap 24px.
// Nilai di bawah diambil dari serialisasi computed browser.
{ elemen: ".grid-proyek", properti: "display", diharapkan: "grid" },
{ elemen: ".grid-proyek", properti: "gap", diharapkan: "24px" },
{
elemen: ".grid-proyek",
properti: "grid-template-columns",
diharapkan: "341.328px 341.328px 341.344px",
},
// Kartu proyek: permukaan putih, garis 1px, radius 10px.
{ elemen: ".kartu-proyek", properti: "background-color", diharapkan: "rgb(255, 255, 255)" },
{ elemen: ".kartu-proyek", properti: "border-top-width", diharapkan: "1px" },
{ elemen: ".kartu-proyek", properti: "border-radius", diharapkan: "10px" },
// Isi kartu adalah grid kecil dengan gap 8px dan padding 24px.
{ elemen: ".kartu-proyek-isi", properti: "display", diharapkan: "grid" },
{ elemen: ".kartu-proyek-isi", properti: "gap", diharapkan: "8px" },
{ elemen: ".kartu-proyek-isi", properti: "padding", diharapkan: "24px" },
// Judul kartu 20px, deskripsi 14px dengan warna lembut.
{ elemen: ".kartu-proyek-judul", properti: "font-size", diharapkan: "20px" },
{ elemen: ".kartu-proyek-deskripsi", properti: "font-size", diharapkan: "14px" },
{ elemen: ".kartu-proyek-deskripsi", properti: "color", diharapkan: "rgb(71, 85, 105)" },
// Tautan kartu berwarna primer.
{ elemen: ".tautan-kartu", properti: "color", diharapkan: "rgb(37, 99, 235)" },
];
tests/responsive-design.spec.js
Unduh berkas// ============================================================
// Spesifikasi uji — Proyek: Website Portofolio
// Bagian 4: Responsive Design
// ============================================================
// Berkas ini merangkum asersi yang diverifikasi oleh harness
// pada halaman uji (viewport 1280x720, sehingga seluruh media
// query max-width berada pada keadaan tidak aktif dan nilai
// desktop yang berlaku):
// /tests/css/proyek/website-portofolio/responsive-design.html
// Setiap entri memasangkan elemen probe dengan properti CSS
// dan nilai computed yang diharapkan.
const asersi = [
// Wadah tetap 1120px dan berpadding 24px di luar breakpoint 767px.
{ elemen: ".wadah", properti: "max-width", diharapkan: "1120px" },
{ elemen: ".wadah", properti: "padding-left", diharapkan: "24px" },
// Hero dua kolom: track 1.1fr dan 0.9fr dari konten 1040px
// (1072 - gap 32) dibagi proporsional menjadi 572px dan 468px.
{ elemen: ".bagian-hero", properti: "display", diharapkan: "grid" },
{ elemen: ".bagian-hero", properti: "gap", diharapkan: "32px" },
{ elemen: ".bagian-hero", properti: "grid-template-columns", diharapkan: "572px 468px" },
// Menu navigasi tetap horizontal dengan gap 24px.
{ elemen: ".navigasi-menu", properti: "flex-direction", diharapkan: "row" },
{ elemen: ".navigasi-menu", properti: "gap", diharapkan: "24px" },
// Grid proyek tetap tiga track (minmax 280px) di desktop.
{
elemen: ".grid-proyek",
properti: "grid-template-columns",
diharapkan: "341.328px 341.328px 341.344px",
},
// Judul hero tetap 44px: clamp mencapai nilai maksimumnya.
{ elemen: ".judul-hero", properti: "font-size", diharapkan: "44px" },
// Navigation tetap sticky di atas halaman.
{ elemen: ".navigasi", properti: "position", diharapkan: "sticky" },
{ elemen: ".navigasi", properti: "top", diharapkan: "0px" },
];