Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions skills/a11y-spec/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
---
name: a11y-spec
description: >
컴포넌트 구현 전 접근성 명세. WCAG 2.2 A/AA와 KWCAG 전수에서 적용 성공 기준(SC)을 걸러
책임을 나누고 요구사항 문서 한 부로 낸다. "Dialog 만들기 전에 접근성 뭐 지켜야 해"처럼
구현 전 설계 입력을 요구할 때 쓴다.
---

# a11y-spec

컴포넌트를 짓기 전에 **무엇을 지켜야 하는지**를 규범 근거와 함께 나열한다. 산출물은 설계
입력이다 — 코드가 아직 없으므로 채점할 대상도 없다.

SC는 컴포넌트가 아니라 콘텐츠 성질에 걸린다. 그래서 필터는 **이름 → 성질 → SC 두 홉**으로
간다. 중간 홉이 성질이라 처음 보는 이름(`Coachmark`, `SegmentedControl`)도 처리된다.

산출물은 `reports/<component>.md` **한 부**다.

## 용어

한 말이 두 뜻으로 쓰이면 문서가 읽히지 않는다. 아래 어휘로 쓴다.

| 말 | 뜻 |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| **기반 구현체** | base-ui 또는 브라우저 네이티브 요소. 이 둘이다 |
| **증거** | 위임을 뒷받침하는 것. **관측**(실측) 또는 **명세**, 둘뿐이다 |
| **전수** | 빠짐없이 판정하기. `sc-map.md` 60행, `properties.md` 23개가 대상이다 |
| **APG 패턴** | W3C 위젯 유형. 「패턴」에 접두사 `APG`를 붙여 쓴다 |
| **패턴** | 접두사 없는 「패턴」은 루트 `CONTEXT.md` 2절 뜻(디자인 시스템의 설계 규칙)이다 |
| **위젯 계약** | 이 위젯이 AT에게 약속하는 것. role·상태·속성·키·포커스·이름 6칸. 5단계 `위임`은 그 계약을 기반 구현체가 이미 이행한다는 판정이다 |
| **표기법 / 파트 구성** | base-ui가 제공하는 방식. `CONTEXT.md` 1절 어휘(조합형·flat·파트) |
| **소비자** | vapor를 가져다 쓰는 개발자 |
| **최종 사용자** | 스크린리더·키보드로 화면을 쓰는 사람 |
| **정찰** | 문서·API 레퍼런스만 읽기 |
| **실측** | 브라우저로 데모를 조작해 결과를 관측하기 |
| **명세 확인** | HTML-AAM·MDN·WAI-ARIA 명세 읽기 |

책임 구획의 이름은 `vapor 책임`이다.

## 절차

### 0. 정찰 — base-ui 문서

`https://base-ui.com/react/components/<컴포넌트>` (kebab-case)와 그 API 레퍼런스를 읽는다.
네 항목을 채우면 끝이다.

1. **해당 컴포넌트 페이지가 있나** — 없으면 기반 구현체는 네이티브 요소이거나 없다
2. **표기법과 파트 구성** — 조합형이냐 flat이냐, 파트 목록은 무엇이냐
3. **네이티브 요소 사용 여부** — 내부적으로 `<button>`·`<dialog>`·`<input>`을 쓰나
4. **판정을 뒤집을 수 있는 prop** — 목록으로 뽑는다

4번이 5단계 실측 계획의 입력이다. prop 조합 하나가 SC 판정을 뒤집는 실례는 `references/verify.md`
§ 실측 예에 있다. 3번도 값이 싸다 — base-ui Dialog가 네이티브 `<dialog>`를 쓰지 않는다는 사실이
여러 SC 판정의 전제였다.

### 1. 성질 — 성질만 본다

`references/properties.md`(성질 어휘 23개, 판별 질문, 추론 규칙)를 읽고 컴포넌트 이름에서 태그를
뽑는다. **23개 전수**에 붙음/안 붙음이 붙고 각각 근거 한 줄이 달리면 끝이다.

