접근성
컴포넌트가 이미 보장하는 것과 직접 챙겨야 하는 것을 정한다.
명도 대비
기준은 WCAG 2.1 AA 인 4.5:1 이다. 비활성은 요구에서 빠지지만 2.0:1 아래로는 두지 않는다. 아래는 계산값이 아니라 실제 렌더된 화면에서 라이트·다크 두 테마로 잰 값이다.
| 대상 | 라이트 | 다크 | 기준 |
|---|---|---|---|
| 기본 버튼 라벨 | 16.17:1 | 15.21:1 | 4.5 |
| primary 버튼 라벨 | 5.30:1 | 5.30:1 | 4.5 |
| 비활성 버튼 라벨 | 2.96:1 | 3.12:1 | 2.0 (하한) |
| 입력 글자 | 16.17:1 | 15.21:1 | 4.5 |
| 에러 메시지 | 5.11:1 | 8.00:1 | 4.5 |
| Tooltip · Toast 글자 | 16.49:1 | 16.24:1 | 4.5 |
npm run verify:a11y 가 브라우저에서 두 테마의 값을 다시 재고 기준에 미달하면 실패시킨다. 터치 영역도 같이 잰다. 박스가 아니라 실제로 눌리는 범위를 재고, 24px 미만은 실패, 이유 없는 44px 미만은 경고다.
색만으로 정보를 전달하지 않는다. 에러는 빨간 테두리에 더해 아이콘과 텍스트를 함께 준다. 색각 이상 사용자에게 빨강과 초록은 구분되지 않는다.
터치 영역
| 기준 | 크기 |
|---|---|
| 권장 | 44 × 44px --nui-size-control-option |
| 디자인 제약으로 44px 이 어려울 때의 하한 | 24 × 24px |
보이는 크기와 누를 수 있는 크기는 다르다. 아이콘이 16px 이어도 누르는 범위는 따로 넓힐 수 있다. 가상요소로 가운데 정렬된 44px 정사각형을 깔면 모양은 그대로인 채 히트만 커진다.
| 자리 | 보이는 크기 | 누르는 크기 | 왜 |
|---|---|---|---|
| Popup 닫기 | 40px | 44px | 가상요소로 넓혔다 |
| Datepicker 이전/다음 | 32px | 44px | 가상요소로 넓히고 두 버튼 사이를 12px 로 뒀다. 4px 이면 두 히트가 겹친다 |
| Datepicker 날짜 | 36px | 44px | 격자라 셀이 히트의 상한이다. 셀을 44px 로 두고 버튼의 히트가 셀을 채운다. --nui-datepicker--day-size 로 줄일 수 있다 |
| Textfield 지우기 · Password 토글 · Search · 달력 열기 · Select 화살표·지우기 | 24px | 24px (하한) | 입력 안에 버튼 둘이 8px 로 붙는다. 44 를 채우면 서로 겹쳐 38 이 상한이라 하한을 쓴다 |
| MultiSelect 칩 × | 28×32px | 28×32px (하한) | 칩이 서로 붙어 있어 44 를 채우면 이웃 칩의 히트를 삼킨다 |
포커스
키보드로 이동할 때 지금 위치를 알리는 표시다. :focus-visible 로만 그려서 마우스로 눌렀을 때는 나타나지 않는다.
크기는 컨트롤 높이로 고른다
작은 컨트롤일수록 같은 굵기의 링이 더 두꺼워 보이기 때문이다.
| 컨트롤 | 토큰 | 쓰는 곳 |
|---|---|---|
| 36px 이하 | --nui-focus-ring-sm | 달력 날짜 칸(36), 년/월 셀렉트(36) |
| 36px 초과 | --nui-focus-ring | Textfield · Select · Datepicker 입력 |
| 선택 컨트롤 | --nui-focus-ring-strong | Checkbox · Radio · Switch. 작지만 강조가 필요하다 |
| error | --nui-focus-ring-error | 크기 규칙과 무관하게 에러면 이것 |
| 토큰 | 값 | 비고 |
|---|---|---|
| --nui-focus-width | 2px | — |
| --nui-focus-offset | 0.25rem | — |
| --nui-focus-ring | 0 0 0 0.25rem var(--nui-color-brand-a5) | — |
| --nui-focus-ring-sm | 0 0 0 0.1875rem var(--nui-color-brand-a5) | — |
| --nui-focus-ring-strong | 0 0 0 0.25rem var(--nui-color-brand-a6) | — |
| --nui-focus-ring-error | 0 0 0 0.25rem var(--nui-color-danger-a5) | — |
수단은 테두리가 있느냐로 갈린다
테두리를 가진 것(입력 컨트롤 · 선택 컨트롤 · 날짜 칸)은 테두리를 포커스 색으로 바꾸고 위 표의 링을 더한다. 테두리 없이 누르는 것(Button · IconButton · 닫기 버튼 · Accordion 헤더)은 outline 하나만 그린다. 링을 겹치지 않는다. offset 은 --nui-focus-offset 토큰이다.
--nui-focus-width 에는 컴포넌트별 변수를 두지 않았다. 치수인데도 막은 유일한 값이다. 얇아지면 키보드로 쓰는 사람만 영향을 받고 마우스로 확인하는 사람은 알아채지 못한다. 색과 같은 성격이다.outline: none 을 쓸 때는 대체 표시를 반드시 준다. 포커스 표시를 없애면 키보드 사용자는 자기 위치를 잃는다.
모션 줄이기
OS 에서 동작 줄이기를 켜면 prefers-reduced-motion: reduce 가 전달되고 모든 --nui-duration-* 이 1ms 가 된다.
transition: 0.2s 라고 쓰면 설정을 켠 사용자에게도 그대로 움직인다. 자세한 것은 모션 문서에 있다.2초 넘게 이어지는 애니메이션은 두지 않는다. 로딩 표시처럼 불가피하면 멈출 수 있어야 한다. 초당 3회 넘게 점멸하지 않는다 — 광과민성 발작 기준이다.
메시지가 나타났다는 사실을 알린다
aria-describedby 는 포커스가 그 컨트롤로 갔을 때 읽힌다. 폼 검증 실패처럼 사용자 조작 없이 메시지가 생기는 경우에는 포커스가 이동하지 않아 스크린리더가 침묵한다.
그래서 에러 메시지 컨테이너에 aria-live="polite" 를 함께 건다. 둘 다 필요하다. describedby 는 이 필드의 설명이고 live 는 지금 바뀌었다는 신호다.
<button> 안의 live 영역은 보조기술이 "live 갱신"이 아니라 버튼 이름의 변경으로 처리해 대체로 무시한다. 그래서 Button 의 로딩 안내는 화면 밖 공용 role="status" 영역에 놓인다. 그 영역은 문구가 생기기 전에 문서에 있어야 읽힌다.완료를 함부로 알리지 않는다. 로딩이 끝난 것이 성공인지 실패인지 컴포넌트는 모른다 — 결과 알림은
Toast 나 에러 메시지 몫이다.이 영역은 메시지가 없어도 DOM 에 있다. 내용과 함께 새로 끼워 넣은 live 영역은 읽히지 않는 경우가 많아서다. 빈 상태는 시각적으로만 숨긴다.
자동 완성
개인정보를 받는 입력에는 autoComplete 로 용도를 지정한다 — 이름 · 이메일 · 전화 · 주소 · 생년월일. WCAG 1.3.5(Identify Input Purpose)와 KRDS 가 요구하는 것이고, 손 떨림 · 인지 장애 · 모바일 사용자에게는 실질적인 입력 보조다.
Textfield 와 Textarea 가 autoComplete="off" 를 기본으로 넣고 있었고 뺐다. 소비자가 알아채려면 라이브러리 소스를 읽어야 하는 종류의 기본값이었다. 끄는 것은 소비자가 명시한다.이미 보장하는 것
| 항목 | 어떻게 |
|---|---|
| 라벨과 입력의 연결 | Field 가 useId() 로 만든 id 를 라벨과 컨트롤이 함께 쓴다 |
| 설명·에러 연결 | 여러 개여도 aria-describedby 로 중복 없이 합친다 |
| 에러 표시 | aria-invalid + 아이콘 + 텍스트 + aria-live |
| 아이콘 전용 버튼 | sr-only 텍스트로 접근 이름을 준다 |
| 그룹 컨트롤 | fieldset + legend 또는 role="group" |
직접 챙겨야 하는 것
IconButton에aria-label을 준다. 없으면 스크린리더가 읽을 것이 없다- placeholder 를 라벨 대신 쓰지 않는다. 입력을 시작하면 사라진다
- 라이브러리가 정한 기본 문자열은 prop 으로 바꾼다. 지우기 버튼(
clearButtonTitle), Password 토글, Accordion 토글(toggleLabel), Popup 닫기, Datepicker 의 요일 이름, Confirm 의 버튼 문구가 여기 해당한다. 다국어 앱이면 교체한다 - 번역하면 라벨이 최대 2.5배까지 늘어난다. 자기 치수를 갖는 컨트롤(Switch · Checkbox) 옆의 긴 라벨이 컨트롤을 찌그러뜨리지 않는지 확인한다