접근성을 컴포넌트마다 따로 챙기면 어딘가는 반드시 빠집니다. 대신 토큰과 기본 스타일에 접근성을 처음부터 심어 두면, 그 시스템으로 만든 화면은 손대지 않아도 자연히 접근성을 갖추게 됩니다. 그 내장 방식을 코드로 보여 드리겠습니다.
43.1 포커스링을 토큰으로 내장
키보드로 이동하는 사람에게 지금 어디에 있는지 알려 주는 포커스링은, 컴포넌트마다 다시 짤 게 아니라 한 번에 전부에 씌워야 합니다. 토큰으로 정하고 낮은 우선순위로 깔아 둡니다.
:root {
--focus-ring: 2px solid #2563eb;
--focus-offset: 2px;
}
:where(a, button, input, select, [tabindex]):focus-visible {
outline: var(--focus-ring);
outline-offset: var(--focus-offset);
}
:where로 감싸 우선순위를 0으로 낮췄기에 어느 컴포넌트든 필요하면 손쉽게 덮어쓸 수 있고, 그러지 않으면 이 기본 포커스링이 그대로 적용됩니다. 새 버튼을 만들어도 포커스 표시를 따로 챙길 일이 없습니다.
43.2 최소 클릭 영역 토큰
손가락으로 누르는 화면에서 목표가 너무 작으면 옆이 눌립니다. 최소 크기를 토큰으로 못박아 두고 눌리는 요소가 모두 그 값을 지키게 합니다.
:root {
--tap-min: 44px;
}
.btn,
.icon-btn {
min-width: var(--tap-min);
min-height: var(--tap-min);
display: inline-flex;
align-items: center;
justify-content: center;
}
아이콘 자체가 20px라도 눌리는 판은 44px로 보장됩니다. 손끝이 조금 어긋나도 제대로 눌리니, 모바일에서 오작동이 확 줄어듭니다.
43.3 온컬러 토큰으로 대비 보장
배경색만 정하고 그 위 글자색을 그때그때 고르면 대비가 무너지기 쉽습니다. 배경 토큰마다 그 위에 얹을 글자색을 짝으로 미리 정해 둡니다.
:root {
--color-brand: #2563eb;
--color-on-brand: #ffffff; /* 브랜드 배경 위 글자색 */
--color-warn: #b45309;
--color-on-warn: #ffffff; /* 경고 배경 위 글자색 */
}
.btn--primary {
background: var(--color-brand);
color: var(--color-on-brand);
}
배경과 온컬러가 늘 한 쌍으로 움직이니, 브랜드 색을 바꿔도 그 위 글자는 미리 검증된 대비를 유지합니다. 글자색을 즉흥으로 고르다 대비가 깨지는 사고가 원천부터 사라집니다.
43.4 움직임 줄이기 대응
화면의 움직임이 어지럼을 부르는 사람이 있습니다. 그런 설정을 켠 사용자에게는 시스템 전체의 전환을 한꺼번에 눌러 줍니다.
.modal {
transition: transform .2s ease;
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: .01ms !important;
animation-iteration-count: 1 !important;
transition-duration: .01ms !important;
}
}
평소에는 부드럽게 미끄러지던 모달이 움직임을 줄인 설정에서는 즉시 자리를 잡습니다. 컴포넌트마다 조건을 걸지 않아도, 이 한 벌의 규칙이 화면 전체의 애니메이션을 대신 다스립니다.
43.5 고대비 설정 대응
더 강한 대비를 원하는 사용자에게는 컴포넌트를 새로 짜는 대신 토큰 값만 진하게 갈아 끼웁니다. 컴포넌트는 여전히 같은 토큰만 바라봅니다.
@media (prefers-contrast: more) {
:root {
--color-text-weak: #374151;
--color-border: #111827;
--focus-ring: 3px solid #1d4ed8;
}
}
옅던 보조 글자색이 진해지고 테두리가 또렷해지며 포커스링이 두꺼워집니다. 값을 바라보던 모든 컴포넌트가 손대지 않아도 함께 선명해집니다.
43.6 sr-only 유틸 클래스
눈에는 보이지 않지만 화면 낭독기에는 읽혀야 하는 문구가 있습니다. 아이콘만 있는 버튼에 이름을 달아 주는 유틸 클래스를 시스템에 하나 둡니다.
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
아이콘 버튼에 이렇게 붙입니다.
<button class="icon-btn">
<svg aria-hidden="true">...</svg>
<span class="sr-only">메뉴 열기</span>
</button>
화면에는 아이콘만 보이지만 낭독기는 메뉴 열기라고 또렷이 읽어 줍니다. 유틸 하나면 모든 아이콘 버튼이 같은 방식으로 이름을 갖습니다.
43.7 aria 상태에 스타일 걸기
보이는 상태와 낭독기가 읽는 상태가 어긋나면 곤란합니다. 스타일을 aria 속성에 직접 걸면 둘이 절대 따로 놀지 않습니다.
.tab {
color: var(--color-text-weak);
}
.tab[aria-selected="true"] {
color: var(--color-brand);
font-weight: 700;
}
.btn[aria-busy="true"] {
opacity: .6;
cursor: progress;
}
마크업에서 aria-selected를 켜면 색과 무게가 함께 바뀝니다.
<button class="tab" aria-selected="true">홈</button>
선택된 모습을 별도 클래스가 아니라 aria 속성이 결정하니, 화면의 강조와 낭독기가 전하는 의미가 늘 한 몸으로 움직입니다. 접근성 속성을 켜는 것이 곧 스타일을 켜는 일이 됩니다.
43.8 흔한 실수 되돌리기
테두리가 지저분해 보인다는 이유로 포커스 표시를 통째로 지우는 코드를 자주 봅니다. 이러면 키보드 사용자가 길을 잃습니다.
button:focus {
outline: none; /* 키보드 사용자에게 위치를 숨김 */
}
포커스링이 거슬리는 건 마우스로 눌렀을 때뿐이니, 지우지 말고 :focus-visible로 키보드 이동일 때만 보이게 하면 됩니다.
button:focus-visible {
outline: var(--focus-ring);
outline-offset: var(--focus-offset);
}
마우스로 누를 때는 링이 뜨지 않고, 키보드로 이동할 때만 또렷한 링이 지금 여기를 짚어 줍니다. 접근성을 지키면서도 깔끔한 화면을 함께 얻는 지점입니다.
43.9 정리
접근성은 마지막에 덧바르는 마감이 아니라 토큰과 기본 스타일에 처음부터 심어 두는 뼈대입니다. 포커스링과 클릭 영역과 온컬러를 토큰으로 정하고, 움직임과 대비 설정에 시스템이 알아서 응답하며, 낭독기용 유틸과 aria 상태 스타일을 기본으로 갖추면, 그 시스템으로 만든 화면은 만드는 사람이 애쓰지 않아도 모두에게 열려 있습니다.