[CSS] Learn Accessibility by Building a Quiz - 접근성을 고려한 홈페이지 만들기
https://www.freecodecamp.org/learn/2022/responsive-web-design/learn-accessibility-by-building-a-quiz/step-1
www.freecodecamp.org
접근성(accessibility)은 모든 사람, 심지어 장애가 있는 사람들도 웹페이지를 쉽게 사용할 수 있도록 만드는 것입니다.
이 과정에서는 퀴즈 웹페이지를 구축합니다. 키보드 단축키, ARIA 속성, 디자인 모범 사례와 같은 접근성 도구를 배웁니다.
접근성과 시맨틱 HTML이 중요한 이유
- 웹사이트는 모든 사용자가 쉽게 탐색할 수 있어야 한다.
- 시각 장애인이나 화면 리더(screen reader)를 사용하는 사람들에게 페이지 구조를 올바르게 전달하는 것이 중요함.
- 그래서 의미가 있는(시맨틱) HTML 태그를 사용하면 접근성이 향상됨.
header와 main의 역할
<header> - 페이지 소개 및 내비게이션 역할
<header>
<h1>Welcome to My Website</h1>
<nav>
<ul>
<li><a href="#home">Home</a></li>
<li><a href="#about">About</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
</nav>
</header>
✔ 내비게이션(<nav>)을 포함할 수 있음
✔ 화면 리더가 이 요소를 인식하면 "이 부분이 페이지의 머리글입니다."라고 안내
<main> - 페이지의 핵심 콘텐츠
<main>
<h2>About This Site</h2>
<p>This site is all about learning HTML and accessibility.</p>
</main>
✔ <main>은 본문(content) 영역
✔ 한 페이지에 한 개만 사용해야 함 (화면 리더가 "메인 콘텐츠"로 인식)
✔ 헤더, 사이드바, 푸터 등의 보조적인 부분을 제외한 주요 내용을 포함
왜 <div> 대신 시맨틱 태그를 써야 할까?
<div class="header"> <!-- 의미가 불명확 -->
<h1>My Website</h1>
</div>
<div class="main-content"> <!-- 의미가 불명확 -->
<p>Welcome to my website!</p>
</div>
div class(위)를 이용한 걸 시맨틱 태그(아래)를 이용하면
<header>
<h1>My Website</h1>
</header>
<main>
<p>Welcome to my website!</p>
</main>
✔ 화면 리더가 시맨틱 태그를 인식하여 더 정확한 안내 가능
✔ 검색 엔진 최적화(SEO)에도 유리 (검색엔진이 페이지 구조를 이해하기 쉬움)
aria-labelledby와 접근성을 위한 section 구조
aria-labelledby란?
- 웹 접근성을 위해 화면 리더(screen reader)가 특정 영역의 목적을 이해할 수 있도록 "라벨(이름)"을 제공하는 속성
- 특정 요소의 id를 참조하여 그 내용을 라벨로 사용
section과 aria-labelledby를 사용하는 이유
- <section> 태그는 페이지를 의미 있는 영역(Region)으로 나누는 역할
- 모든 Region(role=region) 요소는 라벨(label)이 필수
- 라벨을 추가하는 방법:
- <section> 내부에 <h2> 같은 제목을 넣고
- 해당 제목의 id를 aria-labelledby 속성에 연결 -> h2를 이 섹션의 이름으로 사용
- <label for=""><input id=""> 맞췄던 거처럼 <section aria-labelledby=""><h2 id=""> 를 맞춰줘야 한다.
예제 코드
<section aria-labelledby="student-info">
<h2 id="student-info">Student Information</h2>
<p>Name: John Doe</p>
<p>Age: 20</p>
</section>
<section aria-labelledby="html-questions">
<h2 id="html-questions">HTML Questions</h2>
<p>What does the `<div>` element do?</p>
</section>
<section aria-labelledby="css-questions">
<h2 id="css-questions">CSS Questions</h2>
<p>What is the difference between `relative` and `absolute` positioning?</p>
</section>
role="region" 은 언제 써줘야 하나?
- <section>은 시맨틱 태그라서 특별한 role 없이도 자동으로 "region" 역할을 가짐.
- <section role="region">은 불필요!
- <div> 같은 비시맨틱 요소를 쓸 때만 role="region"을 사용
aria-labelledby는 접근성을 위한 필수 속성
- 시각 장애인이 화면 리더를 사용하면 이 영역이 무엇인지 설명을 들을 수 있음
- <h2>만 있어도 시맨틱 HTML을 활용하는 거지만,
aria-labelledby를 추가하면 접근성을 더욱 강화 - 예를 들어, 화면 리더는 이렇게 읽음:"Student Information, Section 시작. Name: John Doe, Age: 20"
Placeholder 대신 Label을 사용하는 것이 접근성에 더 좋다.
Placeholder 의 문제점
- 사용자가 값을 입력한 후 placeholder가 사라짐
- 무엇을 입력해야 하는지 잊을 수 있음
- 일부 사용자는 placeholder를 입력된 값으로 착각
- 특히, 색 대비가 낮거나 흐리게 표시되면 입력된 값처럼 보일 위험이 있음.
- 스크린 리더(screen reader)가 올바르게 읽지 않을 수도 있음
- Label은 확실히 읽어주지만, placeholder는 잘못 읽거나 무시될 가능성이 있음.
Best Practice: Label 사용
<form>
<label for="name">Name:</label>
<input type="text" id="name">
</form>
✔ 명확한 label을 제공하면 시각 장애가 있는 사용자도 쉽게 이해할 수 있음.
✔ for="id값"을 사용해 label과 input을 연결하면 클릭 시 input이 활성화됨
✅ label을 사용하여 명확한 입력 필드를 제공하는 것이 접근성(Best Practice)에 좋음
✅ placeholder는 부가적인 힌트로만 사용하고, 주요 설명은 label로 제공
시각 장애인을 위한 숨겨진 텍스트 추가하기 (.sr-only 클래스 활용)
문제점: 질문 번호만으로는 충분한 정보가 제공되지 않음
<h3>1.</h3>
<p>What is HTML?</p>
- "1."이라는 번호만으로는 질문의 내용을 알기 어려움.
- 특히, 스크린 리더를 사용하는 사용자는 이 숫자만 들으면 질문과의 연결을 이해하기 어려움.
해결: span.sr-only 추가
- sr-only 클래스를 사용하면 화면에는 보이지 않지만, 스크린 리더가 읽을 수 있도록 숨겨진 텍스트를 추가할 수 있음.
<h3>1. <span class="sr-only">Question:</span></h3>
<p>What is HTML?</p>
✔ 이렇게 하면 스크린 리더는 "1. Question"이라고 읽어줌!
✔ 시각적으로는 여전히 "1."만 보이지만, 접근성이 개선됨
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
clip-path: inset(50%);
white-space: nowrap;
}
✅ CSS sr-only 적용 필수: 단순히 display: none; 하면 스크린 리더도 읽지 못하기 때문에 올바른 숨김 스타일 적용 필요

