Kurs HTML i CSS · Moduł 10: Dostępność (a11y)

Projekt: Dostępna strona Muzeum Egipskiego

7 min czytania
W tej lekcji5

Znasz już wszystkie elementy dostępnej strony, ale na razie leżą osobno, jak kamienne bloki na placu budowy. Prawdziwy test zaczyna się wtedy, gdy trzeba z nich złożyć całą piramidę i sprawdzić, czy wszystkie korytarze się łączą. Czas połączyć wszystko, czego nauczyliśmy się o dostępności, w jednym projekcie! Stworzymy stronę Muzeum Staroegipskiego, która będzie w pełni dostępna - spełniająca standardy WCAG 2.1 na poziomie AA.

Elementy projektu

Nasza strona będzie zawierać:

  1. Skip link - przeskakujący do treści głównej
  2. Semantyczna struktura - header, nav, main, aside, footer
  3. Dostępna nawigacja - z aria-label i aria-current
  4. Sekcja wystawy - z dostępnymi obrazami (alt, figure, figcaption)
  5. Formularz rezerwacji - z labelami, fieldset, legend, walidacją
  6. Sekcja multimedialna - z napisami i transkrypcją
  7. Kontrast i typografia - spełniające WCAG AA
  8. Nawigacja klawiaturowa - widoczny focus, logiczna kolejność Tab
  9. ARIA atrybuty - aria-live, aria-expanded, aria-describedby

Struktura HTML

Budowę zaczynamy od góry. lang="pl" mówi czytnikowi, w jakim języku czytać, a skip link musi być pierwszym elementem w <body>, żeby Tab trafił na niego najpierw:

1<!DOCTYPE html>
2<html lang="pl">
3<head>
4    <meta charset="UTF-8">
5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6    <title>Muzeum Starożytnego Egiptu - Dostępna Strona</title>
7</head>
8<body>
9    <!-- 1. Skip link -->
10    <a href="#main-content" class="skip-link">
11        Przejdź do treści głównej
12    </a>
13
14    <!-- 2. Header z nawigacją -->
15    <header role="banner">
16        <h1>Muzeum Starożytnego Egiptu</h1>
17        <nav aria-label="Nawigacja główna">
18            <ul>
19                <li><a href="#exhibitions" aria-current="page">Wystawy</a></li>
20                <li><a href="#reservation">Rezerwacja</a></li>
21                <li><a href="#multimedia">Multimedia</a></li>
22                <li><a href="#contact">Kontakt</a></li>
23            </ul>
24        </nav>
25    </header>

Atrybut role="banner" jest tu nadmiarowy, bo <header> na najwyższym poziomie strony ma tę rolę wbudowaną - w swoim projekcie możesz go usunąć. Nawigacja dostała nazwę "Nawigacja główna", bo w stopce pojawi się druga.

Krok 2: sekcja wystawy

Treść główna dostaje id="main-content", czyli cel skip linka. Sekcja wystawy ma własny nagłówek, a aria-labelledby nadaje jej nazwę z tego nagłówka:

1    <!-- 3. Treść główna -->
2    <main id="main-content">
3        <!-- Sekcja wystawy -->
4        <section aria-labelledby="exhibitions-title">
5            <h2 id="exhibitions-title">Aktualne wystawy</h2>
6
7            <article>
8                <h3>Skarby Tutanchamona</h3>
9                <figure>
10                    <img src="tutankhamun-mask.jpg"
11                         alt="Złota maska pośmiertna faraona Tutanchamona">
12                    <figcaption>
13                        Maska Tutanchamona, XVIII dynastia, ok. 1323 p.n.e.
14                    </figcaption>
15                </figure>
16                <p>Odkryj niezwykłe artefakty z grobowca młodego faraona.</p>
17            </article>
18        </section>

Dzięki nazwie sekcja staje się landmarkiem "Aktualne wystawy". Hierarchia nagłówków jest ciągła: <h1> w nagłówku strony, <h2> dla sekcji i <h3> dla wystawy.

Krok 3: formularz rezerwacji

Formularz łączy wszystko z lekcji o formularzach - etykiety, grupy pól, opisy i autouzupełnianie:

