Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> CSS

Tùy chỉnh Radio Button với CSS appearance Property

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ặc for/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-visible rõ ràng cho keyboard navigation.
  • ARIA: Native radio đã có role="radio", aria-checked tự động - không cần thêm.
  • Disabled: disabled attribute + style mờ.

So sánh: appearance: none vs Hidden Native + Custom Span

Tiêu chí appearance: none Hidden + Span
HTMLSạch, chỉ 1 inputCần span thêm cho UI
CSSStyle trực tiếp trên inputStyle trên span sibling
Focus ringTự nhiên trên inputCần sync thủ công
High Contrast ModeTốt hơnCó thể mất style
Browser supportModern (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ợ).