**이 단계에서는 `properties.md`만 읽는다.** 2단계의 APG 표를 먼저 읽으면 APG에 없는 **시각 축**
태그(`ui-boundary`·`text`·`state-visual`·`pointer-target`)를 빼먹는다. 단계 경계가 그 오염을
막는 장치다.

성질이 명백히 갈리는 이름은 **여기서 즉시 묻는다** — `Menu`가 내비게이션 메뉴냐 액션 메뉴냐에
따라 2단계에서 조회할 APG 패턴 자체가 바뀐다(Menubar / Menu). 나머지 확인은 2단계 게이트로
모은다.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

### 2. 위젯 계약 + 게이트

APG 패턴(`https://www.w3.org/WAI/ARIA/apg/patterns/`)과 MDN ARIA role 문서로 6칸을 채운다.
칸마다 내용이 있거나 "없음 + 사유"가 붙으면 끝이다.

| 칸 | 담는 것 |
| ---------------- | -------------------------------------------- |
| role | 각 파트에 붙는 역할 |
| 상태 | `aria-expanded` `aria-checked` 등 동적 속성 |
| 속성 | `aria-controls` `aria-valuenow` 등 정적 속성 |
| 키 인터랙션 | 키와 그 결과. APG 키보드 표 전수 |
| 포커스 관리 규칙 | 진입 지점·복귀·roving tabindex |
| 접근 가능 이름 | 이름이 어디서 오나(자식 텍스트·`aria-label`) |

**APG 패턴이 없으면**(`card` `badge` `skeleton` `spinner` `text` `callout` `floating-bar`)
"APG 패턴 없음"을 명시하고, **role이 있으면 MDN ARIA role 문서로 채운다** — MDN role 문서에는
APG에 없는 필수 속성 목록이 있다. `spinner`에서 그것이 빠지면 4.1.2 요구사항이 "role만 주면
된다"로 좁아진다.

패턴이 없는 컴포넌트는 성질만으로 진행한다. **비슷한 APG 패턴을 빌리면 유추가 근거 자리에 앉는다**
— `spinner`에 Progress 패턴을 씌우면 없는 `aria-valuemin`을 요구사항으로 강제한다.

#### 게이트 — base-ui에 없을 때

vapor 컴포넌트 42개 중 base-ui 대응이 없는 것이 22개다. 그 경우 **네이티브 요소로 구현하느냐
커스텀으로 구현하느냐**를 추천한다. 근거는 하나다 — **네이티브 요소가 위 6칸 중 몇 칸을 채우나.**
HTML-AAM과 MDN을 그때 조회한다.

- `breadcrumb` — APG 패턴이 있는데도 `<nav><ol><a>`가 role·구조·키보드를 거의 다 준다 → 네이티브
- `segmented-control` — `<input type=radio>`가 role·상태·화살표 이동을 다 준다 → 네이티브
- `multi-select` — `<select multiple>`이 있지만 요구하는 상호작용이 다르다 → 커스텀

**이 단계 끝에서 사용자 확인을 한 번 받는다.** 1단계 성질 결과와 위 추천을 함께 보인다. base-ui가
있으면 성질만 보인다. 게이트 확인은 이 한 번이다 — 1단계의 분기 질문(성질이 명백히 갈리는
이름)만 별개의 예외다.

### 3. SC 필터 — 트리거는 성질이다

`references/sc-map.md`(SC 전수 → 성질 역매핑 표. 이 스킬의 본체)를 훑고, 트리거 열의 태그가
1단계 결과와 하나라도 겹치는 행을 채택한다. 트리거가 `전체`인 행은 무조건 채택한다.

**60행 전수를 판정한다.** 채택이든 제외든 행마다 결과가 있고, 판정하지 않은 행이 0이면 끝이다.
채택 수와 제외 수는 6단계 검산에 들어간다.

