Кастомный checkbox и radio
Стилизованные checkbox и radio через
appearance: none— нативный<input>с кастомным оформлением без потери доступности.
Задача
Стандартные <input type="checkbox"> и <input type="radio"> плохо поддаются стилизации. Нужны красивые кастомные элементы, которые при этом остаются нативными (работают в формах, с Tab-навигацией и screen reader).
Решение
<!-- Checkbox -->
<label class="checkbox">
<input class="checkbox__input" type="checkbox" name="agree" />
<span class="checkbox__box"></span>
Согласен с условиями
</label>
<!-- Radio group -->
<fieldset class="radio-group">
<legend>Выберите план</legend>
<label class="radio">
<input class="radio__input" type="radio" name="plan" value="free" />
<span class="radio__dot"></span>
Бесплатный
</label>
<label class="radio">
<input class="radio__input" type="radio" name="plan" value="pro" />
<span class="radio__dot"></span>
Pro
</label>
</fieldset>
/* === Checkbox === */
.checkbox {
display: inline-flex;
align-items: center;
gap: 10px;
cursor: pointer;
user-select: none;
}
.checkbox__input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.checkbox__box {
width: 20px;
height: 20px;
border: 2px solid #cbd5e1;
border-radius: 4px;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
transition: border-color 0.2s, background 0.2s;
flex-shrink: 0;
}
/* Галочка через ::after */
.checkbox__box::after {
content: '';
width: 5px;
height: 9px;
border: 2px solid #fff;
border-top: none;
border-left: none;
transform: rotate(45deg) translateY(-1px);
opacity: 0;
transition: opacity 0.15s;
}
.checkbox__input:checked + .checkbox__box {
background: #3b82f6;
border-color: #3b82f6;
}
.checkbox__input:checked + .checkbox__box::after { opacity: 1; }
.checkbox__input:focus-visible + .checkbox__box {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
/* === Radio === */
.radio {
display: inline-flex;
align-items: center;
gap: 10px;
cursor: pointer;
user-select: none;
}
.radio__input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.radio__dot {
width: 20px;
height: 20px;
border: 2px solid #cbd5e1;
border-radius: 50%;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
transition: border-color 0.2s;
flex-shrink: 0;
}
.radio__dot::after {
content: '';
width: 8px;
height: 8px;
background: #3b82f6;
border-radius: 50%;
transform: scale(0);
transition: transform 0.2s;
}
.radio__input:checked + .radio__dot { border-color: #3b82f6; }
.radio__input:checked + .radio__dot::after { transform: scale(1); }
.radio__input:focus-visible + .radio__dot { outline: 2px solid #3b82f6; outline-offset: 2px; }
Ключевые моменты
opacity: 0; width: 0; height: 0скрывает нативный input, но не убирает его из DOM — фокус и screen reader работают.<label>оборачивает input — клик по тексту или кастомному элементу активирует чекбокс.:focus-visible(не:focus) — показывает обводку только при клавиатурной навигации, не при клике.- Нативный
<input>сохранён — данные попадают вFormDataи стандартные атрибуты (required,disabled) работают.