Stage 5: Professional Skills

Web Accessibility · Lesson 04

Images and Media

Writing alt text, captions, transcripts and color-blind friendly content.

Web Accessibility3 min readFree lesson

An image without a text alternative is invisible to a screen-reader user. A video without captions is unusable in a noisy room, on a train, or for a deaf viewer. Both are content problems, not edge cases.

Learning objectives

  • Write alt text that serves the image's purpose.
  • Mark decorative images correctly.
  • Handle functional and complex images.
  • Add captions, transcripts and accessible media controls.

1. The alt decision tree

Ask: why is this image here?

Decorative — it adds no information (background, divider, decorative icon next to a label). Use empty alt so it is skipped:

<img src="/pattern.svg" alt="" />

Informative — it conveys content. Describe the content, not the file:

<!-- wrong -->
<img src="campus.jpg" alt="image of campus" />

<!-- right -->
<img src="campus.jpg" alt="Aivasu campus reception on the second floor, with course banners behind the desk" />

Functional — it is the only content of a link or button. Describe the action, not the picture:

<a href="/cart">
  <img src="cart.svg" alt="View cart" />
</a>

Complex — a chart, diagram or screenshot with data. Give a short alt and a longer description nearby:

<figure>
  <img src="placements.png" alt="Placement rate by course, 2025" />
  <figcaption>
    2025 placement rate: Full Stack 92%, Data Science 88%, Cloud 81%.
  </figcaption>
</figure>

Text in an image — avoid it. If it must exist, repeat the text in alt.

2. Alt text rules

  • Be specific and concise (roughly 125 characters for a simple image).
  • Do not start with "Image of" or "Picture of" — screen readers already say "image".
  • Do not repeat the caption word for word; the caption is for everyone, alt is the replacement.
  • Include relevant text visible in the image.
  • End with a period so the screen reader pauses.

3. SVG and icons

<!-- meaningful icon -->
<svg role="img" aria-labelledby="icon-title">
  <title id="icon-title">Certificate</title>
  ...
</svg>

<!-- decorative icon next to visible text -->
<svg aria-hidden="true" focusable="false">...</svg>

Lucide and most icon libraries accept aria-hidden="true". An icon-only button still needs a label on the button:

<button type="button" aria-label="Close">
  <svg aria-hidden="true">...</svg>
</button>

4. Video

<video controls preload="metadata" poster="poster.jpg">
  <source src="lesson.mp4" type="video/mp4" />
  <track kind="captions" src="lesson-en.vtt" srclang="en" label="English" default />
  <track kind="chapters" src="chapters.vtt" srclang="en" label="Chapters" />
</video>
  • Captions for dialogue and meaningful sounds. Auto-generated captions must be reviewed — they are wrong often enough to mislead.
  • Transcript below the video for people who prefer reading or cannot use audio.
  • Audio description for visual information that is not spoken (a demonstrated step, an on-screen error).
  • Never autoplay with sound. If it autoplays muted, provide a visible pause control.
  • controls is mandatory; custom controls must be keyboard accessible and labelled.

5. Audio

<audio controls src="podcast.mp3"></audio>
<p><a href="podcast-transcript.html">Read the transcript</a></p>

A transcript is the audio equivalent of alt text. It also makes the content indexable.

6. Motion and flashing

  • Avoid content that flashes more than three times per second.
  • Respect reduced motion:
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
  • Pause, stop and hide controls for anything that moves for more than five seconds.

7. Colour is not information

<!-- wrong: only colour distinguishes status -->
<span class="dot green"></span>

<!-- right -->
<span class="dot green" aria-hidden="true"></span>
<span>Available</span>

Errors, statuses and required fields need text or an icon with a label, not just red or green.

Common mistakes

  • alt="image", alt="photo", or the filename.
  • Duplicating the caption in alt.
  • Missing alt on logos that are the only content of a home link.
  • Captions that are auto-generated and never reviewed.
  • Autoplaying video with sound.
  • An icon-only button whose SVG has a <title> but the button has no accessible name (the label must be on the control).

Practice

Fix the gallery and media page in the assignment. Every image gets a decision, every video gets captions or a transcript.

Example files

Complete, runnable files used in this lesson. Copy them into your project and experiment.

