Components · 폼
Checkbox · Radio · Toggle · Select (선택 컨트롤)
텍스트 입력 대신 정해진 값을 고르게 하는 폼 컨트롤 4종. Input(텍스트 필드)과 별개로, 다중 선택·단일 선택·on/off·목록 선택을 담당한다.
정의
정해진 값을 고르게 하는 폼 컨트롤 묶음이다 — 다중 선택 Checkbox, 단일 선택 Radio, 즉시 적용 on/off Toggle, 목록 선택 Select.
Anatomy
| 부위 | 토큰 |
|---|---|
| ① Checkbox | 시각 --checkbox-size(16)·히트영역 --checkbox-target(44)·보더 --checkbox-border·선택 bg --checkbox-bg-checked·체크 --checkbox-fg-check·모서리 --checkbox-radius |
| ② Radio | 시각 --radio-size(16)·히트영역 --radio-target(44)·보더 --radio-border·점 --radio-dot |
| ③ Toggle | 트랙 --toggle-track-width(48)×--toggle-track-height(24)·off --toggle-track-off·on --toggle-track-on·손잡이 --toggle-thumb |
| ④ Select | Input 토큰 재사용(--input-bg·--input-border·--input-radius·--input-pad-x/--input-pad-y) + 화살표 --select-arrow-fg |
변형
| 컨트롤 | 용도 | 선택 수 |
|---|---|---|
| Checkbox | 다중 선택·동의 — 3상태(선택/비선택/일부선택 indeterminate) | 0~N |
| Radio | 상호배타 단일 선택(옵션 2~7개) | 1 |
| Toggle | 즉시 적용되는 on/off 설정 | on/off |
| Select | 옵션 7~20개 목록 선택(20개 초과는 콤보박스 검토) | 1 |
상태
| 상태 | Checkbox / Radio | Toggle | Select |
|---|---|---|---|
| Default | 보더 --checkbox-border/--radio-border | 트랙 --toggle-track-off | 보더 --input-border |
| Selected/On | bg --checkbox-bg-checked / 점 --radio-dot | 트랙 --toggle-track-on + 손잡이 이동 | 선택값 표시 |
| Focus | box-shadow: var(--focus-ring) | 동일 | border --input-border-focus + --focus-ring |
| Disabled | opacity: var(--opacity-disabled) + cursor: not-allowed (공통) | 동일 | 동일 |
| Indeterminate | 체크박스 전용 — 가로 막대(--checkbox-fg-check) | 해당 없음 | 해당 없음 |
Do / Don't
Do
- 체크박스·라디오는 시각 박스가 16px여도 클릭 히트영역은
--checkbox-target(44px)을 패딩으로 확보한다. - 전체선택/부분선택 관계는 상위 체크박스의
indeterminate(일부선택) 상태로 표현한다. - Toggle 라벨은 상태와 무관한 명사형으로 고정한다("자동 발행", "다크 모드").
- 라디오·체크박스 그룹은
<fieldset>+<legend>로 묶어 무엇을 고르는지 알린다.
Don't
- Toggle 라벨을 "켜짐/꺼짐"처럼 상태 따라 바꾸지 말 것 — 체크박스와 혼동된다.
- 상호배타 선택에 체크박스를 쓰지 말 것 — 단일 선택은 Radio·Select.
- 옵션이 2~3개인데 Select에 숨기지 말 것 — 라디오로 펼쳐 보여 준다.
콘텐츠 규칙
- 라벨은 명사구·짧은 서술 한 줄. 체크박스 동의문은 능동형("개인정보 수집에 동의합니다").
- Toggle 라벨은 기능 명사로 고정, 상태어(켜짐/꺼짐) 금지.
- Select의 첫 옵션은 안내 겸 placeholder("선택하세요")로 두되 값이 아니게 한다(
value=""). - Radio·Checkbox 라벨은 컨트롤 오른쪽에 두고
<label>로 감싸 라벨 클릭으로도 선택되게 한다.
키보드 인터랙션
| 키 | 동작 |
|---|---|
Tab | 다음 컨트롤(또는 라디오 그룹)으로 이동 |
Space | Checkbox·Toggle 선택 토글 |
방향키 | Radio 그룹 내 선택 이동 / 열린 Select의 옵션 이동 |
Enter / Space | Select 열기·닫기 |
Esc | 열린 Select 닫기(포커스는 트리거로 복귀) |
접근성
- 네이티브 요소 기반(
<input type="checkbox|radio">·<select>) — 커스텀 div 대신 시각만 스타일링(키보드·스크린리더 기본 지원). - 라디오·체크박스 그룹은
<fieldset>+<legend>로 그룹화, 각 컨트롤은<label>연결. - Indeterminate는 시각뿐 아니라 JS로
el.indeterminate = true를 설정한다(HTML 속성만으론 안 됨). - 시각 박스가 16px여도 히트영역은
--target-min(44px). 색만으로 선택 여부를 표시하지 말고 체크/점 형태를 병행한다. 기준:../guidelines/accessibility.md.
코드
html
<form class="pi-form-demo">
<fieldset class="pi-field">
<legend>발행 채널</legend>
<label class="pi-checkbox"><input type="checkbox" id="ch-all"><span>전체 선택</span></label>
<label class="pi-checkbox"><input type="checkbox" checked><span>네이버 블로그</span></label>
<label class="pi-checkbox"><input type="checkbox"><span>인스타그램</span></label>
</fieldset>
<fieldset class="pi-field">
<legend>주력 소재</legend>
<label class="pi-radio"><input type="radio" name="mat" checked><span>노면표시재</span></label>
<label class="pi-radio"><input type="radio" name="mat"><span>직물시트</span></label>
</fieldset>
<label class="pi-toggle">
<input type="checkbox" role="switch" checked>
<span class="pi-toggle__track"></span>
<span class="pi-toggle__label">자동 발행</span>
</label>
<span class="pi-field">
<label for="plan">구독 상품</label>
<span class="pi-select">
<select id="plan">
<option value="">선택하세요</option>
<option>디자인구독 · 베이직</option>
<option>안전시설관리 구독</option>
</select>
</span>
</span>
</form>css
.pi-form-demo { display: flex; flex-direction: column; gap: var(--space-6); }
.pi-field { border: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2); }
.pi-field legend, .pi-field > label { font: var(--text-label); color: var(--color-text-heading); padding: 0; }
.pi-checkbox, .pi-radio { display: flex; align-items: center; gap: var(--space-3); min-height: var(--checkbox-target); cursor: pointer; word-break: keep-all; }
.pi-checkbox input { width: var(--checkbox-size); height: var(--checkbox-size); border-radius: var(--checkbox-radius); accent-color: var(--checkbox-bg-checked); }
.pi-radio input { width: var(--radio-size); height: var(--radio-size); accent-color: var(--radio-dot); }
.pi-checkbox span, .pi-radio span { font: var(--text-body); color: var(--color-text-primary); }
.pi-checkbox input:focus-visible, .pi-radio input:focus-visible { outline: none; box-shadow: var(--focus-ring); }
.pi-toggle { display: inline-flex; align-items: center; gap: var(--space-3); min-height: var(--target-min); cursor: pointer; }
.pi-toggle input { position: absolute; width: 1px; height: 1px; opacity: 0; }
.pi-toggle__track { position: relative; flex: none; width: var(--toggle-track-width); height: var(--toggle-track-height); border-radius: var(--radius-full); background: var(--toggle-track-off); transition: background var(--duration-base) var(--easing-out); }
.pi-toggle__track::before { content: ""; position: absolute; top: var(--space-2); left: var(--space-2); width: var(--space-5); height: var(--space-5); border-radius: var(--radius-full); background: var(--toggle-thumb); transition: transform var(--duration-base) var(--easing-out); }
.pi-toggle input:checked + .pi-toggle__track { background: var(--toggle-track-on); }
.pi-toggle input:checked + .pi-toggle__track::before { transform: translateX(calc(var(--toggle-track-width) - var(--toggle-track-height))); }
.pi-toggle input:focus-visible + .pi-toggle__track { outline: none; box-shadow: var(--focus-ring); }
.pi-toggle__label { font: var(--text-body); color: var(--color-text-primary); }
.pi-select { position: relative; display: inline-flex; align-items: center; }
.pi-select select { appearance: none; -webkit-appearance: none; padding: var(--input-pad-y) var(--space-9) var(--input-pad-y) var(--input-pad-x); border: var(--border-thin) solid var(--input-border); border-radius: var(--input-radius); background: var(--input-bg); color: var(--input-fg); font: var(--text-body); min-height: var(--target-min); cursor: pointer; word-break: keep-all; }
.pi-select select:focus-visible { outline: none; border-color: var(--input-border-focus); box-shadow: var(--focus-ring); }
.pi-select select:disabled { opacity: var(--opacity-disabled); cursor: not-allowed; }
.pi-select::after { content: ""; position: absolute; right: var(--space-4); top: calc(50% - var(--space-1)); pointer-events: none; width: var(--space-2); height: var(--space-2); border-right: var(--border-medium) solid var(--select-arrow-fg); border-bottom: var(--border-medium) solid var(--select-arrow-fg); transform: rotate(45deg); }
@media (prefers-reduced-motion: reduce) { .pi-toggle__track, .pi-toggle__track::before { transition: none; } }js
// 전체선택 체크박스의 일부선택(indeterminate) 동기화 (의존성 없음)
document.querySelectorAll(".pi-form-demo").forEach((form) => {
const all = form.querySelector("#ch-all");
if (!all) return;
const items = [...form.querySelectorAll(".pi-checkbox input")].filter((el) => el !== all);
const sync = () => {
const on = items.filter((el) => el.checked).length;
all.checked = on === items.length;
all.indeterminate = on > 0 && on < items.length;
};
all.addEventListener("change", () => items.forEach((el) => (el.checked = all.checked)));
items.forEach((el) => el.addEventListener("change", sync));
sync();
});Changelog
| 날짜 | 변경 |
|---|---|
| 2026-07-15 | 문서 계약(10섹션) 적용 — KRDS 벤치마킹 규격(2026-07-15) 승계, Checkbox 3상태·Radio·Toggle·Select 4종 변형·코드·접근성 신설 |