접근성은 소수를 위한 곁다리가 아니라 문을 넓히는 일입니다. 턱을 없앤 길이 휠체어뿐 아니라 유아차와 짐수레에도 편하듯이요. 눈에 보이지 않는 배려를 실제 CSS와 마크업으로 하나씩 심어 보겠습니다.

27.1 포커스는 지우지 말고 다듬어라

키보드만 쓰는 사용자는 지금 어디에 있는지 외곽선으로 압니다. 그 외곽선을 없애면 그들은 길을 잃습니다.


button:focus {
outline: none;
}


이렇게 지워 버리는 대신, 마우스로 눌렀을 때만 빼고 키보드 이동에는 또렷한 링을 남깁니다.


button:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 2px;
border-radius: 4px;
}


:focus-visible 는 브라우저가 키보드 조작으로 판단할 때만 링을 그려 줍니다. 마우스 사용자는 깔끔하게, 키보드 사용자는 안전하게, 둘 다 챙깁니다.

27.2 스크린 리더 전용 텍스트

아이콘만 있는 버튼은 낭독기가 읽을 이름이 없습니다. 눈엔 안 보이되 소리로는 읽히는 글자를 숨겨 둡니다.


.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>
<svg aria-hidden="true">...</svg>
<span class="sr-only">장바구니 열기</span>
</button>


화면엔 아이콘만 보이지만 낭독기는 장바구니 열기라고 읽습니다. display: none 으로 숨기면 낭독기도 못 읽으니, 위 방식으로 화면에서만 치워야 합니다.

27.3 시맨틱 버튼 vs div 버튼

겉모습이 버튼이어도 div 로 만들면 키보드로 누를 수 없고 낭독기도 버튼인 줄 모릅니다.


<div class="btn">저장</div>


진짜 button 을 쓰면 포커스, 엔터와 스페이스 키 입력, 낭독기 역할 안내가 공짜로 따라옵니다.


<button type="button">저장</button>


부득이 div 를 써야 한다면 role="button"tabindex="0", 키 입력 처리를 직접 다 얹어야 합니다. 그럴 바엔 처음부터 button 이 정답입니다.

27.4 라벨과 입력을 묶기

입력칸에 이름표가 없으면 낭독기는 그냥 편집창이라고만 읽습니다. labelfor 로 연결해 이름을 붙입니다.


<label for="phone">휴대폰 번호</label>
<input id="phone" type="tel"
aria-describedby="phone-help">
<p id="phone-help">- 없이 숫자만 입력</p>


forid 가 같아 라벨을 눌러도 입력칸에 포커스가 갑니다. aria-describedby 는 도움말까지 함께 읽어 줍니다.

27.5 색 하나에만 뜻을 싣지 않기

초록은 성공, 빨강은 실패로만 나누면 색을 구별하기 어려운 사용자는 아무 신호도 못 받습니다. 색 옆에 기호나 글자를 하나 더 얹습니다.


.status {
display: inline-flex;
align-items: center;
gap: 4px;
}
.status--ok { color: #15803d; }
.status--ok::before { content: "\2713"; }
.status--err { color: #b91c1c; }
.status--err::before { content: "\2715"; }


체크와 엑스 기호가 색과 나란히 붙어, 색을 못 가려도 성공인지 실패인지 모양으로 읽힙니다. 정보의 통로를 둘로 늘린 셈입니다.

27.6 충분한 명암 대비

멋을 낸다고 옅은 회색 글자를 흰 배경에 깔면 눈이 밝은 사람에게도 흐릿합니다. 본문 글자는 대비를 넉넉히 잡습니다.


.text-weak {
color: #9ca3af;
}


흰 바탕에 이 회색은 대비가 모자랍니다. 읽어야 할 본문이라면 더 짙은 색으로 내립니다.


.text-body {
color: #374151;
}


본문 글자는 배경과 대비가 4.5 대 1 이상이면 안전합니다. 세련돼 보인다는 이유로 읽힘을 희생하지 않습니다.

27.7 움직임을 줄여 달라는 뜻 존중

화려하게 미끄러지고 번쩍이는 움직임이 누군가에게는 어지럼과 울렁임을 부릅니다. 기기에 움직임을 줄이라고 정해 둔 사용자에게는 잔잔하게 바꿉니다.


.card {
transition: transform .3s;
}
@media (prefers-reduced-motion: reduce) {
.card {
transition: none;
}
* {
animation-duration: 0.01ms !important;
}
}


사용자가 이미 기기에 일러 둔 뜻을 CSS가 그대로 받아, 큰 움직임을 없애거나 아주 짧게 줄입니다. 멋보다 몸의 편안함이 먼저입니다.

27.8 겉모습과 속뼈대 일치시키기

눈엔 제목처럼 크고 굵어도 그냥 div 면 낭독기는 평범한 한 줄로 읽습니다. 뜻이 제목이면 제목 태그로 적습니다.


<div class="big-bold">공지사항</div>


보이는 크기는 CSS로 정하되, 이것이 제목이라는 속뜻은 태그에 담습니다.


<h2 class="section-title">공지사항</h2>


낭독기 사용자는 제목만 건너뛰며 화면 구조를 훑습니다. 겉과 속이 어긋나지 않아야 그 지도가 제대로 그려집니다.

27.9 이미지에 대체 텍스트

낭독기는 그림 속을 보지 못합니다. 뜻을 담은 이미지에는 그 뜻을 alt 로 적어 목소리를 달아 줍니다.


<img src="chart.png"
alt="3월 방문자가 2월보다 40% 늘었다">


<img src="divider.png" alt="">


정보를 담은 그림은 그 내용을 alt 에 풀어 쓰고, 장식일 뿐인 그림은 alt="" 로 비워 낭독기가 건너뛰게 합니다. 빈 값도 엄연한 신호입니다.

27.10 정리

접근성은 규칙의 목록이라기보다 곁의 한 사람을 잊지 않으려는 마음가짐입니다. 포커스를 다듬고, 색에 기호를 얹고, 대비를 올리고, div 대신 진짜 버튼을 쓰는 이 작은 코드들이 모여 누구도 문 앞에서 돌아서지 않는 화면을 만듭니다. 그리고 이 배려는 대개 모두의 경험까지 함께 나아지게 합니다.