accessible-gallery.htmlHTML
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>접근성 이미지 갤러리</title>
    <style>
        *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
        body { font-family: 'Noto Sans KR', sans-serif; padding: 40px; background: #f8f8f8; }

        h1 { margin-bottom: 8px; color: #1a1a2e; }
        .page-desc { color: #555; margin-bottom: 32px; }

        /* ── 포커스 스타일 ── */
        :focus-visible {
            outline: 3px solid #0066cc;
            outline-offset: 3px;
            border-radius: 3px;
        }

        /* ── 갤러리 그리드 ── */
        .gallery {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
            gap: 20px;
            margin-bottom: 40px;
        }

        /* ── 갤러리 아이템
           button으로 만들어 키보드로 접근 가능하게 함 ── */
        .gallery-item {
            background: white;
            border: 2px solid transparent;
            border-radius: 8px;
            overflow: hidden;
            cursor: pointer;
            text-align: left;
            padding: 0;
            width: 100%;
            transition: border-color 0.2s, box-shadow 0.2s;
        }
        .gallery-item:hover,
        .gallery-item:focus-visible {
            border-color: #0066cc;
            box-shadow: 0 4px 16px rgba(0, 102, 204, 0.2);
        }

        /* ── 이미지 컨테이너 (실제 이미지 대신 색상 박스로 시뮬레이션) ── */
        .img-container {
            width: 100%;
            height: 180px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 3rem;
        }

        .gallery-caption {
            padding: 12px 16px;
        }
        .gallery-caption strong { display: block; color: #1a1a2e; margin-bottom: 4px; }
        .gallery-caption span { font-size: 0.85rem; color: #666; }

        /* ── 라이트박스 오버레이 ── */
        .lightbox {
            position: fixed;
            inset: 0;
            background: rgba(0,0,0,0.9);
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            z-index: 1000;
            padding: 20px;
        }
        .lightbox[hidden] { display: none; }

        /* ── 라이트박스 이미지 영역 ── */
        .lightbox-img {
            width: 300px;
            height: 300px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 6rem;
            background: rgba(255,255,255,0.1);
            border-radius: 8px;
            margin-bottom: 16px;
        }

        /* ── 라이트박스 설명 ── */
        .lightbox-info {
            text-align: center;
            color: white;
            max-width: 500px;
        }
        .lightbox-info h2 { margin-bottom: 8px; }
        .lightbox-info p { color: #ccc; font-size: 0.95rem; line-height: 1.6; }

        /* ── 닫기 버튼 ── */
        .lightbox-close {
            position: absolute;
            top: 20px;
            right: 20px;
            background: rgba(255,255,255,0.1);
            border: 2px solid rgba(255,255,255,0.3);
            color: white;
            width: 48px;
            height: 48px;
            border-radius: 50%;
            font-size: 1.2rem;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .lightbox-close:hover { background: rgba(255,255,255,0.2); }

        /* ── 이전/다음 버튼 ── */
        .lightbox-nav {
            display: flex;
            gap: 16px;
            margin-top: 20px;
        }
        .lightbox-nav button {
            background: rgba(255,255,255,0.1);
            border: 2px solid rgba(255,255,255,0.3);
            color: white;
            padding: 10px 24px;
            border-radius: 6px;
            font-size: 1rem;
            cursor: pointer;
        }
        .lightbox-nav button:hover { background: rgba(255,255,255,0.2); }
        .lightbox-nav button:disabled { opacity: 0.4; cursor: not-allowed; }

        /* ── 현재 위치 표시 (스크린리더용) ── */
        .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;
        }

        /* ── prefers-reduced-motion: 애니메이션 제거 ── */
        @media (prefers-reduced-motion: reduce) {
            .gallery-item { transition: none; }
        }
    </style>
</head>
<body>

    <h1>자연 사진 갤러리</h1>
    <p class="page-desc">
        Tab 키로 사진을 탐색하고 Enter 또는 Space로 크게 볼 수 있습니다.
    </p>

    <!-- 갤러리 컨테이너
         role="list": grid 레이아웃이지만 목록임을 명시 -->
    <ul class="gallery" role="list" aria-label="자연 사진 갤러리">

        <!-- ── 갤러리 아이템
             button 태그로 키보드/스크린리더 지원
             aria-label: 클릭 시 동작과 사진 설명을 함께 전달 ── -->
        <li>
            <button
                class="gallery-item"
                onclick="openLightbox(0)"
                aria-label="산 정상의 일출 사진 크게 보기"
            >
                <!-- 실제 이미지: alt에 사진의 핵심 내용 설명 -->
                <!-- 여기서는 이미지 대신 이모지로 시뮬레이션 -->
                <div class="img-container" style="background:#fff3e0;" aria-hidden="true">🌄</div>
                <div class="gallery-caption">
                    <strong>산 정상의 일출</strong>
                    <span>2024년 3월, 한라산</span>
                </div>
            </button>
        </li>

        <li>
            <button
                class="gallery-item"
                onclick="openLightbox(1)"
                aria-label="에메랄드빛 바다 사진 크게 보기"
            >
                <div class="img-container" style="background:#e0f7fa;" aria-hidden="true">🌊</div>
                <div class="gallery-caption">
                    <strong>에메랄드빛 바다</strong>
                    <span>2024년 7월, 제주 협재해변</span>
                </div>
            </button>
        </li>

        <li>
            <button
                class="gallery-item"
                onclick="openLightbox(2)"
                aria-label="단풍으로 물든 숲 사진 크게 보기"
            >
                <div class="img-container" style="background:#fce4ec;" aria-hidden="true">🍁</div>
                <div class="gallery-caption">
                    <strong>단풍으로 물든 숲</strong>
                    <span>2024년 10월, 내장산</span>
                </div>
            </button>
        </li>

        <li>
            <button
                class="gallery-item"
                onclick="openLightbox(3)"
                aria-label="설원의 침묵 사진 크게 보기"
            >
                <div class="img-container" style="background:#e8eaf6;" aria-hidden="true">❄️</div>
                <div class="gallery-caption">
                    <strong>설원의 침묵</strong>
                    <span>2024년 1월, 대관령</span>
                </div>
            </button>
        </li>
    </ul>

    <!-- ── 라이트박스 모달
         role="dialog": 다이얼로그 역할
         aria-modal="true": 모달 밖을 스크린리더가 무시
         aria-labelledby: 제목과 연결 ── -->
    <div
        class="lightbox"
        id="lightbox"
        role="dialog"
        aria-modal="true"
        aria-labelledby="lightbox-title"
        hidden
    >
        <!-- 닫기 버튼: 라이트박스 안의 첫 번째 포커스 요소 -->
        <button
            class="lightbox-close"
            id="lightbox-close"
            aria-label="갤러리로 돌아가기"
            onclick="closeLightbox()"
        >
            ✕
        </button>

        <!-- 이미지 영역 -->
        <div class="lightbox-img" id="lightbox-img" aria-hidden="true"></div>

        <!-- 이미지 정보
             스크린리더는 aria-labelledby로 제목을 먼저 읽고,
             그 다음 설명을 읽음 -->
        <div class="lightbox-info">
            <h2 id="lightbox-title"></h2>
            <p id="lightbox-desc"></p>
        </div>

        <!-- 이전/다음 내비게이션 -->
        <nav class="lightbox-nav" aria-label="사진 이전/다음">
            <button id="prev-btn" onclick="navigate(-1)" aria-label="이전 사진">
                ← 이전
            </button>
            <!-- 현재 몇 번째 사진인지 스크린리더에 전달 -->
            <span id="nav-status" aria-live="polite" class="sr-only"></span>
            <button id="next-btn" onclick="navigate(1)" aria-label="다음 사진">
                다음 →
            </button>
        </nav>
    </div>

    <script>
        // ── 갤러리 데이터
        const photos = [
            {
                emoji: '🌄',
                bg: '#fff3e0',
                title: '산 정상의 일출',
                desc: '2024년 3월, 한라산 정상에서 촬영. 주황빛 하늘과 구름 위로 떠오르는 태양. 해발 1,947m에서 바라본 장엄한 일출 풍경.',
            },
            {
                emoji: '🌊',
                bg: '#e0f7fa',
                title: '에메랄드빛 바다',
                desc: '2024년 7월, 제주 협재해변. 맑고 투명한 에메랄드색 바다와 새하얀 모래사장. 수평선 너머로 비양도가 보임.',
            },
            {
                emoji: '🍁',
                bg: '#fce4ec',
                title: '단풍으로 물든 숲',
                desc: '2024년 10월, 내장산. 빨강, 주황, 노랑으로 물든 단풍나무 군락. 하늘에서 내려다본 알록달록한 가을 숲.',
            },
            {
                emoji: '❄️',
                bg: '#e8eaf6',
                title: '설원의 침묵',
                desc: '2024년 1월, 강원도 대관령. 새벽에 내린 첫눈으로 뒤덮인 목장. 고요하고 새하얀 설경 속에 홀로 선 나무.',
            },
        ];

        let currentIndex = 0;
        let triggerElement = null;

        // ── 포커스 가능한 요소 선택자
        const FOCUSABLE = 'button:not([disabled]), a[href], [tabindex]:not([tabindex="-1"])';

        function openLightbox(index) {
            triggerElement = document.activeElement; // 트리거 버튼 저장
            currentIndex = index;
            updateLightbox();

            const lightbox = document.getElementById('lightbox');
            lightbox.removeAttribute('hidden');

            // 갤러리 영역을 스크린리더에서 숨김
            document.querySelector('.gallery').setAttribute('aria-hidden', 'true');

            // 첫 번째 요소(닫기 버튼)로 포커스 이동
            setTimeout(() => {
                document.getElementById('lightbox-close').focus();
            }, 50);

            // 키보드 이벤트 등록
            document.addEventListener('keydown', handleLightboxKey);
        }

        function closeLightbox() {
            const lightbox = document.getElementById('lightbox');
            lightbox.setAttribute('hidden', '');

            // 갤러리 접근성 복원
            document.querySelector('.gallery').removeAttribute('aria-hidden');

            // 키보드 이벤트 제거
            document.removeEventListener('keydown', handleLightboxKey);

            // 트리거 버튼으로 포커스 복귀
            if (triggerElement) {
                triggerElement.focus();
                triggerElement = null;
            }
        }

        function updateLightbox() {
            const photo = photos[currentIndex];

            // 이미지 업데이트
            const imgEl = document.getElementById('lightbox-img');
            imgEl.style.background = photo.bg;
            imgEl.textContent = photo.emoji;

            // 제목/설명 업데이트
            document.getElementById('lightbox-title').textContent = photo.title;
            document.getElementById('lightbox-desc').textContent = photo.desc;

            // 현재 위치 안내 (aria-live로 스크린리더에 전달)
            document.getElementById('nav-status').textContent =
                `${currentIndex + 1} / ${photos.length}번째 사진`;

            // 이전/다음 버튼 활성화/비활성화
            document.getElementById('prev-btn').disabled = currentIndex === 0;
            document.getElementById('next-btn').disabled = currentIndex === photos.length - 1;
        }

        function navigate(direction) {
            const newIndex = currentIndex + direction;
            if (newIndex < 0 || newIndex >= photos.length) return;
            currentIndex = newIndex;
            updateLightbox();
        }

        function handleLightboxKey(e) {
            const lightbox = document.getElementById('lightbox');
            const focusables = Array.from(lightbox.querySelectorAll(FOCUSABLE));
            const first = focusables[0];
            const last = focusables[focusables.length - 1];

            switch (e.key) {
                case 'Escape':
                    closeLightbox();
                    break;
                case 'ArrowLeft':
                    navigate(-1); // 왼쪽 화살표: 이전 사진
                    break;
                case 'ArrowRight':
                    navigate(1);  // 오른쪽 화살표: 다음 사진
                    break;
                case 'Tab':
                    // Focus Trap: 라이트박스 안에서만 순환
                    if (e.shiftKey && document.activeElement === first) {
                        e.preventDefault();
                        last.focus();
                    } else if (!e.shiftKey && document.activeElement === last) {
                        e.preventDefault();
                        first.focus();
                    }
                    break;
            }
        }
    </script>
</body>
</html>

Assignment

Assignment 04 — Accessible gallery and media

Create accessible-gallery.html with a gallery, an icon set and one video.

Gallery

  • Six images in a <figure> grid.
  • Each image has a meaningful alt (or alt="" if genuinely decorative).
  • Each figure has a <figcaption> with a title and a short description.
  • One image is a chart; give it a short alt and describe the data in the caption.

Icons

  • Three icon buttons: Share, Download, Close.
  • Each button has an accessible name; the SVG inside is aria-hidden="true".
  • One decorative icon next to visible text must not be announced twice.

Video

<video controls preload="metadata">
  <source src="demo.mp4" type="video/mp4" />
  <track kind="captions" src="demo-en.vtt" srclang="en" label="English" default />
</video>
  • Add a transcript section below the video.
  • No autoplay.
  • Provide a poster image with an appropriate alt (or an empty alt if purely decorative).

Requirements

  1. No alt contains "image of", "picture of" or a filename.
  2. The gallery can be understood with images disabled (read the captions and alt).
  3. Every icon button has a name that describes its action.
  4. The video has captions and a transcript.
  5. Any animation respects prefers-reduced-motion.
  6. Status colours are paired with text.

Test

  • Run axe: no "image-alt", "button-name" or "video-caption" violations.
  • Use a screen reader: each figure is announced with its caption, and each icon button has a name.
  • Zoom to 200%: captions and transcripts remain readable.

Done when

  • Every image has a deliberate alt decision.
  • The video is usable with sound off and with the screen off.
  • Icon-only buttons work with the keyboard and announce an action.
  • The page passes axe with zero critical issues.

Learn this lesson with a mentor

Work through the Build Your Homepage series on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now