Giới thiệu
Radio button native khó style cross-browser. appearance: none xóa giao diện mặc định, cho phép xây dựng radio button tùy chỉnh hoàn toàn với CSS.
Cú pháp
input[type="radio"] {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
/* Custom styles ở đây */
}
Ví dụ 1: Radio Button tròn cơ bản
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Custom Radio Buttons</title>
<style>
* { box-sizing: border-box; }
body { font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; padding: 2rem; display: flex; flex-direction: column; gap: 2rem; align-items: center; background: #fafafa; }
.radioGroup { display: flex; flex-wrap: wrap; gap: 1.5rem; justify-content: center; }
.radioItem {
display: flex; align-items: center; gap: 0.75rem;
cursor: pointer; font-size: 1.1rem; color: #333;
padding: 0.5rem 1rem; border-radius: 50px;
transition: background 0.2s;
}
.radioItem:hover { background: #f0f0f0; }
/* Ẩn radio native */
.radioItem input[type="radio"] {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
width: 22px; height: 22px;
border: 2px solid #ddd;
border-radius: 50%;
background: white;
position: relative;
cursor: pointer;
flex-shrink: 0;
transition: border-color 0.2s, box-shadow 0.2s;
}
/* Hover unchecked */
.radioItem input[type="radio"]:hover { border-color: #8e24aa; }
/* Checked state */
.radioItem input[type="radio"]:checked {
border-color: #8e24aa;
box-shadow: inset 0 0 0 4px white, 0 0 0 2px #8e24aa;
}
/* Inner dot (dùng box-shadow inset) */
.radioItem input[type="radio"]:checked::before {
content: "";
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
width: 8px; height: 8px; border-radius: 50%;
background: #8e24aa;
}
/* Focus visible */
.radioItem input[type="radio"]:focus-visible {
outline: none; box-shadow: 0 0 0 3px rgba(142, 36, 170, 0.3);
}
/* Disabled */
.radioItem input[type="radio"]:disabled {
opacity: 0.5; cursor: not-allowed; border-color: #ccc;
}
.radioItem:has(input:disabled) { color: #999; cursor: not-allowed; }
</style>
</head>
<body>
<h2 style="color:#2c3e50; text-align:center;">Custom Radio Buttons</h2>
<fieldset class="radioGroup">
<legend>Chọn phương thức thanh toán</legend>
<label class="radioItem">
<input type="radio" name="payment" value="visa"> Visa
</label>
<label class="radioItem">
<input type="radio" name="payment" value="mastercard" checked> Mastercard
</label>
<label class="radioItem">
<input type="radio" name="payment" value="paypal"> PayPal
</label>
<label class="radioItem">
<input type="radio" name="payment" value="cod"> COD (Thanh toán khi nhận)
</label>
<label class="radioItem">
<input type="radio" name="payment" value="bank" disabled> Ngân hàng (Tạm ẩn)
</label>
</fieldset>
</body>
</html>
Ví dụ 2: Radio Card (Card clickable toàn bộ)
<style>
.radioCardGroup { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; max-width: 800px; margin: 0 auto; }
.radioCard {
position: relative;
border: 2px solid #e0e0e0; border-radius: 12px;
padding: 1.5rem; text-align: center; cursor: pointer;
transition: border-color 0.2s, box-shadow 0.2s, transform 0.2s;
}
.radioCard:hover { border-color: #8e24aa; transform: translateY(-2px); box-shadow: 0 8px 24px rgba(142, 36, 170, 0.15); }
.radioCard input[type="radio"] {
appearance: none; position: absolute; inset: 0; margin: 0; cursor: pointer; z-index: 1;
}
.radioCard input[type="radio"]:checked + .cardContent { /* Không cần, dùng sibling selector */ }
.radioCard:has(input:checked) { border-color: #8e24aa; background: #faf5ff; box-shadow: 0 8px 24px rgba(142, 36, 170, 0.2); }
.cardIcon { font-size: 2.5rem; margin-bottom: 0.75rem; }
.cardTitle { font-weight: 700; color: #2c3e50; margin-bottom: 0.25rem; }
.cardDesc { font-size: 0.9rem; color: #666; }
</style>
<div class="radioCardGroup">
<label class="radioCard">
<input type="radio" name="plan" value="basic">
<div class="cardContent">
<div class="cardIcon">📦</div>
<div class="cardTitle">Basic</div>
<div class="cardDesc">$9/tháng - 5 projects</div>
</div>
</label>
<label class="radioCard">
<input type="radio" name="plan" value="pro" checked>
<div class="cardContent">
<div class="cardIcon">🚀</div>
<div class="cardTitle">Pro</div>
<div class="cardDesc">$29/tháng - Unlimited</div>
</div>
</label>
<label class="radioCard">
<input type="radio" name="plan" value="enterprise">
<div class="cardContent">
<div class="cardIcon">🏢</div>
<div class="cardTitle">Enterprise</div>
<div class="cardDesc">Custom pricing</div>
</div>
</label>
</div>
Ví dụ 3: Radio Switch/Toggle Style
<style>
.radioSwitchGroup { display: flex; gap: 1rem; }
.radioSwitch {
position: relative; display: flex; align-items: center; gap: 0.5rem;
}
.radioSwitch input[type="radio"] {
appearance: none; width: 56px; height: 28px; border-radius: 14px;
background: #ddd; border: none; cursor: pointer; position: relative;
transition: background 0.3s;
}
.radioSwitch input[type="radio"]::before {
content: ""; position: absolute; top: 2px; left: 2px;
width: 24px; height: 24px; border-radius: 50%;
background: white; box-shadow: 0 2px 4px rgba(0,0,0,0.2);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.radioSwitch input[type="radio"]:checked { background: #8e24aa; }
.radioSwitch input[type="radio"]:checked::before { transform: translateX(28px); }
.radioSwitch input[type="radio"]:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(142, 36, 170, 0.3); }
.radioSwitch span { font-weight: 500; color: #333; }
</style>
<div class="radioSwitchGroup">
<label class="radioSwitch"><input type="radio" name="view" value="grid" checked><span>Grid</span></label>
<label class="radioSwitch"><input type="radio" name="view" value="list"><span>List</span></label>
</div>
Accessibility Checklist (Quan trọng)
- Luôn dùng
<label>bao quanh input hoặcfor/id- tăng click area, screen reader đọc label. - Name attribute giống nhau cho group radio - chỉ chọn 1 trong nhóm.
- Focus visible:
:focus-visiblerõ ràng cho keyboard navigation. - ARIA: Native radio đã có role="radio", aria-checked tự động - không cần thêm.
- Disabled:
disabledattribute + style mờ.
So sánh: appearance: none vs Hidden Native + Custom Span
| Tiêu chí | appearance: none | Hidden + Span |
|---|---|---|
| HTML | Sạch, chỉ 1 input | Cần span thêm cho UI |
| CSS | Style trực tiếp trên input | Style trên span sibling |
| Focus ring | Tự nhiên trên input | Cần sync thủ công |
| High Contrast Mode | Tốt hơn | Có thể mất style |
| Browser support | Modern (prefix needed) | Tất cả |
Khuyến nghị: Dùng appearance: none cho project modern, fallback graceful cho IE11 (nếu còn hỗ trợ).