위젯 계약은 트리거가 아니다 — 성질 누락 교차검증과 요구사항 문장의 출처로 쓴다. 계약을
트리거로 쓰면 **시각 축**이 통째로 사라진다(APG에도 base-ui에도 없다. 목록은 `sc-map.md`가
소유한다).

표에 있는 행만 쓴다. 표가 WCAG 2.2 A/AA와 KWCAG 전수라서, 표에 없는 SC는 A/AA가 아니거나
존재하지 않는다.

### 4. 책임 4구획

채택한 각 SC를 네 구획으로 나눈다. 채택 SC 전부에 구획이 붙으면 끝이다.

| 구획 | 담는 것 |
| -------------------- | ------------------------------------------ |
| **기반 구현체 위임** | base-ui 또는 네이티브 요소가 보장한다 |
| **vapor 자체 구현** | vapor가 끝까지 보장한다 |
| **공동(통로)** | vapor가 통로를 뚫고 소비자가 내용을 채운다 |
| **소비자** | 사용처가 보장한다 |

`sc-map.md`의 책임 열은 `컴포넌트 / 공동 / 소비자` 셋이다. **`컴포넌트`가 여기서 두 갈래로
갈리고**, 어느 쪽인지는 5단계 증거가 정한다.

`공동(통로)`은 **"어떤 prop·파트가 필요한가"로 번역해서** 적는다. 이 구획이 곧 prop 설계
목록이다.

요건은 규범 수준까지 적는다. "3:1 이상"까지가 이 스킬이고, 어떤 토큰을 쓸지는 디자인 결정이다.

### 5. 증거 수집 — 실측 또는 명세 확인

`references/verify.md`를 따른다. 기반 구현체가 base-ui면 실측하고, 네이티브 요소면 명세를
확인한다. `기반 구현체 위임`과 `vapor 자체 구현` 구획의 SC 전부에 판정 어휘가 붙으면 끝이다.

**위임은 증거를 요구한다.** 증거가 있는 SC만 위임으로 적고, 나머지는 `확인 불가`로 남긴다.

### 6. 문서 한 부

`references/report-template.md`의 구조를 그대로 따라 `reports/<component>.md`에 쓴다. 모든
컴포넌트의 문서가 같은 골격이어야 서로 비교되고, 빠진 칸이 눈에 띈다.

본문은 요구사항 표만 담는다. 부록이 근거와 판단을 담는다 — ① 성질 → ② 위젯 계약 → ③ 제외
목록 → ④ 설계 결정.

헤더의 **검산 3줄이 맞으면 끝이다.** 식과 각 줄이 무엇을 잡는지는 `report-template.md`가
소유한다.

골격을 바꿔야 할 이유가 생기면 템플릿을 고치고 기존 문서도 맞춘다.
63 changes: 63 additions & 0 deletions skills/a11y-spec/evals/dialog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# eval: Dialog

표가 깨지면 실패하는 최소 검증. 스킬을 `Dialog`로 돌리고 아래와 대조한다.

## 입력

> Dialog 만들려고 하는데 접근성 기준 뽑아줘

## 기대 0단계 정찰 (4항목)

1. base-ui에 `dialog` 페이지 **있음** → 기반 구현체 = base-ui. **게이트 없음**(네이티브/커스텀
추천을 하지 않는다)
2. 표기법 조합형, 파트 다수(`Root` `Trigger` `Portal` `Backdrop` `Popup` `Title` `Description`
`Close`)
3. 네이티브 `<dialog>` **미사용**
4. 판정을 뒤집을 prop: `modal` (`true` / `"trap-focus"` / `false`)

4번이 빠지면 실측 계획이 기본값 하나로 좁아진다.

## 기대 성질 (8개)

`interactive` `modal` `overlay` `ui-boundary` `text` `pointer-target` `visible-label` `icon`

