Checkbox
사용자가 하나 이상의 옵션을 고르게 하는 컴포넌트. 여러 항목 가운데 여러 개를 고르거나, 저장하기 전에 켜고 끌 것을 모아 둘 때 쓴다.
구조는 당근 SEED Checkbox(Apache-2.0)를 따른다 — 칸(Checkmark) · 칸 + 라벨(Checkbox) · 묶음(Checkbox Group). 값은 groo 토큰이고, SEED 와 다른 자리는 맨 아래 "SEED 와 다른 점" 에 적었다(2026-09-30 사용자 결정).
수치 원본은 checkbox.yaml 이다. 수치 표 자리([표: …])와 그림 자리([그림: …])는 사이트가 그 파일로 그린다 — GitHub 에서는 그 파일 · 그림 코드로 가는 링크로 보인다.
직접 골라 보기
크기 · 모양 · 톤 · 굵기 · 상태를 고르면 스펙대로 그린 Checkbox 와 그 코드가 바뀐다. 칸은 실제로 눌러 볼 수 있다.
import { Checkbox } from "@/components/ui/checkbox"
<Checkbox defaultChecked label="단종된 카드도 보기" />Anatomy
| 부위 | 설명 |
|---|---|
| ⓐ Checkmark | 칸 — 배경 · 테두리 · 모양. 체크 · 가로줄 아이콘이 들어간다. 따로 떼어 목록 행 · 표 머리에 쓴다. |
| ⓑ Icon | 체크(선택) · 가로줄(일부 선택). lucide Check · Minus, 선 3. |
| ⓒ Label | 무엇을 고르는지. 칸과 함께 눌린다. |
| ⓓ Focus ring | 키보드 포커스에만 2px 링 · 2px 띄움(v106). |
부위
| 부위 | 설명 |
|---|---|
root | Checkbox 한 줄 — 칸 + 라벨, 누르는 영역 |
checkmark | 칸(Checkmark) — 배경 · 테두리 · 모양. 목록 행에는 이것만 넣어 쓴다 |
icon | 체크 · 가로줄(lucide Check · Minus) |
label | 글자(Label) |
focusRing | 키보드 포커스 링(웹 · 앱 — 키보드로 옮길 때만) |
group | 묶음(Checkbox Group) — 세로로 쌓는다 |
Properties
Size
두 가지다. 칸 · 라벨 · 줄 높이가 함께 정해진다.
medium(칸 20 · 라벨 14 · 줄 32) — 기본. 화면 안의 목록 · 설정.large(칸 24 · 라벨 16 · 줄 36) — 한 화면의 중심 선택, 모바일에서 홀로 서는 선택.
누르는 영역은 라벨까지 묶어 가로 · 세로 44 까지 넓힌다(기초 Inclusive — 44 는 반드시).
크기
| size | 최소 높이 | checkmark 크기 | 글자 | 설명 |
|---|---|---|---|---|
medium (기본) | 32px | 20px | text-t4 14px / 19px | 칸 20 · 라벨 14 — 기본 |
large | 36px | 24px | text-t5 16px / 22px | 칸 24 · 라벨 16 — 한 화면의 중심 선택 · 모바일 단독 |
칸 모양마다 아이콘 크기가 다르다 — Ghost 는 칸이 없어 아이콘이 크다.
크기별 아이콘 — Square
| 속성 | medium (기본) | large |
|---|---|---|
| 아이콘 | 12px | 14px |
크기별 아이콘 — Ghost
| 속성 | medium (기본) | large |
|---|---|---|
| checkmark 배경 | transparent | transparent |
| checkmark 테두리 두께 | 0px | 0px |
| 아이콘 | 14px | 18px |
| icon 색 | color-fg-placeholder #767C8B · 다크 #838997 — 선택 안 됨도 옅은 체크로 보인다 | color-fg-placeholder #767C8B · 다크 #838997 — 선택 안 됨도 옅은 체크로 보인다 |
모든 조합에 공통인 값:
공통
| 부위 | 상태 | 속성 | 값 | 비고 |
|---|---|---|---|---|
root | enabled | 간격 | spacing-x2 8px | |
root | enabled | 커서 | pointer | |
root | enabled | 줄 맞춤 | flex-start | 줄은 칸 + 라벨만큼만 차지한다 — 묶음 폭으로 늘이지 않는다(직접 짠 줄은 묶음 폭을 쓴다) |
root | enabled | 터치 영역 | 44 × 44 | 라벨까지 묶어 44 — 목록 행에서 칸만 쓰면 줄 전체가 누르는 영역(SEED touch target · 기초 Inclusive) |
checkmark | enabled | 모서리 | radius-r1 4px | |
checkmark | enabled | 전환 대상 | background-color, border-color, color | |
checkmark | enabled | 전환 시간 | motion-duration-color-transition 150ms | |
checkmark | enabled | 전환 곡선 | motion-ease-easing cubic-bezier(0.35, 0, 0.35, 1) | |
icon | enabled | 글리프 | Check | lucide Check · 선 3 |
label | enabled | 글꼴 | font-sans | |
label | enabled | 글자·아이콘 | color-fg-neutral #1A1F2E · 다크 #F5F6FA | |
group | enabled | 간격 | spacing-x3 12px | 묶음 안 줄 사이(세로) — 줄 32 · 36 에 12 를 더해 44 · 48 마다 한 줄이라 이웃 줄과 누르는 영역(44)이 겹치지 않는다(사용자 결정 — SEED 는 4) |
focusRing | focused | 링 두께 | 2px | 웹 · 앱 같다 — 앱은 하드웨어 키보드로 옮길 때만 그린다(Flutter FocusHighlightMode.traditional), 손가락으로 누를 때는 없다(2026-10-08 — DESIGN.md State · Button 과 같다) |
focusRing | focused | 링 간격 | 2px | |
focusRing | focused | 링 색 | color-stroke-focus-ring #1A1F2E · 다크 #F5F6FA | |
checkmark | pressed | 배율 | 세로 2px 축소 | 칸만 준다 — 기준 길이 max(높이, 폭 ÷ 4, 24) 에서 2px(Feedback 의 눌림 피드백, v104) |
checkmark | pressed | 축소 시간 | motion-duration-pressed-scale 150ms | |
checkmark | pressed | 축소 곡선 | motion-ease-pressed-scale cubic-bezier(0, 0, 0.15, 1) | |
root | disabled | 커서 | not-allowed | 포인터 이벤트는 켜 둔다 — 끄면 이 커서가 안 보인다(SEED 와 같다) |
label | disabled | 글자·아이콘 | color-fg-disabled #8A91A0 · 다크 #838997 |
Weight
라벨 굵기다. 강조하거나 묶음의 부모처럼 한 단계 위에 설 때 bold.
굵기
| weight | 굵기 | 설명 |
|---|---|---|
regular (기본) | 400 | 라벨 400(기본) |
bold | 700 | 라벨 700 — 강조 · 묶음의 부모 |
Tone
선택했을 때의 색이다. neutral(짙은 회색)이 기본이고, 브랜드 색(brand)은 서비스 핵심 흐름에서만 쓴다 — 버튼에서 정한 "브랜드 색은 꼭 필요한 곳에만" 과 같다. Radio · Switch 도 같은 규칙이다.
Shape
square— 칸 + 체크. 여러 개를 고르는 목록, 사용자가 알고 골라야 하는 선택(기본).ghost— 칸 없이 체크만. 선택 안 됨도 옅은 체크로 보인다. 필수가 아니고 셋 이하일 때.
Desk 할 일 완료도 Checkbox square 다 — 옛 동그라미 체크는 따로 두지 않는다(2026-10-23 할 일 결정 — 할 일 줄 앞 Checkbox).
State
체크 여부(선택 안 됨 · 선택 · 일부 선택)와 상호작용 상태가 곱해진다.
| 상태 | 모습 |
|---|---|
enabled | 기본 |
hovered | 웹. 누름 색과 같다(v106), 축소는 없다 |
focused | 웹 · 앱. 키보드 포커스에만 링 2px · 띄움 2px(v106). 앱은 하드웨어 키보드로 옮길 때만 그리고 손가락으로 누를 때는 없다(2026-10-08) |
pressed | 누름 색 + 칸 세로 2px 거리 축소(v104). 라벨은 줄지 않는다 |
disabled | 전용 색(bg-disabled · fg-disabled, v106). 불투명도로 흐리게 하지 않는다 |
| 기본 enabled | 호버 hovered | 포커스 focused | 누름 pressed | 비활성 disabled | |
|---|---|---|---|---|---|
| 선택 안 됨 | |||||
| 선택 | |||||
| 일부 선택 |
| 기본 enabled | 호버 hovered | 포커스 focused | 누름 pressed | 비활성 disabled | |
|---|---|---|---|---|---|
| 선택 안 됨 | |||||
| 선택 | |||||
| 일부 선택 |
| 기본 enabled | 호버 hovered | 포커스 focused | 누름 pressed | 비활성 disabled | |
|---|---|---|---|---|---|
| 선택 안 됨 | |||||
| 선택 | |||||
| 일부 선택 |
상태 매트릭스 — Square · 선택 안 됨
| 상태 | 커서 | checkmark 배경 | checkmark 테두리 | checkmark 테두리 두께 | checkmark 배율 | checkmark 축소 시간 | checkmark 축소 곡선 | icon 글리프 | label 글자·아이콘 | 링 두께 | 링 간격 | 링 색 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
enabled 기본 | pointer | transparent | color-stroke-neutral-solid #767C8B · 다크 #838997 — 3:1 이상 — v109(SEED 는 옅은 선) | 1px | — | — | — | none — 선택 안 된 칸에는 아이콘이 없다 | color-fg-neutral #1A1F2E · 다크 #F5F6FA | — | — | — |
hovered 웹 — 누름 색과 같다(v106), 축소는 없다 | pointer | color-bg-layer-default-pressed #F7F8FD · 다크 #353B4D | color-stroke-neutral-solid #767C8B · 다크 #838997 — 3:1 이상 — v109(SEED 는 옅은 선) | 1px | — | — | — | none — 선택 안 된 칸에는 아이콘이 없다 | color-fg-neutral #1A1F2E · 다크 #F5F6FA | — | — | — |
focused 키보드 포커스에만 링 — 웹 · 앱(앱은 2026-10-08 부터, 손가락으로 누를 때는 없다) | pointer | transparent | color-stroke-neutral-solid #767C8B · 다크 #838997 — 3:1 이상 — v109(SEED 는 옅은 선) | 1px | — | — | — | none — 선택 안 된 칸에는 아이콘이 없다 | color-fg-neutral #1A1F2E · 다크 #F5F6FA | 2px — 웹 · 앱 같다 — 앱은 하드웨어 키보드로 옮길 때만 그린다(Flutter FocusHighlightMode.traditional), 손가락으로 누를 때는 없다(2026-10-08 — DESIGN.md State · Button 과 같다) | 2px | color-stroke-focus-ring #1A1F2E · 다크 #F5F6FA |
pressed 누름 색 + 칸 세로 2px 거리 축소(v104) | pointer | color-bg-layer-default-pressed #F7F8FD · 다크 #353B4D — SEED bg.transparent-pressed 의 불투명 짝 | color-stroke-neutral-solid #767C8B · 다크 #838997 — 3:1 이상 — v109(SEED 는 옅은 선) | 1px | 세로 2px 축소 — 칸만 준다 — 기준 길이 max(높이, 폭 ÷ 4, 24) 에서 2px(Feedback 의 눌림 피드백, v104) | motion-duration-pressed-scale 150ms | motion-ease-pressed-scale cubic-bezier(0, 0, 0.15, 1) | none — 선택 안 된 칸에는 아이콘이 없다 | color-fg-neutral #1A1F2E · 다크 #F5F6FA | — | — | — |
disabled 전용 색(v106) — 불투명도로 흐리게 하지 않는다 | not-allowed — 포인터 이벤트는 켜 둔다 — 끄면 이 커서가 안 보인다(SEED 와 같다) | color-bg-disabled #F5F6FA · 다크 #353B4D | color-stroke-neutral-weak #E5E8EF · 다크 #404757 — SEED stroke.neutral-muted 의 불투명 짝 | 1px | — | — | — | none — 선택 안 된 칸에는 아이콘이 없다 | color-fg-disabled #8A91A0 · 다크 #838997 | — | — | — |
상태 매트릭스 — Square · neutral · 선택
| 상태 | 커서 | checkmark 배경 | checkmark 테두리 두께 | checkmark 배율 | checkmark 축소 시간 | checkmark 축소 곡선 | icon 색 | label 글자·아이콘 | 링 두께 | 링 간격 | 링 색 |
|---|---|---|---|---|---|---|---|---|---|---|---|
enabled 기본 | pointer | color-bg-neutral-inverted #1A1F2E · 다크 #F5F6FA | 0px | — | — | — | color-fg-neutral-inverted #FFFFFF · 다크 #242938 | color-fg-neutral #1A1F2E · 다크 #F5F6FA | — | — | — |
hovered 웹 — 누름 색과 같다(v106), 축소는 없다 | pointer | color-bg-neutral-inverted-pressed #535866 · 다크 #B7BDCC | 0px | — | — | — | color-fg-neutral-inverted #FFFFFF · 다크 #242938 | color-fg-neutral #1A1F2E · 다크 #F5F6FA | — | — | — |
focused 키보드 포커스에만 링 — 웹 · 앱(앱은 2026-10-08 부터, 손가락으로 누를 때는 없다) | pointer | color-bg-neutral-inverted #1A1F2E · 다크 #F5F6FA | 0px | — | — | — | color-fg-neutral-inverted #FFFFFF · 다크 #242938 | color-fg-neutral #1A1F2E · 다크 #F5F6FA | 2px — 웹 · 앱 같다 — 앱은 하드웨어 키보드로 옮길 때만 그린다(Flutter FocusHighlightMode.traditional), 손가락으로 누를 때는 없다(2026-10-08 — DESIGN.md State · Button 과 같다) | 2px | color-stroke-focus-ring #1A1F2E · 다크 #F5F6FA |
pressed 누름 색 + 칸 세로 2px 거리 축소(v104) | pointer | color-bg-neutral-inverted-pressed #535866 · 다크 #B7BDCC | 0px | 세로 2px 축소 — 칸만 준다 — 기준 길이 max(높이, 폭 ÷ 4, 24) 에서 2px(Feedback 의 눌림 피드백, v104) | motion-duration-pressed-scale 150ms | motion-ease-pressed-scale cubic-bezier(0, 0, 0.15, 1) | color-fg-neutral-inverted #FFFFFF · 다크 #242938 | color-fg-neutral #1A1F2E · 다크 #F5F6FA | — | — | — |
disabled 전용 색(v106) — 불투명도로 흐리게 하지 않는다 | not-allowed — 포인터 이벤트는 켜 둔다 — 끄면 이 커서가 안 보인다(SEED 와 같다) | color-bg-disabled #F5F6FA · 다크 #353B4D | 0px | — | — | — | color-fg-disabled #8A91A0 · 다크 #838997 | color-fg-disabled #8A91A0 · 다크 #838997 | — | — | — |
상태 매트릭스 — Ghost · neutral · 선택
| 상태 | 커서 | checkmark 배경 | checkmark 테두리 두께 | checkmark 배율 | checkmark 축소 시간 | checkmark 축소 곡선 | 아이콘 | icon 색 | label 글자·아이콘 | 링 두께 | 링 간격 | 링 색 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
enabled 기본 | pointer | transparent | 0px | — | — | — | 14px | color-fg-neutral #1A1F2E · 다크 #F5F6FA | color-fg-neutral #1A1F2E · 다크 #F5F6FA | — | — | — |
hovered 웹 — 누름 색과 같다(v106), 축소는 없다 | pointer | color-bg-neutral-weak #F5F6FA · 다크 #353B4D | 0px | — | — | — | 14px | color-fg-neutral #1A1F2E · 다크 #F5F6FA | color-fg-neutral #1A1F2E · 다크 #F5F6FA | — | — | — |
focused 키보드 포커스에만 링 — 웹 · 앱(앱은 2026-10-08 부터, 손가락으로 누를 때는 없다) | pointer | transparent | 0px | — | — | — | 14px | color-fg-neutral #1A1F2E · 다크 #F5F6FA | color-fg-neutral #1A1F2E · 다크 #F5F6FA | 2px — 웹 · 앱 같다 — 앱은 하드웨어 키보드로 옮길 때만 그린다(Flutter FocusHighlightMode.traditional), 손가락으로 누를 때는 없다(2026-10-08 — DESIGN.md State · Button 과 같다) | 2px | color-stroke-focus-ring #1A1F2E · 다크 #F5F6FA |
pressed 누름 색 + 칸 세로 2px 거리 축소(v104) | pointer | color-bg-neutral-weak #F5F6FA · 다크 #353B4D — SEED palette.gray-200 | 0px | 세로 2px 축소 — 칸만 준다 — 기준 길이 max(높이, 폭 ÷ 4, 24) 에서 2px(Feedback 의 눌림 피드백, v104) | motion-duration-pressed-scale 150ms | motion-ease-pressed-scale cubic-bezier(0, 0, 0.15, 1) | 14px | color-fg-neutral #1A1F2E · 다크 #F5F6FA | color-fg-neutral #1A1F2E · 다크 #F5F6FA | — | — | — |
disabled 전용 색(v106) — 불투명도로 흐리게 하지 않는다 | not-allowed — 포인터 이벤트는 켜 둔다 — 끄면 이 커서가 안 보인다(SEED 와 같다) | transparent | 0px | — | — | — | 14px | color-fg-disabled #8A91A0 · 다크 #838997 | color-fg-disabled #8A91A0 · 다크 #838997 | — | — | — |
모션
| 전환 | 시간 | 곡선 | 대상 | 비고 |
|---|---|---|---|---|
선택 안 됨 ↔ 선택 ↔ 일부 선택 | motion-duration-color-transition 150ms | motion-ease-easing cubic-bezier(0.35, 0, 0.35, 1) | background, border, icon color | 칸의 채움 · 테두리 · 아이콘 색이 함께 바뀐다(SEED Checkmark 와 같다) — 아이콘은 커지거나 줄지 않는다 |
누름 | motion-duration-pressed-scale 150ms | motion-ease-pressed-scale cubic-bezier(0, 0, 0.15, 1) | scale | 칸만 세로 2px 거리 축소 — 라벨은 줄지 않는다(v104) |
Group
여러 항목을 묶어 세로로 쌓는다. 줄 사이는 12 다 — 줄 높이 32 · 36 에 더하면 44 · 48 마다 한 줄이 서서, 묶음 안에서도 줄마다 누르는 영역 44 를 온전히 받는다(사용자 결정). 줄은 칸 + 라벨만큼만 차지하고 묶음 폭으로 늘이지 않는다. 부모 Checkbox 를 맨 위에 둘 수 있다 — 부모를 고르면 자식이 모두 선택되고, 자식을 일부만 고르면 부모는 일부 선택(가로줄)이 된다.
묶음
| 속성 | 값 | 비고 |
|---|---|---|
| 간격 | spacing-x3 12px | 묶음 안 줄 사이(세로) — 줄 32 · 36 에 12 를 더해 44 · 48 마다 한 줄이라 이웃 줄과 누르는 영역(44)이 겹치지 않는다(사용자 결정 — SEED 는 4) |
Guidelines
누르는 영역
라벨을 포함한 줄 전체가 누르는 영역이다 — 위아래로 44 까지 넓히고, 묶음에서는 줄 사이 12 가 이 영역이 서로 겹치지 않게 한다. 목록처럼 칸(Checkmark)만 행에 넣어 쓸 때는 행 전체가 눌려야 한다.
묶음 쓰기
항목이 여럿이면 묶음으로 둔다. 모두를 한 번에 고를 일이 있으면 부모를 맨 위에 둔다.
모양 고르기
필수가 아니고 셋 이하면 ghost, 필수이거나 사용자가 알고 골라야 하면 square 다.
선택 색
선택 색은 짙은 회색이 기본이다. 브랜드 색은 서비스 핵심 흐름에만 — 체크가 많은 화면에 브랜드 색을 깔면 브랜드 색 버튼이 설 자리가 없어진다.
오류
칸 모양은 바꾸지 않는다. 묶음 아래에 무엇을 해야 하는지 글로 알린다(사용자 결정 — SEED 와 같다). 오늘 제품의 체크 오류는 모두 묶음 단위(하나 이상 고르기)다. 묶음을 Field 로 감싸면 그 꼬리가 오류 글 자리이고, 라벨은 묶음의 이름으로 이어진다(2026-10-01). Field 의 오류 · 막힘도 칸에 닿는다 — 오류면 칸마다 aria-invalid, 막히면 묶음 aria-disabled 와 칸마다 disabled 다(2026-10-08). 묶음(role="group")에는 aria-invalid · aria-required 를 달지 않는다 — 그 역할은 받지 않는다(SEED 와 같다).
Checkbox 와 Switch
둘 다 켜고 끄는 선택을 보인다.
| Checkbox | Switch | |
|---|---|---|
| 값이 적용될 때 | 저장 같은 액션을 해야 적용(권장) | 누르는 순간 적용 — 저장해야 적용되는 값에는 쓰지 않는다 |
| 항목 구성 | 한 묶음에 여러 항목 | 항목마다 따로 |
| 하위 항목 | 부모가 모두를 고르고 풀 수 있다 | 부모와 하위 사이 관계 없음 — 위가 꺼지면 아래를 막는다(값은 그대로) |
| 경계 사례 | 고정 금액으로 기록(켜면 금액 칸이 나와도 저장 때 반영) · 자동 기록 · 상단에 고정 | 결제 알림 · 단종 카드 포함(누르면 목록이 바로 바뀐다) · 일정의 종일(저장 폼 안이어도 날짜 칸이 바로 바뀐다) |
이 표는 Checkbox 와 Switch 의 한 벌이다 — Switch 문서는 이 표를 가리킨다. 결정(누르는 순간 바뀌는 것만 Switch — SEED 와 같은 강도)은 그대로이고, 이관 때 고를 경계 사례 한 줄을 더했다(2026-10-22 사용자 결정 7B — https://claude.ai/artifact/SmHD31RQxTjWbfK83nbcym). 다른 고르는 컴포넌트와의 표는 Select 의 "고르는 컴포넌트 고르기".
코드
레시피 recipes/shadcn/components/ui/checkbox.tsx 를 쓴다 — Checkbox(칸 + 라벨) · Checkmark(칸) · CheckboxGroup(묶음). 아래 미리보기는 스펙 값으로 그린 모습이다.
기본
import { Checkbox } from "@/components/ui/checkbox"
<Checkbox label="단종된 카드도 보기" />크기 · 굵기
<Checkbox size="medium" label="medium" />
<Checkbox size="large" label="large" />
<Checkbox size="large" weight="bold" label="large · bold" />모양 · 톤
<Checkbox defaultChecked label="neutral" />
<Checkbox defaultChecked tone="brand" label="brand" />
<Checkbox defaultChecked shape="ghost" label="ghost" />묶음 · 일부 선택
const all = ["tx", "budget", "memo"] as const
const [picked, setPicked] = useState<string[]>(["tx"])
const parent = picked.length === all.length ? true : picked.length ? "indeterminate" : false
<CheckboxGroup aria-label="내보낼 데이터">
<Checkbox weight="bold" label="전체" checked={parent}
onCheckedChange={(v) => setPicked(v === true ? [...all] : [])} />
<Checkbox label="거래 내역" checked={picked.includes("tx")}
onCheckedChange={(v) => setPicked((p) => (v ? [...p, "tx"] : p.filter((x) => x !== "tx")))} />
{/* 예산 · 메모도 같은 방식 */}
</CheckboxGroup>비활성
<Checkbox disabled label="이 카드 기억하기" />
<Checkbox disabled defaultChecked label="이 카드 기억하기" />칸만(목록 행)
목록 줄은 List 의 ListCheckItem 을 쓴다 — 줄의 여백 · 글자 · 누름이 정해져 있다. 표처럼 행을 따로 짜야 할 때만 아래처럼 행을 <label> 로 감싸 행 어디를 눌러도 선택되게 하고, group/checkbox 를 달아 행을 누르거나 올려도 칸이 누름 색 · 축소로 반응하게 한다.
import { Checkmark } from "@/components/ui/checkbox"
<label className="group/checkbox flex cursor-pointer items-center gap-x3 px-x6 py-x3">
<Checkmark checked={selected} onCheckedChange={setSelected} aria-label="9월 25일 월급 선택" />
<span className="flex-1">월급</span>
<span>+3,200,000원</span>
</label>Behavior
| 인터랙션 | 동작 |
|---|---|
| Click / Tap(칸 · 라벨) | 선택 ↔ 선택 안 됨. 일부 선택이면 선택으로. disabled 면 무시. |
Keyboard Space | 포커스 상태에서 누르기와 같다. |
Keyboard Enter | 폼을 제출한다 — 진짜 <input type="checkbox"> 처럼 폼의 기본 버튼(첫 제출 버튼)을 누른다. 칸은 바뀌지 않는다. 기본 버튼이 없거나 막혀 있으면 아무것도 하지 않는다. Radix 는 Enter 를 막기만 해서 레시피가 연다(사용자 결정 2026-10-08 — SEED 와 같다). 칸만(Checkmark) 쓰는 자리 · List 의 체크 줄 · Select Box 여럿 고르기도 같다. |
Keyboard Tab | 다음 포커스로. 묶음 안의 항목도 하나씩 들어간다. |
| 부모(일부 선택) | 누르면 자식을 모두 선택. 다 선택이면 모두 해제. |
| Disabled | 누르기 · 키보드 불가, 포커스에서 빠진다. 묶음에 disabled 를 주거나 막힌 Field 로 감싸면 칸이 모두 막힌다(묶음 aria-disabled). |
Form 안 — <input type="checkbox">(또는 Radix 의 숨은 input)는 폼 제출에 들어간다. 여러 개의 결과는 배열이다. 검증은 onSubmit 또는 묶음을 떠날 때 — 누르는 동안 오류를 띄우지 않는다.
Accessibility
| 기준 | 검증 |
|---|---|
| WCAG 1.4.3 Color contrast(라벨 ≥ 4.5:1) | 라벨 fg-neutral × 흰 표면 16:1+ ✓ |
| WCAG 1.4.11 Non-text contrast(UI ≥ 3:1) | 선택 안 된 칸 테두리 stroke-neutral-solid × 표면 4.2:1(다크 4.1:1) ✓ · 선택 채움 bg-neutral-inverted 16:1+ ✓ · Ghost 선택 안 됨은 fg-placeholder 4.18:1(다크 4.13:1) ✓ — v119 에서 gray-600 으로 옅어졌다(2026-10-16 글자 역할 D7) |
| WCAG 2.4.7 Focus visible | 키보드 포커스에만 링 2px · 띄움 2px(stroke-focus-ring) — 앱도 키보드로 옮길 때만 같은 링(2026-10-08) |
| WCAG 2.5.8 Target Size — Minimum(AA ≥ 24×24) | 칸 20 · 24 + 라벨까지 묶은 줄 32 · 36 ✓ |
| WCAG 2.5.5 Target Size — Enhanced(AAA ≥ 44×44) | 라벨까지 묶어 위아래 44 로 넓힌다 ✓. 묶음 안에서도 줄 사이 12 라 줄마다 44 · 48 을 받는다(줄 사이가 4 면 이웃과 겹쳐 36 · 40 이 된다) — 목록 행에서 칸만 쓰면 행 전체 |
| ARIA | role="checkbox" + aria-checked="true · false · mixed"(mixed = 일부 선택). 라벨은 <label> 로 묶거나 aria-labelledby. 칸만 쓰면 aria-label 필수. 묶음은 <fieldset> + <legend>(또는 role="group" + aria-labelledby), 오류 글은 묶음에 aria-describedby. 오류면 칸마다 aria-invalid, 막히면 묶음 aria-disabled + 칸마다 disabled — role="group" 은 aria-invalid · aria-required 를 받지 않아 묶음에는 달지 않는다(Field 로 감싸면 레시피가 건다). 앱은 묶음에 이름 노드만 둔다 — 묶음 제목을 Semantics(label) 로 단 노드 하나, 묶음 역할(group)은 없다(Flutter 3.44 에 그 역할이 없다 — 2026-10-17 사용자 결정 03A-014 A). |
| Reduced motion | 모션 줄이기면 누름 축소를 빼고 색만 바꾼다(기초 Motion). |
Do / Don't
✅ Do
- 모든 Checkbox 에 라벨을 둔다(보이는 글자 또는
aria-label). - 여러 개 고르기는 Checkbox, 하나만 고르기는 Radio, 바로 적용되는 켜기 · 끄기는 Switch.
- 부모 · 자식 묶음에서 일부만 고르면 부모는 일부 선택으로 둔다.
- 목록 행에 칸만 넣으면 행 전체가 눌리게 한다.
- 약관 보기 같은 링크는 라벨 옆이나 아래에 둔다 — 라벨은 고르는 글만.
❌ Don't
- 바로 적용되는 켜기 · 끄기에 Checkbox — "알림 받기" 는 Switch 다.
- 하나만 고르는 묶음에 Checkbox — Radio 를 쓴다.
- 선택 색을 브랜드로 깔아 두기 — 브랜드 색은 핵심 흐름에서만.
- 오류를 칸 색만으로 알리기 — 묶음 아래 글로 알린다.
- 라벨 안에 링크 · 버튼 — 줄의 누르는 영역(44)이 덮어 눌리지 않는다. 라벨 옆이나 아래에 따로 둔다(사용자 결정 2026-10-08).
Specification
checkbox.yaml 의 규칙을 하나도 빼지 않고 조건마다 그린다 — 웹 · 앱이 Checkbox 를 만들 때 이 값을 그대로 쓴다. 조건이 없는 Base 가 모든 조합에 걸리고, 뒤의 규칙이 앞의 같은 값을 덮는다. 상태는 enabled 에서 바뀌는 값만 적었다.
Base
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 간격gap | spacing-x28px |
| 줄 맞춤alignSelf | flex-start줄은 칸 + 라벨만큼만 차지한다 — 묶음 폭으로 늘이지 않는다(직접 짠 줄은 묶음 폭을 쓴다) | ||
| 커서cursor | pointer | ||
| 터치 영역touchTarget | 44 × 44라벨까지 묶어 44 — 목록 행에서 칸만 쓰면 줄 전체가 누르는 영역(SEED touch target · 기초 Inclusive) | ||
checkmark | enabled | 모서리radius | radius-r14px |
| 전환 대상transitionProperty | background-color, border-color, color | ||
| 전환 시간transitionDuration | motion-duration-color-transition150ms | ||
| 전환 곡선transitionEasing | motion-ease-easingcubic-bezier(0.35, 0, 0.35, 1) | ||
icon | enabled | 글리프glyph | Checklucide `Check` · 선 3 |
label | enabled | 글자·아이콘foreground | fg-neutral#1A1F2E다크 #F5F6FA |
| 글꼴fontFamily | font-sansPretendard, Inter, sans-serif | ||
group | enabled | 간격gap | spacing-x312px묶음 안 줄 사이(세로) — 줄 32 · 36 에 12 를 더해 44 · 48 마다 한 줄이라 이웃 줄과 누르는 영역(44)이 겹치지 않는다(사용자 결정 — SEED 는 4) |
focusRing | focused | 링 두께width | 2px웹 · 앱 같다 — 앱은 하드웨어 키보드로 옮길 때만 그린다(Flutter FocusHighlightMode.traditional), 손가락으로 누를 때는 없다(2026-10-08 — DESIGN.md State · Button 과 같다) |
| 링 색color | stroke-focus-ringDesk#0147AD다크 #7AA9F6HR#075B3C다크 #2ABE84 | ||
| 링 간격offset | 2px | ||
checkmark | pressed | 배율scale | 세로 2px 축소칸만 준다 — 기준 길이 max(높이, 폭 ÷ 4, 24) 에서 2px(Feedback 의 눌림 피드백, v104) |
| 축소 시간scaleDuration | motion-duration-pressed-scale150ms | ||
| 축소 곡선scaleEasing | motion-ease-pressed-scalecubic-bezier(0, 0, 0.15, 1) | ||
root | disabled | 커서cursor | not-allowed포인터 이벤트는 켜 둔다 — 끄면 이 커서가 안 보인다(SEED 와 같다) |
label | disabled | 글자·아이콘foreground | fg-disabled#8A91A0다크 #838997 |
size=medium
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 최소 높이minHeight | 32px |
checkmark | enabled | 크기size | 20px |
label | enabled | 글자typography | text-t414px / 19px |
size=large
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 최소 높이minHeight | 36px |
checkmark | enabled | 크기size | 24px |
label | enabled | 글자typography | text-t516px / 22px |
size=medium, shape=square
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
icon | enabled | 아이콘size | 12px |
size=large, shape=square
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
icon | enabled | 아이콘size | 14px |
size=medium, shape=ghost
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
icon | enabled | 아이콘size | 14px |
size=large, shape=ghost
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
icon | enabled | 아이콘size | 18px |
weight=regular
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
label | enabled | 굵기fontWeight | 400 |
weight=bold
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
label | enabled | 굵기fontWeight | 700 |
shape=square, checked=unchecked
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
checkmark | enabled | 배경background | transparent |
| 테두리borderColor | stroke-neutral-solid#767C8B다크 #8389973:1 이상 — v109(SEED 는 옅은 선) | ||
| 테두리 두께borderWidth | 1px | ||
icon | enabled | 글리프glyph | none선택 안 된 칸에는 아이콘이 없다 |
checkmark | hovered | 배경background | bg-layer-default-pressed#F7F8FD다크 #353B4D |
checkmark | pressed | 배경background | bg-layer-default-pressed#F7F8FD다크 #353B4DSEED bg.transparent-pressed 의 불투명 짝 |
checkmark | disabled | 배경background | bg-disabled#F5F6FA다크 #353B4D |
| 테두리borderColor | stroke-neutral-weak#E5E8EF다크 #404757SEED stroke.neutral-muted 의 불투명 짝 |
shape=square, checked=checked
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
checkmark | enabled | 테두리 두께borderWidth | 0px |
checkmark | disabled | 배경background | bg-disabled#F5F6FA다크 #353B4D |
icon | disabled | 색color | fg-disabled#8A91A0다크 #838997 |
shape=square, checked=indeterminate
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
checkmark | enabled | 테두리 두께borderWidth | 0px |
icon | enabled | 글리프glyph | Minuslucide `Minus` · 선 3 |
checkmark | disabled | 배경background | bg-disabled#F5F6FA다크 #353B4D |
icon | disabled | 색color | fg-disabled#8A91A0다크 #838997 |
shape=square, tone=neutral, checked=checked
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
checkmark | enabled | 배경background | bg-neutral-inverted#1A1F2E다크 #F5F6FA |
icon | enabled | 색color | fg-neutral-inverted#FFFFFF다크 #242938 |
checkmark | hovered | 배경background | bg-neutral-inverted-pressed#535866다크 #B7BDCC |
checkmark | pressed | 배경background | bg-neutral-inverted-pressed#535866다크 #B7BDCC |
shape=square, tone=neutral, checked=indeterminate
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
checkmark | enabled | 배경background | bg-neutral-inverted#1A1F2E다크 #F5F6FA |
icon | enabled | 색color | fg-neutral-inverted#FFFFFF다크 #242938 |
checkmark | hovered | 배경background | bg-neutral-inverted-pressed#535866다크 #B7BDCC |
checkmark | pressed | 배경background | bg-neutral-inverted-pressed#535866다크 #B7BDCC |
shape=square, tone=brand, checked=checked
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
checkmark | enabled | 배경background | bg-brand-solidDesk#0147AD다크 #1049A4HR#075B3C다크 #1B583E |
icon | enabled | 색color | static-white#FFFFFF |
checkmark | hovered | 배경background | bg-brand-solid-pressedDesk#013D96다크 #1A5AC2HR#064F33다크 #0C6C49 |
checkmark | pressed | 배경background | bg-brand-solid-pressedDesk#013D96다크 #1A5AC2HR#064F33다크 #0C6C49 |
shape=square, tone=brand, checked=indeterminate
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
checkmark | enabled | 배경background | bg-brand-solidDesk#0147AD다크 #1049A4HR#075B3C다크 #1B583E |
icon | enabled | 색color | static-white#FFFFFF |
checkmark | hovered | 배경background | bg-brand-solid-pressedDesk#013D96다크 #1A5AC2HR#064F33다크 #0C6C49 |
checkmark | pressed | 배경background | bg-brand-solid-pressedDesk#013D96다크 #1A5AC2HR#064F33다크 #0C6C49 |
shape=ghost
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
checkmark | enabled | 배경background | transparent |
| 테두리 두께borderWidth | 0px | ||
icon | enabled | 색color | fg-placeholder#767C8B다크 #838997선택 안 됨도 옅은 체크로 보인다 |
checkmark | hovered | 배경background | bg-layer-default-pressed#F7F8FD다크 #353B4D |
checkmark | pressed | 배경background | bg-layer-default-pressed#F7F8FD다크 #353B4D |
icon | disabled | 색color | fg-disabled#8A91A0다크 #838997 |
shape=ghost, checked=indeterminate
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
icon | enabled | 글리프glyph | Minuslucide `Minus` · 선 3 |
shape=ghost, tone=neutral, checked=checked
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
icon | enabled | 색color | fg-neutral#1A1F2E다크 #F5F6FA |
checkmark | hovered | 배경background | bg-neutral-weak#F5F6FA다크 #353B4D |
checkmark | pressed | 배경background | bg-neutral-weak#F5F6FA다크 #353B4DSEED palette.gray-200 |
icon | disabled | 색color | fg-disabled#8A91A0다크 #838997 |
shape=ghost, tone=neutral, checked=indeterminate
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
icon | enabled | 색color | fg-neutral#1A1F2E다크 #F5F6FA |
checkmark | hovered | 배경background | bg-neutral-weak#F5F6FA다크 #353B4D |
checkmark | pressed | 배경background | bg-neutral-weak#F5F6FA다크 #353B4D |
icon | disabled | 색color | fg-disabled#8A91A0다크 #838997 |
shape=ghost, tone=brand, checked=checked
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
icon | enabled | 색color | fg-brandDesk#0147AD다크 #7AA9F6HR#075B3C다크 #2ABE84 |
checkmark | hovered | 배경background | bg-brand-weak-pressedDesk#D7E5FC다크 #1F3A69HR#D6E8DE다크 #214132 |
checkmark | pressed | 배경background | bg-brand-weak-pressedDesk#D7E5FC다크 #1F3A69HR#D6E8DE다크 #214132SEED palette.carrot-200 자리 — 브랜드 200 |
icon | disabled | 색color | fg-disabled#8A91A0다크 #838997 |
shape=ghost, tone=brand, checked=indeterminate
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
icon | enabled | 색color | fg-brandDesk#0147AD다크 #7AA9F6HR#075B3C다크 #2ABE84 |
checkmark | hovered | 배경background | bg-brand-weak-pressedDesk#D7E5FC다크 #1F3A69HR#D6E8DE다크 #214132 |
checkmark | pressed | 배경background | bg-brand-weak-pressedDesk#D7E5FC다크 #1F3A69HR#D6E8DE다크 #214132 |
icon | disabled | 색color | fg-disabled#8A91A0다크 #838997 |
SEED 와 다른 점
- 선택 안 된 칸의 테두리는
stroke-neutral-solid(3:1 이상, v109). SEED 는 옅은 선(stroke.neutral-weak)이지만 groo 는 칸을 선으로 알아봐야 한다(1.4.11). - 투명한 누름(SEED bg.transparent-pressed) · 비활성 테두리(stroke.neutral-muted)는 불투명한 가장 가까운 역할(
bg-layer-default-pressed·stroke-neutral-weak)로 — 8자리 hex 를 lint 가 거부한다. - Ghost 의 누름 바탕(SEED palette.gray-200 · carrot-200)은 역할 색
bg-neutral-weak·bg-brand-weak-pressed로. - 묶음 줄 사이는 12(사용자 결정) — SEED 는 4 다. 4 면 이웃 줄의 누르는 영역(44)과 겹쳐 한 줄이 실제로 36 · 40 만 받는다. groo 는 기초에서 44 를 반드시로 정했다(Inclusive Design).
- 웹의
hovered·focused를 더한다(v106).focused는 앱의 키보드 포커스에도 그린다(2026-10-08). - 아이콘은 lucide(선 3) — SEED 는 채운 체크 아이콘.
Migration notes
2026-10-22 — Switch 와 견주는 표에 경계 사례
"Checkbox 와 Switch" 표에 경계 사례 한 줄(고정 금액 · 자동 기록 · 상단 고정은 Checkbox, 결제 알림 · 단종 카드 포함 · 일정 종일은 Switch)을 더하고 이 표를 한 벌로 — Switch 문서는 가리킨다(사용자 결정 7B — https://claude.ai/artifact/SmHD31RQxTjWbfK83nbcym). 결정 그대로, 값 변경 없음.
2026-10-08 — Enter · 묶음 상태 · 라벨 안 링크
앱 적용(desk-front #428 ~ #430 · desk-app #407 ~ #410)이 남긴 문제를 사용자가 비교 페이지에서 정했다.
- Enter(B1) — 스펙은 "Enter 는 폼 제출에 둔다" 인데 Radix 는 Enter 를 막기만 해 아무 일도 없었다. 레시피의 칸(
Checkmark)이 Enter 를 받아 폼의 기본 버튼을 누른다 — 진짜 input · SEED 와 같다. 칸은 바뀌지 않고, 고르기는 Space 다.Checkmark를 쓰는 List 의 체크 줄 · Select Box 여럿 고르기도 같이 바뀌었다. - 묶음 상태(9) — Field 의 오류 · 막힘이 묶음에 닿지 않았다(막힌 Field 안의 칸이 눌렸다). 오류는 칸마다
aria-invalid, 막힘은 묶음aria-disabled+ 칸마다disabled로 건다. 필수는 묶음에 걸지 않는다(role="group"은 받지 않는다) — 고르지 않고 내면 오류 글이 알린다.CheckboxGroup에disabled를 더했다. - 라벨 안 링크(C1) — 줄의 누르는 영역이 라벨 안 링크를 덮는다. 레시피는 그대로 두고 Don't 에 적었다 — 링크는 라벨 옆 · 아래에.
2026-09-30 — 묶음 줄 사이 12 · 모션 표
Radio 를 옮기다 드러난 것을 Checkbox 에도 맞췄다(groo-design#143 뒤).
| 옛 | 새 |
|---|---|
| 묶음 줄 사이 4(SEED 값) — 이웃 줄과 44 영역이 겹쳐 한 줄이 36 · 40 만 받았다 | 줄 사이 12(spacing-x3) — 줄마다 44 · 48. 사용자 결정(기초의 "44 를 반드시") |
| 줄 맞춤을 적지 않음 — 레시피는 줄을 묶음 폭으로 늘이고 미리보기는 내용만큼 | 줄은 칸 + 라벨만큼만(alignSelf: flex-start), 네 곳 모두 |
| 모션 표: 아이콘 scale 0.8 → 1 · 불투명도(옛 스펙에서 온 값 — 레시피 · 미리보기 어디에도 구현된 적이 없다) | 색 전환(채움 · 테두리 · 아이콘 색) + 누름 축소 — 구현 그대로이고 SEED Checkmark 와 같다 |
| 칸(버튼) 위 커서 기본 화살표 | 손가락(cursor-pointer) — 라벨 위와 같게 |
2026-09-30 — SEED Checkbox 구조로
사용자가 비교 페이지(https://claude.ai/artifact/SERp881jg537tG3nMxeUEB)에서 정했다 — 틀은 SEED(Checkmark · Checkbox · Checkbox Group, 크기 medium 20 · large 24 + 라벨 14 · 16 + 줄 32 · 36) · 선택 색은 neutral 기본 + brand 선택(Radio · Switch 도 같은 규칙) · 모양은 Square + Ghost(할 일 동그라미는 할 일 목록 차례에) · 오류는 묶음 아래 글만.
| 옛 | 새 |
|---|---|
sm(16) · md(18, 기본) · lg(20) | medium(20, 기본) · large(24) |
선택 = primary(브랜드) 채움 | 선택 = bg-neutral-inverted(짙은 회색), tone="brand" 면 bg-brand-solid |
선택 안 됨 테두리 border-strong · 바탕 surface-default · 호버 surface-input | 테두리 stroke-neutral-solid · 바탕 투명 · 호버 = 누름 bg-layer-default-pressed |
| 비활성 50% 흐림 | bg-disabled · fg-disabled(v106) |
오류 aria-invalid 빨간 테두리 | 없음 — 묶음 아래 안내 글 |
라벨은 쓰는 쪽이(label-md 14/500 · gap-2) | Checkbox 가 라벨까지 — t4 14/400(굵게는 weight="bold" 700) |
칸 컴포넌트 이름 Checkbox | 칸은 Checkmark, Checkbox 는 칸 + 라벨 |
제품은 앱 적용 단계에서 옮긴다 — Desk 웹 공용 Checkbox 6곳 · 앱 PCheckbox 5곳 · HR 웹 shadcn 기본 체크박스 19곳. Desk 의 손으로 그린 흉내(더치페이 참여자 · "나도 포함" · 캘린더 보이기 — 캘린더는 칸 색이 캘린더마다 제 색이라 캘린더 차례에)와 HR 의 일부 선택 없음(권한 "전체" 가 빈칸에 체크)도 그때.
다른 컴포넌트 — Table 의 선택 열(table.md ⓙ · table-examples.mjs)은 칸(Checkmark)을 쓰고 행 전체가 누르는 영역이 된다. Table 차례에 옮긴다. 메뉴의 체크 항목(Dropdown · Context · Menubar 의 CheckboxItem)은 이 컴포넌트가 아니다.
2026-09-29 — 수치를 YAML 로
수치 표를 checkbox.yaml 로 옮겼다(값은 그대로). 이전 md 는 checkbox.history/v-pre-yaml-numbers.md.
Chart
숫자의 흐름 · 비율 · 세기를 그림으로 보이는 차트 — 수입 · 지출 추이(선), 기간별 합(막대), 카테고리 비율(도넛), 요일 × 시간 세기(열지도). 차트는 카드 본문 안에 두고, 같은 숫자를 범례 · 툴팁의 글로도 읽을 수 있게 한다. 숫자 하나는 Card 의 지표 카드, 줄과 열은 Table 이다.
Chip
고르거나 넣은 값을 보이는 작은 알약. 2 ~ 4개 짧은 폼 값 고르기 · 누르면 값을 채워 주는 제안 · 목록 위 필터 바 · 넣은 값(지우기 버튼으로 뺀다)을 맡는다. 5개 이상은 Select, 글이 긴 2 ~ 4개는 Radio · Checkbox, 다른 구역으로 옮기는 탭 · 같은 내용을 바로 거르는 2 ~ 4개는 Tabs · Segmented Control(그 차례에), 누를 수 없는 표시는 Badge · Tag Group 이다.