Komponen · · 2 menit baca
Komponen e-commerce: kartu produk, badge diskon & rating bintang
Kode lengkap kartu produk toko online: rating bintang dua lapis tanpa gambar, aspect-ratio yang mencegah layout shift, line-clamp, dan state habis stok — editor + preview live berdampingan.
<div class="grid">
<article class="card">
<div class="card__media">
<span class="card__badge">-20%</span>
</div>
<div class="card__body">
<p class="card__kicker">Arabika</p>
<h3 class="card__name">Kopi Gayo Premium 250g</h3>
<!-- Rating: 1 elemen isi di atas 1 elemen dasar, lebar = persen -->
<span class="rating" role="img" aria-label="4,2 dari 5">
<span class="rating__base">★★★★★</span>
<span class="rating__fill" style="width: 84%">★★★★★</span>
</span>
<p class="card__price">
<strong>Rp 85.000</strong>
<s>Rp 106.000</s>
</p>
<button class="btn" type="button">+ Keranjang</button>
</div>
</article>
<article class="card card--out">
<div class="card__media">
<span class="card__badge card__badge--out">Habis</span>
</div>
<div class="card__body">
<p class="card__kicker">Robusta</p>
<h3 class="card__name">Robusta Temanggung 500g</h3>
<span class="rating" role="img" aria-label="5 dari 5">
<span class="rating__base">★★★★★</span>
<span class="rating__fill" style="width: 100%">★★★★★</span>
</span>
<p class="card__price"><strong>Rp 62.000</strong></p>
<button class="btn" type="button" disabled>Stok habis</button>
</div>
</article>
</div>.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
gap: 14px;
}
.card {
border: 1px solid #e4e4e7;
background: #fff;
display: flex;
flex-direction: column;
}
/* Rasio terkunci: grid tidak "loncat" saat gambar termuat */
.card__media {
position: relative;
aspect-ratio: 1 / 1;
background: #fafafa;
}
.card__badge {
position: absolute;
top: 10px;
left: 10px;
background: #0d9488;
color: #fff;
font-size: 11px;
font-weight: 600;
padding: 3px 8px;
}
.card__badge--out { background: #71717a; }
.card__body { padding: 14px; }
.card__kicker {
margin: 0;
font-size: 10px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: #71717a;
}
.card__name {
margin: 6px 0 8px;
font-size: 14px;
line-height: 1.35;
/* Nama panjang tidak merusak tinggi kartu */
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Rating bintang tanpa gambar & tanpa JavaScript */
.rating {
position: relative;
display: inline-block;
font-size: 13px;
line-height: 1;
color: #d4d4d8;
}
.rating__fill {
position: absolute;
inset: 0;
overflow: hidden;
white-space: nowrap;
color: #f59e0b;
}
.card__price {
margin: 10px 0 0;
display: flex;
align-items: baseline;
gap: 8px;
}
.card__price strong { font-size: 17px; }
.card__price s { font-size: 12px; color: #a1a1aa; }
.btn {
margin-top: 12px;
width: 100%;
border: 0;
background: #09090b;
color: #fff;
font-size: 12px;
padding: 9px 12px;
cursor: pointer;
}
.btn:hover { background: #0f766e; }
.btn:disabled {
background: #f4f4f5;
color: #a1a1aa;
cursor: not-allowed;
}Preview
Kartu produk adalah komponen paling banyak dilihat di sebuah toko online — ia muncul puluhan kali di setiap halaman katalog. Editor di atas berisi kode lengkapnya, dan panel kanan menjalankan kode itu juga: yang kamu baca persis yang kamu lihat.
Rating bintang tanpa gambar dan tanpa JavaScript
Trik yang kupakai di semua project: dua lapis teks bintang yang identik, satu di atas yang lain. Lapisan bawah abu-abu (★★★★★), lapisan atas berwarna emas dan dipotong dengan width persen:
<span class="rating" role="img" aria-label="4,2 dari 5">
<span class="rating__base">★★★★★</span>
<span class="rating__fill" style="width: 84%">★★★★★</span>
</span>
4,2 dari 5 = 84%. Rating setengah bintang bekerja otomatis, tanpa sprite, tanpa SVG, tanpa perhitungan di JavaScript. role="img" + aria-label membuat screen reader membacanya sebagai satu nilai, bukan sepuluh karakter bintang.
Tiga detail yang menjaga grid tetap rapi
aspect-ratio: 1 / 1pada area gambar. Tanpa ini, kartu memendek sebelum gambar termuat lalu “meloncat” — layout shift yang merusak Core Web Vitals dan terasa murah.-webkit-line-clamp: 2pada nama produk. Nama panjang dipotong di dua baris, jadi semua kartu dalam satu baris grid punya tinggi yang sama.grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)). Jumlah kolom menyesuaikan sendiri tanpa satu pun media query.
Status habis stok: nonaktifkan, jangan sembunyikan
Produk habis tetap ditampilkan (SEO dan pengunjung yang mau menunggu restock), tapi tombolnya disabled dan badge-nya berubah abu-abu. Yang penting: keadaan itu dikomunikasikan lewat atribut disabled, bukan hanya warna — tombol non-aktif otomatis dilewati keyboard dan diumumkan screen reader.
Harga: satu format, satu helper
Perhatikan struktur harga — harga aktif <strong>, harga coret <s>. Elemen <s> punya makna semantik “tidak lagi berlaku”, tepat untuk harga sebelum diskon. Formatnya selalu lewat helper Intl.NumberFormat yang sama (lihat build note Katalog + keranjang), jangan pernah diformat manual per tempat.
Pelajaran
- Efek visual yang terlihat “butuh library” sering hanya butuh dua elemen dan satu
overflow: hidden. - Kunci rasio dan tinggi teks sejak awal — layout shift adalah bug yang paling sering dianggap “wajar” padahal paling terasa.
- Setiap state (habis, diskon, rating) harus punya penanda non-visual, bukan cuma warna.
Mau fitur seperti ini dibangun untuk bisnismu?