label에 중첩된 input은 for="", id="" 로 연결해주는 게 필수는 아니지만 명시적으로 연결하는 것이 좋다.
마지막 Semantic HTML 요소: <footer>와 <address>
- <footer> 요소
- 페이지와 관련된 콘텐츠를 담는 컨테이너 역할
- 일반적으로 저작권 정보, 사이트 맵, 관련 링크 등을 포함
- <address> 요소
- 페이지 작성자의 연락처 정보를 담는 컨테이너
- 이메일, 전화번호, 물리적 주소 등의 정보 포함
➡️ <footer>는 페이지 전체 또는 섹션과 관련된 정보를 제공하며, <address>는 페이지 작성자의 연락처 정보를 담는 데 사용됨.
Header 상단에 고정하기
header {
position: fixed;
top: 0;
}
prefers-reduced-motion과 @media at-rule 사용법
1️⃣ 모션 기반 애니메이션과 사용자 접근성
일부 사용자는 어지럼증등의 이유로 화면에서 움직이는 요소가 불편할 수 있다.
-> 특히 자동 스크롤, 애니메이션 효과 같은 모션 기반 UI
이를 고려해, CSS에서는 사용자의 설정(reduce , no-preference )에 따라 애니메이션을 조정하는 방법을 제공함.
이때 사용하는 것이 @media (prefers-reduced-motion) 미디어 쿼리.
2️⃣ prefers-reduced-motion 미디어 쿼리
이 미디어 쿼리는 사용자가 운영체제(OS)에서 "모션 줄이기" 설정을 활성화했는지 여부를 확인하는 기능
- reduce → 사용자가 모션을 줄이도록 설정했음 (애니메이션 최소화)
- no-preference → 사용자가 특별한 설정을 하지 않음 (기본 애니메이션 적용 가능)
👉 scroll-behavior: smooth; 스타일을 적용할 때, 사용자가 모션 줄이기(reduce)를 설정한 경우에는 적용하지 않아야 함.
👉 대신 모션 줄이기 설정이 없는(no-preference) 경우에만 scroll-behavior: smooth;를 적용해야 함.
@media (prefers-reduced-motion: no-preference) {
html {
scroll-behavior: smooth;
}
}
- @media (prefers-reduced-motion: no-preference) → 사용자가 모션 줄이기 설정을 하지 않은 경우에만 아래 스타일 적용
- html { scroll-behavior: smooth; } → 부드러운 스크롤 적용
prefers-reduced-motion 미디어 쿼리를 사용하면, 모든 사용자에게 더 나은 접근성을 제공할 수 있다
멀미 나는 사람은 편리하지만 멀미나는 모션이 안들어갈 거고
멀미 안나는 사람은 모션이 들어가서 훨씬 편리한 기능을 쓸 수 있다.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="freeCodeCamp Accessibility Quiz practice project" />
<title>Accessibility Quiz</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<header>
<img id="logo" alt="freeCodeCamp" src="https://cdn.freecodecamp.org/platform/universal/fcc_primary.svg">
<h1>HTML/CSS Quiz</h1>
<nav>
<ul>
<li><a href="#student-info">INFO</a></li>
<li><a href="#html-questions">HTML</a></li>
<li><a href="#css-questions">CSS</a></li>
</ul>
</nav>
</header>
<main>
<form method="post" action="https://freecodecamp.org/practice-project/accessibility-quiz">
<section role="region" aria-labelledby="student-info">
<h2 id="student-info">Student Info</h2>
<div class="info">
<label for="student-name">Name:</label>
<input type="text" name="student-name" id="student-name" />
</div>
<div class="info">
<label for="student-email">Email:</label>
<input type="email" name="student-email" id="student-email" />
</div>
<div class="info">
<label for="birth-date">Date of Birth:</label>
<input type="date" name="birth-date" id="birth-date" />
</div>
</section>
<section role="region" aria-labelledby="html-questions">
<h2 id="html-questions">HTML</h2>
<div class="question-block">
<h3><span class="sr-only">Question</span>1</h3>
<fieldset class="question" name="html-question-one">
<legend>
The legend element represents a caption for the content of its
parent fieldset element
</legend>
<ul class="answers-list">
<li>
<label for="q1-a1">
<input type="radio" id="q1-a1" name="q1" value="true" />
True
</label>
</li>
<li>
<label for="q1-a2">
<input type="radio" id="q1-a2" name="q1" value="false" />
False
</label>
</li>
</ul>
</fieldset>
</div>
<div class="question-block">
<h3><span class="sr-only">Question</span>2</h3>
<fieldset class="question" name="html-question-two">
<legend>
A label element nesting an input element is required to have a
for attribute with the same value as the input's id
</legend>
<ul class="answers-list">
<li>
<label for="q2-a1">
<input type="radio" id="q2-a1" name="q2" value="true" />
True
</label>
</li>
<li>
<label for="q2-a2">
<input type="radio" id="q2-a2" name="q2" value="false" />
False
</label>
</li>
</ul>
</fieldset>
</div>
</section>
<section role="region" aria-labelledby="css-questions">
<h2 id="css-questions">CSS</h2>
<div class="formrow">
<div class="question-block">
<label for="selector">Can the CSS margin property accept negative values?</label>
</div>
<div class="answer">
<select name="selector" id="selector" required>
<option value="">Select an option</option>
<option value="yes">Yes</option>
<option value="no">No</option>
</select>
</div>
<div class="question-block">
<label for="css-textarea">Do you have any questions:</label>
</div>
<div class="answer">
<textarea id="css-textarea" name="css-questions" rows="5" cols="24"></textarea>
</div>
</div>
</section>
<button type="submit">Send</button>
</form>
</main>
<footer>
<address>
<a href="https://freecodecamp.org">freeCodeCamp</a><br />
San Francisco<br />
California<br />
USA
</address>
</footer>
</body>
</html>
styles.css
@media (prefers-reduced-motion: no-preference) {
* {
scroll-behavior: smooth;
}
}
body {
background: #f5f6f7;
color: #1b1b32;
font-family: Helvetica;
margin: 0;
}
header {
width: 100%;
height: 50px;
background-color: #1b1b32;
display: flex;
justify-content: space-between;
align-items: center;
position: fixed;
top: 0;
}
#logo {
width: max(10rem, 18vw);
background-color: #0a0a23;
aspect-ratio: 35 / 4;
padding: 0.4rem;
}
h1 {
color: #f1be32;
font-size: min(5vw, 1.2em);
text-align: center;
}
nav {
width: 50%;
max-width: 300px;
height: 50px;
}
nav > ul {
display: flex;
justify-content: space-evenly;
flex-wrap: wrap;
align-items: center;
padding-inline-start: 0;
margin-block: 0;
height: 100%;
}
nav > ul > li {
color: #dfdfe2;
margin: 0 0.2rem;
padding: 0.2rem;
display: block;
}
nav > ul > li:hover {
background-color: #dfdfe2;
color: #1b1b32;
cursor: pointer;
}
li > a {
color: inherit;
text-decoration: none;
}
main {
padding-top: 50px;
}
section {
width: 80%;
margin: 0 auto 10px auto;
max-width: 600px;
}
h1,
h2 {
font-family: Verdana, Tahoma;
}
h2 {
border-bottom: 4px solid #dfdfe2;
margin-top: 0px;
padding-top: 60px;
}
.info {
padding: 10px 0 0 5px;
}
.formrow {
margin-top: 30px;
padding: 0px 15px;
}
input {
font-size: 1rem;
}
.info label, .info input {
display: inline-block;
}
.info input {
width: 50%;
text-align: left;
}
.info label {
width: 10%;
min-width: 55px;
text-align: right;
}
.question-block {
text-align: left;
display: block;
width: 100%;
margin-top: 20px;
padding-top: 5px;
}
h3 {
margin-top: 5px;
padding-left: 15px;
font-size: 1.375rem;
}
h3::before {
content: "Question #";
}
.question {
border: none;
padding-bottom: 0;
}
.answers-list {
list-style: none;
padding: 0;
}
button {
display: block;
margin: 40px auto;
width: 40%;
padding: 15px;
font-size: 1.438rem;
background: #d0d0d5;
border: 3px solid #3b3b4f;
}
footer {
background-color: #2a2a40;
display: flex;
justify-content: center;
}
footer,
footer a {
color: #dfdfe2;
}
address {
text-align: center;
padding: 0.3em;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
clip-path: inset(50%);
white-space: nowrap;
}