`composite` 아님 — Dialog 자체는 자식 항목을 roving tabindex로 관리하지 않는다.
`transient` 아님 — hover로 뜨고 지지 않는다.
`status-message` 아님 — Dialog는 포커스를 옮긴다.

## 기대 SC (채택 27 / 제외 33 / 합 60)

WCAG 24: 1.1.1, 1.3.1, 1.3.2, 1.3.3, 1.3.4, 1.4.3, 1.4.4, 1.4.5, 1.4.10, 1.4.11, 1.4.12,
2.1.1, 2.1.2, 2.4.3, 2.4.6, 2.4.7, 2.4.11, 2.5.2, 2.5.3, 2.5.8, 3.1.2, 3.2.1, 3.2.4, 4.1.2

KWCAG 고유 3: 5.4.4, 8.1.1, 8.2.1

## 회귀 신호

- **2.4.11이 빠지면** → `overlay`/`modal` 트리거 소실. Dialog가 포커스된 배경 요소를 가리는
문제를 못 잡는다
- **1.4.13이 들어오면** → `transient` 오추론. Dialog는 호버 콘텐츠가 아니다
- **4.1.2에 KWCAG 8.2.1이 안 붙으면** → KWCAG 대응 열 파손
- **부록 C에 2.4.1·2.4.2·3.1.1이 없으면** → 페이지 수준 제외 사유가 문서에서 누락
- **검산 셋째 줄이 `60 = 27 + 33`이 아니면** → 필터가 전수를 돌지 않았다. 앞 두 줄은 이 사고를
못 잡는다(이미 실린 것만 세므로)
- **부록 C 행수가 33이 아니면** → 검산 셋째 줄과 부록의 대조가 깨졌다
- **시각 축(1.4.3 / 1.4.4 / 1.4.10 / 1.4.11 / 1.4.12 / 2.4.7 / 2.5.8 / 5.4.4)의 컴포넌트 몫이
`1. 기반 구현체 위임`에 들어가면** → base-ui는 unstyled다. 컴포넌트 몫은 전부 `2. vapor 자체
구현`이어야 한다(1.4.3은 `공동`이라 소비자 행이 짝으로 따로 있다)
- **위젯 계약이 SC 필터의 트리거로 쓰이면** → 위 시각 축 8건이 사라진다. 트리거는 성질뿐이다
- **부록 B의 슬러그 역참조 칸이 비어 있고 사유도 없으면** → 정리했는데 문서에 안 실린 행위가 있다
- **산출물이 두 파일이면** → `reports/dialog.md` 한 부만 나와야 한다

## base-ui 실측 (2026-08-20 완료)

기본 `modal` 설정에서 4.1.2·2.4.3·2.1.2 충족 확인. 단 수단은 `aria-modal`도 네이티브
`<dialog>`도 아닌 배경 `aria-hidden`이다 — 문서에는 `지원 (다른 방식)`으로 적고 무엇으로
대신했는지 붙인다. 1.3.2는 배경 소멸로 판정하지 않는다 — 팝업 내부 순서(Title → Description →
본문 → 컨트롤)가 접근성 트리에서 읽기 순서와 일치하는지로 판정한다. 전문과 명령은
`references/verify.md` § 경로 1 실측 예 참조.

`modal` 세 값(`true` / `"trap-focus"` / `false`)도 실측 완료. `trap-focus`가 `true`와 갈리는
지점은 **`body` 스크롤 락 하나뿐**이고, `Dialog.Backdrop`은 세 값 전부에서 렌더된다 — 2026-08-14
판의 "`trap-focus`는 백드롭을 끈다"는 반증됐다. 따라서 AT 사용자에게만 배경이 숨겨지는 괴리는
prop이 만들지 않고 **소비자가 Backdrop을 안 그릴 때** 생긴다. 부록 D의 열린 결정으로 남긴다.
Loading
Loading