본문으로 건너뛰기
Centralby 디자인 시스템

원칙

접근성 기준

대비, 키보드와 초점, 44px, forced-colors, 동작 줄이기, lang, 라이브 리전 정책, 그리고 출시를 막는 시험 게이트.

기준선

미검증 칸이 하나라도 있으면 출시하지 않는다.

기준선
항목기준
대비본문 4.5:1, 24px 이상·UI·초점 3:1. 반투명·그라디언트는 최악 합성 지점 기준. 빛 위 muted 글자 금지
초점:focus-visible에 outline 2px + offset 2px, 인접한 모든 면 대비 3:1. box-shadow 링 금지
조작 영역모든 대화형 요소 44×44 이상, 320px 가로 넘침 0, 400% 확대
동작reduce에서 transform none, 모든 duration ≤120ms(토큰 단계)
강제 색상outline·테두리·상태 표식이 남는다. 빛·블러·그림자는 꺼진다
언어서버가 <html lang> 설정(ko · en · zh-Hans · es). 사용자 원문은 감지한 lang 또는 dir="auto"
이름과 상태모든 컴포넌트 meta에 이름 규칙·키보드 표·4개 언어

네이티브 우선

ARIA는 네이티브로 할 수 없을 때만 쓴다.

요소 선택
필요1순위쓰지 않는 것
URL이 바뀌는 이동<a href>button + router.push
이 화면에서의 행동<button type="button">div onClick, role="button"
짧은 목록 단일 선택<select>커스텀 listbox
상호배타 2–5개<fieldset> + radiodiv role=radiogroup
접고 펴기<details><summary>aria-expanded 버튼 + div
모달<dialog> + showModal()role=dialog div + 수동 초점 가둠
비모달 떠 있는 패널popover="auto"포털 + 수동 바깥 클릭
진행률<progress>role=progressbar div
날짜·시각<time datetime>텍스트만
다열 비교<table> + <caption> + <th scope>CSS grid div
  • 이렇게 하지 않기선택은 메뉴가 아니다. 값을 고르는 목록(범위, 언어, 정렬)에 role=menu를 쓰지 않는다.
  • 이렇게 하지 않기양수 tabindex 금지. 링크 안에 대화형 요소를 넣지 않는다(카드 전체가 링크면 안쪽 행동 0개).

키보드와 초점

초점은 사용자 행동이 있을 때만 움직인다. 예외는 아래 표뿐이다. 복합 위젯(Tabs, SegmentedControl, Menu, 달력 격자, ReorderList)은 Tab 정지점이 하나다(roving tabindex). 초점 순서 = 시각 순서.

초점 이동
사건초점이 가는 곳
Modal·Sheet·Drawer 열림initialFocus, 없으면 제목(tabindex=-1). ConfirmDialog는 취소
오버레이 닫힘호출한 요소. 사라졌으면 다음 형제 → 이전 형제 → 컨테이너 제목
Menu·Popover 열림 / 닫힘첫 활성 항목 / 트리거
폼 제출 실패첫 오류 필드(3개 이상이면 오류 요약)
상세 열림 / 닫힘상세 h1 / 같은 id의 행 → 다음 행 → 이전 행 → 목록 제목
NewItemsChip 누름맨 위로 스크롤, 첫 새 행
Google OAuth 취소 후 복귀같은 ProviderButton

Esc 순서와 오버레이 층

  • Esc는 가장 위의 것 하나만 닫는다: Tooltip → Menu 하위 → Menu/Popover → Combobox 패널 → ConfirmDialog → Modal/Sheet/Drawer. Toast는 Esc로 닫지 않는다.
  • 모달 깊이는 최대 2(모달 + 확인 1). 모달이 열리면 열린 Popover·Menu·Tooltip은 닫힌다.
  • 저장하지 않은 입력이 있으면 Esc·배경 클릭이 “변경 사항 버리기?”를 연다.
:focus-visible {
  outline: var(--cb-focus-width) solid var(--cb-focus);
  outline-offset: var(--cb-focus-offset);
}
/* sticky chrome never hides focus (WCAG 2.4.11) */
.scroller { scroll-padding-top: calc(var(--topbar-h) + var(--cb-space-2)); }

44px · 320px · 확대

  • 보이는 크기가 44보다 작으면 ::after로 조작 영역을 넓힌다. 넓힌 영역끼리 겹치지 않고 보이는 간격은 8 이상.
  • 320px 폭, 브라우저 200·400% 확대에서 가로 넘침 0. 확대로 split이 push로 바뀌어도 초점은 같은 요소에 남는다.
  • 글자 간격 주입(WCAG 1.4.12) 뒤에도 잘림 0. 버튼은 es에서 2줄까지 허용하고 말줄임하지 않는다.

