Kurs HTML i CSS · Moduł 10: Dostępność (a11y)
Projekt: Dostępna strona Muzeum Egipskiego
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ć:
- Skip link - przeskakujący do treści głównej
- Semantyczna struktura - header, nav, main, aside, footer
- Dostępna nawigacja - z aria-label i aria-current
- Sekcja wystawy - z dostępnymi obrazami (alt, figure, figcaption)
- Formularz rezerwacji - z labelami, fieldset, legend, walidacją
- Sekcja multimedialna - z napisami i transkrypcją
- Kontrast i typografia - spełniające WCAG AA
- Nawigacja klawiaturowa - widoczny focus, logiczna kolejność Tab
- ARIA atrybuty - aria-live, aria-expanded, aria-describedby
Struktura HTML
Krok 1: head, skip link i nawigacja
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ść
Krok 6: kolory, skip link i focus
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">▵</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 © 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?