본문 바로가기

Components · 폼

Checkbox · Radio · Toggle · Select (선택 컨트롤)

텍스트 입력 대신 정해진 값을 고르게 하는 폼 컨트롤 4종. Input(텍스트 필드)과 별개로, 다중 선택·단일 선택·on/off·목록 선택을 담당한다.

정의

정해진 값을 고르게 하는 폼 컨트롤 묶음이다 — 다중 선택 Checkbox, 단일 선택 Radio, 즉시 적용 on/off Toggle, 목록 선택 Select.

Anatomy

자동 발행 동의 일부 선택 노면표시재 자동 발행 디자인구독 · 베이직 ① Checkbox(3상태) ② Radio ③ Toggle ④ Select
부위토큰
① 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
④ SelectInput 토큰 재사용(--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 / RadioToggleSelect
Default보더 --checkbox-border/--radio-border트랙 --toggle-track-off보더 --input-border
Selected/Onbg --checkbox-bg-checked / 점 --radio-dot트랙 --toggle-track-on + 손잡이 이동선택값 표시
Focusbox-shadow: var(--focus-ring)동일border --input-border-focus + --focus-ring
Disabledopacity: 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다음 컨트롤(또는 라디오 그룹)으로 이동
SpaceCheckbox·Toggle 선택 토글
방향키Radio 그룹 내 선택 이동 / 열린 Select의 옵션 이동
Enter / SpaceSelect 열기·닫기
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종 변형·코드·접근성 신설