1        <!-- Formularz rezerwacji -->
2        <section aria-labelledby="reservation-title">
3            <h2 id="reservation-title">Zarezerwuj wizytę</h2>
4            <form aria-labelledby="reservation-title" novalidate>
5                <fieldset>
6                    <legend>Dane osobowe</legend>
7                    <div class="form-group">
8                        <label for="name">Imię i nazwisko:</label>
9                        <input type="text" id="name"
10                               autocomplete="name"
11                               aria-required="true">
12                    </div>
13                    <div class="form-group">
14                        <label for="email">E-mail:</label>
15                        <input type="email" id="email"
16                               autocomplete="email"
17                               aria-required="true"
18                               aria-describedby="email-help">
19                        <p id="email-help" class="hint">
20                            Na ten adres wyślemy potwierdzenie rezerwacji.
21                        </p>
22                    </div>
23                </fieldset>
24
25                <fieldset>
26                    <legend>Wybierz wystawę</legend>
27                    <label>
28                        <input type="radio" name="exhibition"
29                               value="tutankhamun">
30                        Skarby Tutanchamona
31                    </label>
32                    <label>
33                        <input type="radio" name="exhibition"
34                               value="mummies">
35                        Mumie królewskie
36                    </label>
37                </fieldset>
38
39                <button type="submit">Zarezerwuj</button>
40            </form>
41        </section>
42    </main>

Każde pole ma label z pasującym for, grupa radio ma własny <fieldset> z <legend>, a podpowiedź pod e-mailem jest połączona z polem przez aria-describedby.

Krok 4: panel boczny i stopka

Na koniec elementy poza treścią główną - godziny otwarcia w <aside> i stopka z drugą nawigacją:

1    <!-- 4. Sidebar -->
2    <aside aria-label="Informacje dodatkowe">
3        <h2>Godziny otwarcia</h2>
4        <dl>
5            <dt>Poniedziałek - Piątek</dt>
6            <dd>9:00 - 17:00</dd>
7            <dt>Sobota - Niedziela</dt>
8            <dd>10:00 - 18:00</dd>
9        </dl>
10    </aside>
11
12    <!-- 5. Footer -->
13    <footer>
14        <p>Muzeum Starożytnego Egiptu 2024</p>
15        <nav aria-label="Nawigacja stopki">
16            <ul>
17                <li><a href="/polityka-prywatnosci">Polityka prywatności</a></li>
18                <li><a href="/dostepnosc">Deklaracja dostępności</a></li>
19            </ul>
20        </nav>
21    </footer>
22</body>
23</html>

Godziny otwarcia to lista definicji, bo łączą dzień z godzinami. Druga nawigacja ma inną nazwę, więc czytnik odróżni ją od głównej, a link do deklaracji dostępności to dobry zwyczaj - podmioty publiczne w Polsce muszą ją publikować.

Krok 5: komunikaty dynamiczne

Wymagania obejmują też atrybuty ARIA dla treści, które zmieniają się na stronie. Dodaj region na komunikat po rezerwacji i przycisk rozwijający godziny otwarcia:

1<!-- Komunikat po wysłaniu formularza -->
2<p id="form-status" aria-live="polite"></p>
3
4<!-- Przycisk rozwijający dodatkowe informacje -->
5<button type="button" aria-expanded="false" aria-controls="hours">
6    Pokaż godziny otwarcia
7</button>

Region aria-live stoi na stronie od początku i jest pusty - czytnik ogłosi tekst dopiero wtedy, gdy skrypt go tam wpisze. Skrypt przełącza też aria-expanded na "true" po rozwinięciu, a aria-controls wskazuje id rozwijanego elementu.

Style CSS - dostępność

W CSS zaczynamy od zmiennych z kolorami o wysokim kontraście, a potem chowamy skip link i projektujemy widoczną obwódkę focusu:

1/* Zmienne kolorystyczne z wysokim kontrastem */
2:root {
3    --text-primary: #1a1a2e;
4    --text-secondary: #4a4a5a;
5    --bg-primary: #ffffff;
6    --bg-secondary: #f5f1e3;
7    --accent-gold: #8b6914;
8    --accent-hover: #6b4c00;
9    --error: #d32f2f;
10    --success: #2e7d32;
11    --focus-color: #d4af37;
12}
13
14/* Skip link */
15.skip-link {
16    position: absolute;
17    top: -50px;
18    left: 16px;
19    background: var(--text-primary);
20    color: var(--bg-primary);
21    padding: 12px 24px;
22    z-index: 1000;
23    font-size: 1rem;
24    border-radius: 0 0 4px 4px;
25    transition: top 0.2s;
26}
27
28.skip-link:focus {
29    top: 0;
30}
31
32/* Widoczny focus dla wszystkich interaktywnych elementów */
33a:focus-visible,
34button:focus-visible,
35input:focus-visible,
36select:focus-visible,
37textarea:focus-visible {
38    outline: 3px solid var(--focus-color);
39    outline-offset: 2px;
40}