forced-colors

  • 이렇게채운 컨트롤·캡슐·카드·위젯에는 투명 1px border를 둬서 경계가 남게 한다.
  • 이렇게선택·현재 상태는 배경색이 아닌 표식(체크, 밑줄, 테두리 굵기, aria-current)으로도 보인다.
  • 이렇게시스템 색: 비활성 GrayText, 링크 LinkText, 선택 Highlight/HighlightText, 검색 일치 Mark/MarkText. 아이콘은 currentColor.
  • 이렇게 하지 않기forced-color-adjust: none은 서비스 로고와 차트 표식에만, 텍스트 라벨이 있을 때만.

forced-colors는 페이지 안에서 흉내 낼 수 없어 카탈로그에 토글이 없다. e2e에서 forcedColors: "active" 에뮬레이션으로 검사한다.

라이브 리전 정책

  1. 앱 셸에 리전 두 개를 첫 페인트부터 비워 둔다: polite(role=status)와 assertive(role=alert). 컴포넌트는 aria-live를 직접 만들지 않고 announce()를 호출한다. 예외는 Toast 영역뿐.
  2. 상태 변화만 알린다. 내용은 알리지 않는다(행 추가, 스트리밍 결과, 타이핑 중 결과 수 변화).
  3. 진행 알림은 10초에 최대 1회(가져오기: 시작 · 첫 결과 · 일부 실패 · 완료).
  4. 검색 결과 수는 입력이 멈추고 500ms 뒤 한 번.
  5. 같은 문장이 2초 안에 다시 오면 버린다. 초점 이동으로 이미 읽히는 것은 알리지 않는다.
  6. assertive는 사용자 행동의 결과가 실패했을 때만. 백그라운드 실패는 polite.
  7. 실행 취소가 가능한 결과는 그 가능성을 문장에 넣는다(“지금은 접어 둠. 실행 취소 가능”).

실제 컴포넌트: announce() — 공유 리전 하나로 말하기

카탈로그 셸에 LiveAnnouncer가 한 번 있다. 컴포넌트·페이지는 aria-live를 만들지 않고 announce()를 부른다(lint R9). 같은 문장이 2초 안에 다시 오면 버린다.

lang과 언어

  • <html lang>은 서버가 centralby_locale 쿠키로 설정한다: ko · en · zh-Hans · es.
  • 사용자 원문은 번역하지 않는다. 출처 메타데이터에 언어가 있으면 lang, 없으면 dir="auto"만. 언어를 추정하지 않는다.
  • LanguageSwitcher의 각 항목은 자기 언어의 lang을 가진다.
  • 날짜·숫자·상대 시간은 Intl.*에 UI 로캘을 넘겨 만든다. 문자열을 이어 붙이지 않는다.
  • 이 가이드도 같은 규칙을 따른다: zh·es 화면에서 영어 본문으로 대체될 때는 그 요소에 lang="en"을 단다.

시험 게이트

자동: test:design (PR마다, DB 없음)

  • semantic.css 전 쌍을 본문 4.5 / 24px 이상 3 / 비텍스트 3으로. 반투명과 빛은 최악 합성으로.
  • 카탈로그 각 페이지 × 라이트·다크 × ko·es × 320·1280: axe wcag2a–22aa 위반 0, 44px, 넘침 0, Tab 순회 outline ≥2px·대비 ≥3, 1.4.12 간격, reduce, forced-colors, html[lang].
  • 정적 검사: design:lint(R1 색 리터럴 … R9 라이브 리전), design:check(registry·index·폴더 일치, 테마 블록 쌍).
매트릭스 열 (Priya A–I)
열통과 조건
A네이티브 요소
B계약 키 전부 동작
C초점 보임, 순서, 복귀
D스크린리더 3종에서 이름·역할·상태
E대비
F44px · 320 · 400%
Greduce에서 의미 유지
Hforced-colors에서 경계·초점·상태
Ies 2줄 이내, lang

출시 전 수동

  • VoiceOver·NVDA·TalkBack으로 가입 경로와 첫 화면
  • 실제 200·400% 확대, Windows 고대비, 맑은 고딕 기본 환경, 키보드 전용, 음성 제어

현재 상태(2026-10-21): 실제 보조기기 검증 0건. 320px·400%·forced-colors·스크린리더·es 최장 라벨은 FINISH-PLAN E 단계에서 측정한다.

근거 문서: round-2/priya.md · DIRECTION.md §3 · contracts/README.md §1 §2 §6–§10 §13 · foundation/foundation.md §4.3 §5 · scripts/design-lint.mjs