Sprawdź jednak jedną rzecz sam: złota obwódka #d4af37 ma na białym tle tylko ok. 2,1:1, a WCAG wymaga dla stanów interfejsu co najmniej 3:1. Na jasnym tle lepiej sprawdzi się ciemniejsze --accent-gold (ok. 5:1) - dobry dowód, że gotowy kod zawsze warto przetestować.

Krok 7: typografia i preferencje użytkownika

Ostatnia część arkusza ustawia czytelną typografię i reaguje na trzy ustawienia systemu: ograniczenie ruchu, większy kontrast i tryb ciemny:

1/* Typografia dostępna */
2body {
3    font-family: Georgia, 'Times New Roman', serif;
4    font-size: 100%;
5    line-height: 1.6;
6    color: var(--text-primary);
7    background: var(--bg-primary);
8}
9
10/* Szerokość tekstu 50-80 znaków */
11main {
12    max-width: 70ch;
13    margin: 0 auto;
14    padding: 1rem;
15}
16
17/* Prefers reduced motion */
18@media (prefers-reduced-motion: reduce) {
19    * {
20        animation-duration: 0.01ms !important;
21        transition-duration: 0.01ms !important;
22    }
23}
24
25/* Prefers contrast */
26@media (prefers-contrast: more) {
27    :root {
28        --text-primary: #000000;
29        --bg-primary: #ffffff;
30        --accent-gold: #6b4c00;
31    }
32}
33
34/* Dark mode */
35@media (prefers-color-scheme: dark) {
36    :root {
37        --text-primary: #e0d5c0;
38        --bg-primary: #1a1a2e;
39        --bg-secondary: #2d2d44;
40    }
41}

Media queries tylko podmieniają wartości zmiennych, więc reszta arkusza zostaje nietknięta. Czcionka w procentach i kolumna 70ch sprawiają, że strona dobrze znosi powiększenie do 200%.

Checklist projektu

Po ukończeniu projektu sprawdź:

  • Skip link działa i jest widoczny na focus
  • Semantyczna struktura (header, nav, main, aside, footer)
  • Nawigacja z aria-label i aria-current
  • Wszystkie obrazy mają alt text
  • Formularz ma label dla każdego pola
  • Fieldset i legend dla grup pól
  • aria-required i aria-describedby w formularzu
  • Kontrast kolorów minimum 4.5:1
  • Widoczny focus na wszystkich elementach
  • Strona działa na 200% powiększenia
  • prefers-reduced-motion wspierane
  • prefers-contrast wspierane
  • prefers-color-scheme wspierane
  • aria-live dla komunikatów i aria-expanded dla rozwijanych elementów

Każdy punkt sprawdzisz narzędziami z lekcji o testowaniu: Lighthouse i axe na start, potem klawiatura, powiększenie i czytnik ekranowy. Link do strony wyślij razem z krótkim opisem zastosowanych technik.

Gratulacje! Stworzyłeś stronę, która jest naprawdę dostępna dla wszystkich - tak jak starożytne świątynie Egiptu były otwarte dla każdego pielgrzyma. Mohamed byłby dumny! W następnym module poznasz narzędzia, które przyspieszą Twoją pracę - od DevTools po publikację strony w internecie.

Praktyka

W edytorze poniżej znajdziesz kompletną wersję projektu. Przejdź ją z checklistą w ręku, zanim zbudujesz własną:

Kod do tej lekcji: index.html
1<!DOCTYPE html>
2<html lang="pl">
3<head>
4    <meta charset="UTF-8">
5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6    <title>Muzeum Starorzytnego Egiptu - Dostepna Strona</title>
7    <link rel="stylesheet" href="style.css">
8</head>
9<body>
10    <!-- Skip link -->
11    <a href="#main-content" class="skip-link">
12        Przejdz do tresci glownej
13    </a>
14
15    <!-- Header -->
16    <header role="banner">
17        <div class="header-inner">
18            <h1>
19                <span aria-hidden="true">&#9653;</span>
20                Muzeum Starorzytnego Egiptu
21            </h1>
22            <nav aria-label="Nawigacja glowna">
23                <ul>
24                    <li><a href="#exhibitions" aria-current="page">Wystawy</a></li>
25                    <li><a href="#reservation">Rezerwacja</a></li>
26                    <li><a href="#gallery">Galeria</a></li>
27                    <li><a href="#contact">Kontakt</a></li>
28                </ul>
29            </nav>
30        </div>
31    </header>
32
33    <!-- Main content -->
34    <main id="main-content">
35        <!-- Exhibitions section -->
36        <section id="exhibitions" aria-labelledby="ex-title">
37            <h2 id="ex-title">Aktualne wystawy</h2>
38
39            <article>
40                <h3>Skarby Tutanchamona</h3>
41                <figure>
42                    <img src="https://placehold.co/600x350/d4af37/1a1a2e?text=Maska+Tutanchamona"
43                         alt="Zlota maska posmiertna faraona Tutanchamona z inkrustacjami z lapis lazuli">
44                    <figcaption>
45                        Maska posmiertna Tutanchamona, XVIII dynastia, ok. 1323 p.n.e.
46                    </figcaption>
47                </figure>
48                <p>Odkryj ponad 5000 niezwyklych artefaktow z grobowca
49                   mlodego faraona, odkrytego w 1922 roku.</p>
50                <a href="#reservation" class="btn">Zarezerwuj wizyte</a>
51            </article>
52
53            <article>
54                <h3>Mumie Krolewskie</h3>
55                <p>Poznaj tajemnice mumifikacji i obejrzyj zachowane
56                   mumie faraonow z Doliny Krolow.</p>
57                <a href="#reservation" class="btn">Zarezerwuj wizyte</a>
58            </article>
59        </section>
60
61        <!-- Reservation form -->
62        <section id="reservation" aria-labelledby="res-title">
63            <h2 id="res-title">Zarezerwuj wizyte</h2>
64
65            <form aria-labelledby="res-title" novalidate>
66                <fieldset>
67                    <legend>Dane osobowe</legend>
68
69                    <div class="form-group">
70                        <label for="name">
71                            Imie i nazwisko
72                            <span class="required" aria-hidden="true">*</span>
73                        </label>
74                        <input type="text" id="name"
75                               autocomplete="name"
76                               aria-required="true">
77                    </div>
78
79                    <div class="form-group">
80                        <label for="email">
81                            Adres e-mail
82                            <span class="required" aria-hidden="true">*</span>
83                        </label>
84                        <input type="email" id="email"
85                               autocomplete="email"
86                               aria-required="true"
87                               aria-describedby="email-help">
88                        <p id="email-help" class="hint">
89                            Na ten adres wyslemy potwierdzenie.
90                        </p>
91                    </div>
92                </fieldset>
93
94                <fieldset>
95                    <legend>Wybierz wystawe</legend>
96                    <label class="radio-label">
97                        <input type="radio" name="exhibition" value="tutankhamun">
98                        Skarby Tutanchamona
99                    </label>
100                    <label class="radio-label">
101                        <input type="radio" name="exhibition" value="mummies">
102                        Mumie Krolewskie
103                    </label>
104                </fieldset>
105
106                <fieldset>
107                    <legend>Szczegoly wizyty</legend>
108                    <div class="form-row">
109                        <div class="form-group">
110                            <label for="visit-date">Data wizyty:</label>
111                            <input type="date" id="visit-date" aria-required="true">
112                        </div>
113                        <div class="form-group">
114                            <label for="visitors">Liczba osob:</label>
115                            <input type="number" id="visitors" min="1" max="20" value="1">
116                        </div>
117                    </div>
118                </fieldset>
119
120                <button type="submit" class="btn btn-primary">
121                    Zarezerwuj wizyte
122                </button>
123            </form>
124        </section>
125    </main>
126
127    <!-- Sidebar -->
128    <aside aria-label="Informacje praktyczne">
129        <h2>Godziny otwarcia</h2>
130        <dl>
131            <dt>Poniedzialek - Piaatek</dt>
132            <dd>9:00 - 17:00</dd>
133            <dt>Sobota - Niedziela</dt>
134            <dd>10:00 - 18:00</dd>
135        </dl>
136
137        <h2>Cennik</h2>
138        <dl>
139            <dt>Bilet normalny</dt>
140            <dd>45 zl</dd>
141            <dt>Bilet ulgowy</dt>
142            <dd>25 zl</dd>
143            <dt>Bilet rodzinny</dt>
144            <dd>100 zl (2+2)</dd>
145        </dl>
146    </aside>
147
148    <!-- Footer -->
149    <footer>
150        <nav aria-label="Nawigacja stopki">
151            <ul>
152                <li><a href="/privacy">Polityka prywatnosci</a></li>
153                <li><a href="/accessibility">Deklaracja dostepnosci</a></li>
154                <li><a href="/sitemap">Mapa strony</a></li>
155            </ul>
156        </nav>
157        <p>Muzeum Starorzytnego Egiptu &copy; 2024. Wszelkie prawa zastrzezone.</p>
158    </footer>
159</body>
160</html>

Pamiętaj: dostępność to nie ozdoba dodana na końcu, lecz fundament piramidy - buduj ją od pierwszego bloku, a każdy pielgrzym dotrze do komnaty faraona.

Widzisz błąd w tej lekcji?

Przydatne artykuły