HTML5 i Accessibility - Pytania Rekrutacyjne 2026
HTML5 i dostępność (accessibility, a11y) to tematy, które wielu kandydatów frontend ignoruje - i płacą za to na rozmowach. W 2026 roku firmy coraz częściej pytają o semantyczny HTML, ARIA, WCAG i testowanie dostępności. Nie tylko dlatego, że to "słuszne" - ale dlatego, że przepisy prawne (European Accessibility Act) wymuszają dostępne strony.
W tym przewodniku znajdziesz 37 pytań z HTML5 i accessibility - od podstaw semantyki po zaawansowane ARIA patterns. Każda odpowiedź w formacie "30 sekund / 2 minuty".
Spis Treści
- Podstawy HTML5
- Elementy Semantyczne
- Formularze HTML5
- Multimedia: Audio i Video
- Canvas i Grafika
- Storage i Offline
- Accessibility - Podstawy
- ARIA - Accessible Rich Internet Applications
- WCAG i Testowanie
- Pytania Praktyczne
Podstawy HTML5
1. Co oznacza "5" w nazwie HTML5 i jakie problemy rozwiązuje?
Odpowiedź w 30 sekund:
HTML5 to piąta wersja standardu HTML, wprowadzona w 2014 roku. Rozwiązuje problemy poprzednich wersji: brak natywnych multimediów (wymagały Flash), słaba semantyka (wszystko w div), brak API do storage, geolokacji, canvas. HTML5 to także "living standard" - ciągle ewoluuje.
Odpowiedź w 2 minuty:
HTML5 wprowadził szereg innowacyjnych rozwiązań, które wyeliminowały konieczność używania zewnętrznych technologii jak Flash i uprościły tworzenie nowoczesnych aplikacji webowych:
| Problem HTML4 | Rozwiązanie HTML5 |
|---|---|
| Brak wideo/audio |
<video>, <audio> natywnie |
Wszystko w <div>
|
Elementy semantyczne |
| Flash dla animacji |
<canvas>, SVG |
| Cookies jedyne storage | localStorage, sessionStorage |
| Brak walidacji formularzy | Typy input, required, pattern |
| Brak geolokacji | Geolocation API |
| Brak offline | Service Workers, Cache API |
Kluczowe cechy HTML5:
<!-- Prostsza deklaracja DOCTYPE -->
<!DOCTYPE html>
<!-- Semantyczna struktura -->
<header>
<nav>...</nav>
</header>
<main>
<article>...</article>
<aside>...</aside>
</main>
<footer>...</footer>
<!-- Natywne multimedia -->
<video src="movie.mp4" controls></video>
<audio src="song.mp3" controls></audio>
<!-- Nowe typy input -->
<input type="email" required>
<input type="date">
<input type="range" min="0" max="100">
HTML5 jako "Living Standard":
- W3C i WHATWG utrzymują standard
- Ciągłe aktualizacje bez numerów wersji
- Nowe elementy:
<dialog>,<details>,<summary> - Nowe API: Web Components, Intersection Observer
2. Jaki jest cel deklaracji ?
Odpowiedź w 30 sekund:
<!DOCTYPE html> informuje przeglądarkę, że dokument jest HTML5 i powinna użyć trybu standardów (standards mode) zamiast quirks mode. Bez DOCTYPE przeglądarka może renderować stronę w trybie kompatybilności ze starymi stronami, co powoduje niespójne zachowanie CSS i JS.
Odpowiedź w 2 minuty:
DOCTYPE kontroluje tryb renderowania przeglądarki - poniższa tabela pokazuje trzy główne tryby i ich wpływ na interpretację HTML i CSS:
| Tryb | Kiedy | Zachowanie |
|---|---|---|
| Standards Mode | <!DOCTYPE html> |
Zgodne ze specyfikacją |
| Quirks Mode | Brak DOCTYPE | Emulacja IE5/Netscape |
| Almost Standards | Stare DOCTYPE | Prawie standardy |
Przykłady DOCTYPE:
<!-- HTML5 (zalecane) -->
<!DOCTYPE html>
<!-- HTML 4.01 Strict -->
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<!-- XHTML 1.0 -->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
Dlaczego HTML5 DOCTYPE jest prosty:
<!DOCTYPE html>
<!-- To wystarczy! Nie potrzeba DTD URL -->
Sprawdzenie trybu w JavaScript:
console.log(document.compatMode);
// "CSS1Compat" = standards mode
// "BackCompat" = quirks mode
Różnice w quirks mode:
- Box model działa inaczej (IE5 box model)
- Tabele dziedziczą font-size
- Inline elementy mają inne marginesy
- CSS specificity może działać inaczej
3. Jakie nowe elementy strukturalne wprowadzono w HTML5?
Odpowiedź w 30 sekund:
HTML5 wprowadził elementy semantyczne: <header>, <nav>, <main>, <article>, <section>, <aside>, <footer>, <figure>, <figcaption>. Zastępują generyczne <div> nadając znaczenie strukturze dokumentu. Pomagają screen readerom, SEO i utrzymaniu kodu.
Odpowiedź w 2 minuty:
Struktura strony HTML5:
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Strona</title>
</head>
<body>
<header>
<h1>Logo</h1>
<nav>
<ul>
<li><a href="/">Start</a></li>
<li><a href="/about">O nas</a></li>
</ul>
</nav>
</header>
<main>
<article>
<header>
<h2>Tytuł artykułu</h2>
<time datetime="2026-01-07">7 stycznia 2026</time>
</header>
<p>Treść artykułu...</p>
<figure>
<img src="photo.jpg" alt="Opis zdjęcia">
<figcaption>Podpis pod zdjęciem</figcaption>
</figure>
<footer>
<p>Autor: Jan Kowalski</p>
</footer>
</article>
<aside>
<h3>Powiązane artykuły</h3>
<ul>...</ul>
</aside>
</main>
<footer>
<p>© 2026 Firma</p>
</footer>
</body>
</html>
Znaczenie elementów:
| Element | Znaczenie | Landmark ARIA |
|---|---|---|
<header> |
Nagłówek strony/sekcji | banner (tylko top-level) |
<nav> |
Główna nawigacja | navigation |
<main> |
Główna treść (1 na stronę) | main |
<article> |
Niezależna treść | article |
<section> |
Tematyczna grupa treści | region (z aria-label) |
<aside> |
Treść poboczna | complementary |
<footer> |
Stopka strony/sekcji | contentinfo (tylko top-level) |
Elementy Semantyczne
4. Czym różni się <section> od <article> i <div>?
Odpowiedź w 30 sekund:
<article> to niezależna, samodzielna treść (post na blogu, komentarz, widget). <section> to tematyczna grupa treści w ramach dokumentu. <div> nie ma znaczenia semantycznego - używaj gdy potrzebujesz kontenera do stylowania. Zasada: czy treść ma sens samodzielnie? → article. Czy to tematyczna sekcja? → section.
Odpowiedź w 2 minuty:
Kiedy używać którego:
<!-- ARTICLE: niezależna treść, ma sens sama -->
<article>
<h2>Post na blogu</h2>
<p>Treść posta...</p>
<footer>Autor: Jan</footer>
</article>
<!-- Komentarze też są article (zagnieżdżone) -->
<article>
<h2>Artykuł główny</h2>
<p>Treść...</p>
<section>
<h3>Komentarze</h3>
<article>
<p>Świetny artykuł!</p>
<footer>Anna</footer>
</article>
<article>
<p>Zgadzam się</p>
<footer>Piotr</footer>
</article>
</section>
</article>
<!-- SECTION: tematyczna grupa -->
<section>
<h2>Nasze usługi</h2>
<p>Oferujemy...</p>
</section>
<section>
<h2>Cennik</h2>
<table>...</table>
</section>
<!-- DIV: brak znaczenia, tylko styling -->
<div class="card-container">
<article class="card">...</article>
<article class="card">...</article>
</div>
Pytanie testowe:
<!-- Czy to article czy section? -->
<??? >
<h2>Prognoza pogody</h2>
<p>Dziś: 15°C, słonecznie</p>
</???>
Odpowiedź: <article> - prognoza pogody jest niezależną jednostką treści, która może być syndykowana (np. w RSS), wyświetlona osobno na innej stronie.
Zasada kciuka:
- Czy możesz to wkleić w RSS feed? →
<article> - Czy to sekcja większego dokumentu? →
<section> - Czy potrzebujesz tylko kontenera CSS? →
<div>
5. Jakie jest znaczenie elementu <main> i ile razy może wystąpić na stronie?
Odpowiedź w 30 sekund:
<main> oznacza główną treść dokumentu - unikatową dla tej strony, bez powtarzających się elementów (nawigacja, sidebar, stopka). Powinien wystąpić raz na stronę. Screen readery używają go do przeskoczenia do głównej treści. Nie może być potomkiem <article>, <aside>, <header>, <footer>, <nav>.
Odpowiedź w 2 minuty:
Poprawne użycie:
<body>
<header>
<nav>...</nav>
</header>
<main>
<!-- Główna, unikatowa treść strony -->
<h1>Witamy w sklepie</h1>
<section>
<h2>Polecane produkty</h2>
...
</section>
</main>
<aside>
<!-- Sidebar NIE jest w main -->
</aside>
<footer>...</footer>
</body>
Błędne użycie:
<!-- ❌ Wiele main -->
<main>...</main>
<main>...</main>
<!-- ❌ main w article -->
<article>
<main>...</main>
</article>
<!-- ❌ main w aside -->
<aside>
<main>...</main>
</aside>
Wyjątek - hidden main:
<!-- Dozwolone: wiele main jeśli tylko jeden widoczny -->
<main>Strona główna</main>
<main hidden>Strona produktu</main>
<!-- Lub z inert (SPA) -->
<main inert>Nieaktywna strona</main>
<main>Aktywna strona</main>
Skip link do main:
<body>
<a href="#main-content" class="skip-link">
Przejdź do głównej treści
</a>
<header>...</header>
<main id="main-content">
...
</main>
</body>
<style>
.skip-link {
position: absolute;
left: -9999px;
}
.skip-link:focus {
left: 0;
top: 0;
z-index: 1000;
}
</style>
6. Kiedy używać <figure> i <figcaption>?
Odpowiedź w 30 sekund:
<figure> grupuje samodzielną treść (obraz, diagram, kod, cytat) z opcjonalnym podpisem <figcaption>. Używaj gdy treść jest referencjonowana z głównego tekstu, ale mogłaby być przeniesiona (np. do appendixu) bez utraty kontekstu. Figcaption może być na początku lub końcu figure.
Odpowiedź w 2 minuty:
Typowe użycia:
<!-- Obraz z podpisem -->
<figure>
<img src="chart.png" alt="Wykres sprzedaży Q4 2025">
<figcaption>Rys. 1: Sprzedaż w Q4 2025 wzrosła o 25%</figcaption>
</figure>
<!-- Kod z opisem -->
<figure>
<pre><code>
function hello() {
console.log('Hello');
}
</code></pre>
<figcaption>Listing 1: Funkcja powitalna</figcaption>
</figure>
<!-- Cytat -->
<figure>
<blockquote>
Prostota jest szczytem wyrafinowania.
</blockquote>
<figcaption>— Leonardo da Vinci</figcaption>
</figure>
<!-- Wiele obrazów z jednym podpisem -->
<figure>
<img src="before.jpg" alt="Przed remontem">
<img src="after.jpg" alt="Po remoncie">
<figcaption>Porównanie przed i po remoncie</figcaption>
</figure>
Kiedy NIE używać figure:
<!-- ❌ Dekoracyjny obraz bez znaczenia -->
<figure>
<img src="decorative-line.png" alt="">
</figure>
<!-- ✅ Lepiej: po prostu img lub CSS background -->
<img src="decorative-line.png" alt="" role="presentation">
Figure vs img:
<!-- Tylko obraz (bez podpisu, inline) -->
<p>Logo firmy: <img src="logo.png" alt="Logo Acme"></p>
<!-- Obraz z podpisem (blokowy, samodzielny) -->
<figure>
<img src="team.jpg" alt="Zespół Acme na konferencji">
<figcaption>Nasz zespół na DevConf 2025</figcaption>
</figure>
Formularze HTML5
7. Jakie nowe typy input wprowadzono w HTML5?
Odpowiedź w 30 sekund:
HTML5 dodał: email, url, tel, number, range, date, time, datetime-local, month, week, color, search. Zapewniają natywną walidację, odpowiednią klawiaturę na mobile (np. @ dla email), i date pickery bez JavaScript.
Odpowiedź w 2 minuty:
Przegląd typów:
<!-- Tekst z walidacją -->
<input type="email" placeholder="jan@example.com">
<input type="url" placeholder="https://example.com">
<input type="tel" placeholder="+48 123 456 789">
<!-- Liczby -->
<input type="number" min="0" max="100" step="5">
<input type="range" min="0" max="100" value="50">
<!-- Data i czas -->
<input type="date"> <!-- 2026-01-07 -->
<input type="time"> <!-- 14:30 -->
<input type="datetime-local"> <!-- 2026-01-07T14:30 -->
<input type="month"> <!-- 2026-01 -->
<input type="week"> <!-- 2026-W02 -->
<!-- Inne -->
<input type="color" value="#ff0000">
<input type="search" placeholder="Szukaj...">
Korzyści na mobile:
| Typ | Klawiatura mobile |
|---|---|
email |
@ i . łatwo dostępne |
tel |
Klawiatura numeryczna |
url |
/ i .com dostępne |
number |
Klawiatura numeryczna |
Fallback dla starszych przeglądarek:
<input type="date" id="birthday">
<script>
// Sprawdź czy przeglądarka wspiera type="date"
const input = document.createElement('input');
input.type = 'date';
if (input.type === 'text') {
// Fallback - załaduj date picker JS
loadDatePickerLibrary();
}
</script>
Walidacja z pattern:
<input
type="tel"
pattern="[0-9]{3}-[0-9]{3}-[0-9]{3}"
placeholder="123-456-789"
title="Format: 123-456-789"
>
8. Jak działa natywna walidacja formularzy w HTML5?
Odpowiedź w 30 sekund:
HTML5 oferuje atrybuty walidacji: required, pattern, min/max, minlength/maxlength, type (email, url). Przeglądarka blokuje submit i pokazuje komunikat błędu. Możesz customizować komunikaty przez setCustomValidity() i stylować przez pseudo-klasy :valid, :invalid.
Odpowiedź w 2 minuty:
Atrybuty walidacji:
<form>
<!-- Required -->
<input type="text" name="name" required>
<!-- Min/Max length -->
<input type="text" minlength="3" maxlength="50">
<!-- Pattern (regex) -->
<input
type="text"
pattern="[A-Za-z]{3,}"
title="Minimum 3 litery"
>
<!-- Number range -->
<input type="number" min="1" max="10" step="1">
<!-- Email (wbudowany pattern) -->
<input type="email" required>
<button type="submit">Wyślij</button>
</form>
Custom komunikaty:
<input
type="email"
id="email"
required
oninvalid="this.setCustomValidity('Proszę podać poprawny email')"
oninput="this.setCustomValidity('')"
>
JavaScript Validation API:
const form = document.querySelector('form');
const email = document.querySelector('#email');
// Sprawdzenie validity
console.log(email.validity.valueMissing); // true jeśli puste a required
console.log(email.validity.typeMismatch); // true jeśli nie email
console.log(email.validity.patternMismatch);
console.log(email.validity.tooShort);
console.log(email.validity.tooLong);
console.log(email.validity.valid); // true jeśli wszystko OK
// Custom walidacja
email.addEventListener('input', () => {
if (email.value.includes('test')) {
email.setCustomValidity('Email nie może zawierać "test"');
} else {
email.setCustomValidity('');
}
});
// Wyłączenie natywnej walidacji
form.noValidate = true;
// Lub w HTML: <form novalidate>
CSS styling:
input:valid {
border-color: green;
}
input:invalid {
border-color: red;
}
input:required {
border-left: 3px solid orange;
}
/* Pokaż błąd tylko po interakcji */
input:invalid:not(:placeholder-shown) {
border-color: red;
}
9. Do czego służą atrybuty autocomplete i autofocus?
Odpowiedź w 30 sekund:
autocomplete kontroluje podpowiedzi przeglądarki - wartości: on, off, lub konkretne typy (name, email, tel, address-line1). Pomaga autofill i password managerom. autofocus ustawia fokus na elemencie po załadowaniu strony - używaj oszczędnie (jeden na stronę, może dezorientować użytkowników AT).
Odpowiedź w 2 minuty:
Autocomplete wartości:
<form autocomplete="on">
<!-- Dane osobowe -->
<input type="text" name="name" autocomplete="name">
<input type="email" autocomplete="email">
<input type="tel" autocomplete="tel">
<!-- Adres -->
<input type="text" autocomplete="address-line1">
<input type="text" autocomplete="address-line2">
<input type="text" autocomplete="postal-code">
<input type="text" autocomplete="country">
<!-- Karta kredytowa -->
<input type="text" autocomplete="cc-number">
<input type="text" autocomplete="cc-exp">
<input type="text" autocomplete="cc-csc">
<!-- Wyłączenie dla wrażliwych pól -->
<input type="text" autocomplete="off">
<!-- One-time code (SMS) -->
<input type="text" autocomplete="one-time-code">
</form>
Autofocus:
<!-- Fokus po załadowaniu strony -->
<input type="search" autofocus placeholder="Szukaj...">
<!-- Ale UWAGA: -->
<!-- ❌ Nie używaj na formularzach w środku strony -->
<!-- ❌ Może zdezorientować screen reader users -->
<!-- ❌ Tylko jeden element może mieć autofocus -->
Lepsze rozwiązanie niż autofocus:
// Ustaw fokus warunkowo
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
document.querySelector('#search').focus();
}
// Lub po user action
dialog.addEventListener('open', () => {
dialog.querySelector('input').focus();
});
Autocomplete a bezpieczeństwo:
<!-- ❌ Nie używaj autocomplete="off" na hasłach -->
<!-- Password managery potrzebują autocomplete -->
<input type="password" autocomplete="current-password">
<input type="password" autocomplete="new-password">
Multimedia: Audio i Video
10. Jak używać elementów <video> i <audio> z wieloma źródłami?
Odpowiedź w 30 sekund:
Użyj wielu <source> wewnątrz <video>/<audio> z różnymi formatami. Przeglądarka wybierze pierwszy wspierany. Atrybuty: controls (pokazuje UI), autoplay (wymaga muted), loop, preload. Fallback tekst wyświetli się jeśli żaden format nie działa.
Odpowiedź w 2 minuty:
Video z wieloma źródłami:
<video controls width="640" height="360" poster="thumbnail.jpg">
<!-- Przeglądarka wybierze pierwszy wspierany -->
<source src="movie.webm" type="video/webm">
<source src="movie.mp4" type="video/mp4">
<source src="movie.ogv" type="video/ogg">
<!-- Fallback dla starych przeglądarek -->
<p>
Twoja przeglądarka nie wspiera HTML5 video.
<a href="movie.mp4">Pobierz wideo</a>
</p>
</video>
Audio:
<audio controls>
<source src="song.opus" type="audio/opus">
<source src="song.ogg" type="audio/ogg">
<source src="song.mp3" type="audio/mpeg">
<p>Twoja przeglądarka nie wspiera HTML5 audio.</p>
</audio>
Atrybuty:
<video
src="movie.mp4"
controls <!-- Pokazuje kontrolki -->
autoplay <!-- Auto odtwarzanie (wymaga muted) -->
muted <!-- Wyciszony -->
loop <!-- Zapętlenie -->
playsinline <!-- Nie fullscreen na iOS -->
preload="metadata" <!-- none | metadata | auto -->
poster="thumb.jpg" <!-- Miniatura -->
width="640"
height="360"
>
</video>
JavaScript API:
const video = document.querySelector('video');
// Kontrola
video.play();
video.pause();
video.currentTime = 30; // Przeskocz do 30s
// Eventy
video.addEventListener('play', () => console.log('Started'));
video.addEventListener('pause', () => console.log('Paused'));
video.addEventListener('ended', () => console.log('Finished'));
video.addEventListener('timeupdate', () => {
console.log(video.currentTime);
});
// Właściwości
console.log(video.duration); // Długość w sekundach
console.log(video.paused); // true/false
console.log(video.volume); // 0-1
Napisy:
<video controls>
<source src="movie.mp4" type="video/mp4">
<track
kind="subtitles"
src="subs-pl.vtt"
srclang="pl"
label="Polski"
default
>
<track
kind="subtitles"
src="subs-en.vtt"
srclang="en"
label="English"
>
</video>
11. Jak zapewnić dostępność multimediów?
Odpowiedź w 30 sekund:
Dostępne multimedia wymagają: napisów (captions) dla głuchych, transkrypcji dla audio, audiodeskrypcji dla niewidomych, kontrolek dostępnych klawiaturą. Użyj <track> dla napisów VTT. Unikaj autoplay (dezorientuje screen readers). Zapewnij kontrolę głośności i pauzy.
Odpowiedź w 2 minuty:
Napisy i transkrypcje:
<video controls>
<source src="lecture.mp4" type="video/mp4">
<!-- Napisy (captions) - dialog + dźwięki -->
<track
kind="captions"
src="captions-pl.vtt"
srclang="pl"
label="Polski (napisy)"
default
>
<!-- Subtitles - tylko dialog -->
<track
kind="subtitles"
src="subtitles-en.vtt"
srclang="en"
label="English"
>
<!-- Opisy (dla niewidomych) -->
<track
kind="descriptions"
src="descriptions-pl.vtt"
srclang="pl"
label="Audiodeskrypcja"
>
</video>
<!-- Transkrypcja pod wideo -->
<details>
<summary>Transkrypcja</summary>
<p>
[00:00] Witam na wykładzie o HTML5...
[00:15] Dzisiaj omówimy...
</p>
</details>
Format VTT:
WEBVTT
00:00:00.000 --> 00:00:03.000
Witam na wykładzie o HTML5.
00:00:03.500 --> 00:00:07.000
Dzisiaj omówimy dostępność multimediów.
00:00:07.500 --> 00:00:12.000
[dźwięk klaskania]
Kontrolki dostępne klawiaturą:
<!-- Natywne controls są dostępne -->
<video controls>...</video>
<!-- Custom kontrolki muszą być focusable -->
<div class="video-player">
<video id="myVideo">...</video>
<div class="controls">
<button aria-label="Odtwórz" onclick="togglePlay()">▶</button>
<button aria-label="Wycisz" onclick="toggleMute()">🔊</button>
<input
type="range"
aria-label="Głośność"
min="0"
max="100"
onchange="setVolume(this.value)"
>
</div>
</div>
Autoplay - best practices:
<!-- ❌ Autoplay z dźwiękiem -->
<video autoplay src="ad.mp4"></video>
<!-- ✅ Autoplay wyciszony -->
<video autoplay muted src="background.mp4"></video>
<!-- ✅ Lub pozwól użytkownikowi zdecydować -->
<video controls src="movie.mp4"></video>
Canvas i Grafika
12. Jaka jest różnica między <canvas> a <svg>?
Odpowiedź w 30 sekund:
<canvas> to rastrowa grafika renderowana przez JavaScript pixel po pixelu - dobre dla gier, wizualizacji danych, manipulacji obrazów. <svg> to wektorowa grafika w DOM - skaluje bez utraty jakości, elementy są dostępne przez CSS/JS, lepsze dla ikon, logo, diagramów. Canvas jest szybszy dla wielu obiektów, SVG lepszy dla interaktywnych elementów.
Odpowiedź w 2 minuty:
Porównanie:
| Cecha | Canvas | SVG |
|---|---|---|
| Typ | Rastrowy (piksele) | Wektorowy (ścieżki) |
| Skalowanie | Pikselizacja | Bez utraty jakości |
| DOM | Jeden element | Każdy kształt w DOM |
| Interakcja | Ręczna (hit detection) | Event handlers na elementach |
| Wydajność | Lepsza dla wielu obiektów | Wolniejsza przy wielu elementach |
| Accessibility | Brak (wymaga fallback) | Text i ARIA dostępne |
| Animacje | requestAnimationFrame | CSS/SMIL/JS |
Canvas - przykład:
<canvas id="myCanvas" width="400" height="200">
<!-- Fallback dla screen readers -->
Wykres sprzedaży pokazujący wzrost o 25% w Q4.
</canvas>
<script>
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// Rysowanie
ctx.fillStyle = 'blue';
ctx.fillRect(10, 10, 100, 80);
ctx.beginPath();
ctx.arc(200, 100, 50, 0, Math.PI * 2);
ctx.fillStyle = 'red';
ctx.fill();
ctx.font = '20px Arial';
ctx.fillStyle = 'black';
ctx.fillText('Hello Canvas', 250, 100);
</script>
SVG - przykład:
<svg width="400" height="200" role="img" aria-labelledby="title desc">
<title id="title">Wykres sprzedaży</title>
<desc id="desc">Wykres pokazujący wzrost sprzedaży o 25% w Q4</desc>
<rect x="10" y="10" width="100" height="80" fill="blue"/>
<circle cx="200" cy="100" r="50" fill="red"/>
<text x="250" y="100" font-size="20">Hello SVG</text>
</svg>
Kiedy używać którego:
| Użycie | Wybór |
|---|---|
| Ikony, logo | SVG |
| Gry | Canvas |
| Wykresy (prosty) | SVG |
| Wykresy (dużo danych) | Canvas |
| Edytor graficzny | Canvas |
| Interaktywne diagramy | SVG |
| Mapy | SVG lub Canvas |
| Manipulacja obrazów | Canvas |
13. Jak zapewnić dostępność elementu <canvas>?
Odpowiedź w 30 sekund:
Canvas jest z natury niedostępny - to tylko piksele. Rozwiązania: fallback content wewnątrz <canvas>, aria-label dla opisu, Shadow DOM dla interaktywnych elementów, osobna tabela/lista danych dla wykresów. Dla złożonych canvas rozważ SVG lub dodatkowy tekst opisujący content.
Odpowiedź w 2 minuty:
Podstawowy fallback:
<canvas id="chart" width="600" height="400" role="img" aria-label="Wykres słupkowy sprzedaży">
<!-- Fallback content dla screen readers i gdy JS wyłączony -->
<h3>Sprzedaż kwartalna 2025</h3>
<table>
<tr><th>Kwartał</th><th>Sprzedaż</th></tr>
<tr><td>Q1</td><td>100 000 zł</td></tr>
<tr><td>Q2</td><td>120 000 zł</td></tr>
<tr><td>Q3</td><td>90 000 zł</td></tr>
<tr><td>Q4</td><td>150 000 zł</td></tr>
</table>
</canvas>
Aria describedby:
<canvas id="gameCanvas" aria-describedby="game-description">
</canvas>
<p id="game-description" class="visually-hidden">
Gra platformowa. Użyj strzałek do ruchu, spacji do skoku.
Aktualny wynik: <span id="score">0</span> punktów.
</p>
Aktualizacja dla dynamicznego content:
const canvas = document.getElementById('gameCanvas');
const scoreSpan = document.getElementById('score');
function updateScore(newScore) {
score = newScore;
scoreSpan.textContent = newScore;
// Ogłoś dla screen readers
const announcement = document.getElementById('announcement');
announcement.textContent = `Wynik: ${newScore} punktów`;
}
<div id="announcement" aria-live="polite" class="visually-hidden"></div>
Interaktywny canvas z hit regions (deprecated, ale koncept):
// Nowoczesne podejście: dodaj focusable elementy
const canvas = document.getElementById('mapCanvas');
// Dla każdego interaktywnego regionu dodaj ukryty button
regions.forEach(region => {
const btn = document.createElement('button');
btn.textContent = region.name;
btn.className = 'visually-hidden';
btn.addEventListener('focus', () => highlightRegion(region));
btn.addEventListener('click', () => selectRegion(region));
canvas.parentElement.appendChild(btn);
});
Storage i Offline
14. Czym różni się localStorage od sessionStorage i cookies?
Odpowiedź w 30 sekund:
localStorage - persystentny, ~5MB, tylko klient. sessionStorage - do zamknięcia karty, ~5MB, per karta. Cookies - wysyłane z każdym requestem, ~4KB, można ustawić expiration i httpOnly. Używaj localStorage dla preferencji użytkownika, sessionStorage dla tymczasowych danych, cookies dla auth tokenów.
Odpowiedź w 2 minuty:
Porównanie:
| Cecha | localStorage | sessionStorage | Cookies |
|---|---|---|---|
| Pojemność | ~5-10 MB | ~5-10 MB | ~4 KB |
| Czas życia | Permanentny | Do zamknięcia karty | Configurable |
| Wysyłany do serwera | Nie | Nie | Tak (każdy request) |
| Dostęp JS | Tak | Tak | Tak (bez httpOnly) |
| Scope | Origin | Origin + karta | Origin + path |
| API | Synchroniczne | Synchroniczne | document.cookie |
Użycie:
// localStorage - preferencje, cache
localStorage.setItem('theme', 'dark');
localStorage.setItem('user', JSON.stringify({ name: 'Jan' }));
const theme = localStorage.getItem('theme');
localStorage.removeItem('theme');
localStorage.clear();
// sessionStorage - tymczasowe dane
sessionStorage.setItem('formDraft', JSON.stringify(formData));
const draft = JSON.parse(sessionStorage.getItem('formDraft'));
// Cookies - auth, tracking
document.cookie = 'session=abc123; max-age=86400; secure; samesite=strict';
// Odczyt cookies (tylko bez httpOnly)
const cookies = document.cookie.split(';').reduce((acc, cookie) => {
const [key, value] = cookie.trim().split('=');
acc[key] = value;
return acc;
}, {});
Kiedy używać czego:
| Scenariusz | Wybór |
|---|---|
| Theme preference | localStorage |
| Auth token (secure) | Cookie (httpOnly, secure) |
| Form draft | sessionStorage |
| Shopping cart | localStorage |
| Analytics ID | Cookie |
| Cached API response | localStorage / IndexedDB |
| Multi-tab state | localStorage + storage event |
Storage event (sync między kartami):
window.addEventListener('storage', (e) => {
if (e.key === 'theme') {
applyTheme(e.newValue);
}
});
15. Kiedy używać IndexedDB zamiast localStorage?
Odpowiedź w 30 sekund:
IndexedDB dla: dużych danych (setki MB), złożonych struktur, wyszukiwania po indeksach, operacji asynchronicznych. localStorage dla: prostych key-value, małych danych (<5MB), synchronicznego dostępu. IndexedDB to baza danych w przeglądarce z transakcjami i indeksami.
Odpowiedź w 2 minuty:
Porównanie:
| Cecha | localStorage | IndexedDB |
|---|---|---|
| Pojemność | ~5-10 MB | Setki MB - GB |
| API | Synchroniczne | Asynchroniczne |
| Struktura | Key-value (string) | Obiekty, indeksy |
| Wyszukiwanie | Tylko po kluczu | Po indeksach |
| Transakcje | Brak | Tak |
| Web Workers | Nie | Tak |
localStorage - proste dane:
// ✅ Dobre dla prostych danych
localStorage.setItem('settings', JSON.stringify({
theme: 'dark',
language: 'pl'
}));
IndexedDB - złożone dane:
// Otwarcie bazy
const request = indexedDB.open('MyApp', 1);
request.onupgradeneeded = (e) => {
const db = e.target.result;
// Tworzenie object store z indeksem
const store = db.createObjectStore('products', { keyPath: 'id' });
store.createIndex('category', 'category', { unique: false });
store.createIndex('price', 'price', { unique: false });
};
request.onsuccess = (e) => {
const db = e.target.result;
// Dodawanie danych
const tx = db.transaction('products', 'readwrite');
const store = tx.objectStore('products');
store.add({ id: 1, name: 'Laptop', category: 'electronics', price: 2999 });
// Wyszukiwanie po indeksie
const index = store.index('category');
const query = index.getAll('electronics');
query.onsuccess = () => console.log(query.result);
};
Kiedy IndexedDB:
| Scenariusz | Wybór |
|---|---|
| Offline-first app | IndexedDB |
| Cached API responses (duże) | IndexedDB |
| User preferences | localStorage |
| Image/file cache | IndexedDB |
| Form draft | localStorage/sessionStorage |
| Full-text search | IndexedDB z indeksami |
Biblioteki ułatwiające pracę z IndexedDB:
- Dexie.js
- idb (by Jake Archibald)
- localForage (fallback na localStorage)
Accessibility - Podstawy
16. Dlaczego semantyczny HTML jest fundamentem dostępności?
Odpowiedź w 30 sekund:
Semantyczne elementy (<button>, <nav>, <main>) mają wbudowaną dostępność - role, stany, fokus klawiaturą. Screen readery rozpoznają strukturę strony. Używanie <div onclick> zamiast <button> wymaga ręcznego dodania role, tabindex, keyboard events. "No ARIA is better than bad ARIA" - natywny HTML jest zawsze lepszy.
Odpowiedź w 2 minuty:
Div vs Button:
<!-- ❌ Div jako button - wymaga DUŻO pracy -->
<div
class="btn"
onclick="handleClick()"
tabindex="0"
role="button"
aria-pressed="false"
onkeydown="if(event.key==='Enter'||event.key===' ')handleClick()"
>
Kliknij mnie
</div>
<!-- ✅ Natywny button - wszystko wbudowane -->
<button onclick="handleClick()">
Kliknij mnie
</button>
Co daje button za darmo:
- Fokus klawiaturą (Tab)
- Aktywacja Enter i Space
- Role "button" dla screen readers
- Disabled state
- Form submission
Struktura strony:
<!-- Screen reader ogłasza: "Navigation landmark" -->
<nav>
<ul>
<li><a href="/">Start</a></li>
</ul>
</nav>
<!-- "Main landmark" - skok do głównej treści -->
<main>
<!-- "Heading level 1: Witamy" -->
<h1>Witamy</h1>
<!-- "Article" -->
<article>
<h2>Tytuł</h2>
<p>Treść...</p>
</article>
</main>
Formularze:
<!-- ❌ Brak powiązania label-input -->
<span>Email:</span>
<input type="email">
<!-- ✅ Screen reader: "Email, edit text, required" -->
<label for="email">Email:</label>
<input type="email" id="email" required>
<!-- ✅ Alternatywa: label owija input -->
<label>
Email:
<input type="email" required>
</label>
Obrazy:
<!-- ❌ Brak alt - screen reader czyta nazwę pliku -->
<img src="IMG_2847.jpg">
<!-- ✅ Opisowy alt -->
<img src="team.jpg" alt="Zespół Acme przy pracy w biurze">
<!-- ✅ Dekoracyjny obraz - pusty alt -->
<img src="decorative-line.png" alt="">
17. Jak prawidłowo używać nagłówków (h1-h6) dla dostępności?
Odpowiedź w 30 sekund:
Nagłówki tworzą hierarchię dokumentu - screen readery generują z nich "spis treści" do nawigacji. Zasady: jeden <h1> na stronę (tytuł), nie pomijaj poziomów (h1→h2→h3, nie h1→h3), nie używaj nagłówków do stylowania. Użytkownicy AT nawigują po nagłówkach - 67% screen reader users używa tego jako głównej nawigacji.
Odpowiedź w 2 minuty:
Poprawna hierarchia:
<h1>Sklep z elektroniką</h1>
<h2>Laptopy</h2>
<h3>Gaming</h3>
<h3>Biznesowe</h3>
<h2>Smartfony</h2>
<h3>Apple</h3>
<h3>Samsung</h3>
<h2>Akcesoria</h2>
Błędy:
<!-- ❌ Wiele h1 -->
<h1>Logo</h1>
<h1>Tytuł strony</h1>
<h1>Newsletter</h1>
<!-- ❌ Pominięty poziom -->
<h1>Tytuł</h1>
<h3>Podsekcja</h3> <!-- Gdzie jest h2? -->
<!-- ❌ Nagłówek dla stylowania -->
<h3 class="small-text">Copyright 2026</h3>
<!-- ✅ Użyj CSS -->
<p class="small-text">Copyright 2026</p>
Screen reader nawigacja:
Użytkownik wciska "H" aby przejść do następnego nagłówka:
[h1] Sklep z elektroniką
[h2] Laptopy
[h3] Gaming
[h3] Biznesowe
[h2] Smartfony
...
Wizualnie ukryty nagłówek:
<!-- Nagłówek dla AT, niewidoczny -->
<h2 class="visually-hidden">Główna nawigacja</h2>
<nav>...</nav>
<style>
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
</style>
Artykuły a nagłówki:
<main>
<h1>Blog</h1>
<article>
<h2>Pierwszy post</h2>
<h3>Sekcja 1</h3>
</article>
<article>
<h2>Drugi post</h2>
<h3>Sekcja 1</h3>
</article>
</main>
18. Jak zapewnić dostępność klawiaturową?
Odpowiedź w 30 sekund:
Wszystkie interaktywne elementy muszą być dostępne Tab (fokus) i aktywowalne Enter/Space. Natywne elementy (<button>, <a>, <input>) mają to wbudowane. Dla custom elementów: tabindex="0" (focusable), obsługa keydown (Enter, Space, Escape, strzałki). Widoczny focus indicator jest obowiązkowy.
Odpowiedź w 2 minuty:
Focus management:
<!-- ✅ Natywnie focusable -->
<button>Click me</button>
<a href="/page">Link</a>
<input type="text">
<!-- ✅ Dodanie do tab order -->
<div tabindex="0" role="button" onclick="...">Custom button</div>
<!-- Usunięcie z tab order -->
<button tabindex="-1">Skip this</button>
<!-- ❌ tabindex > 0 - zaburza naturalny order -->
<button tabindex="5">Don't do this</button>
Keyboard event handling:
customButton.addEventListener('keydown', (e) => {
switch (e.key) {
case 'Enter':
case ' ':
e.preventDefault();
handleClick();
break;
case 'Escape':
closeDropdown();
break;
}
});
Focus trap (dla modali):
function trapFocus(element) {
const focusableElements = element.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
element.addEventListener('keydown', (e) => {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
if (document.activeElement === firstElement) {
lastElement.focus();
e.preventDefault();
}
} else {
if (document.activeElement === lastElement) {
firstElement.focus();
e.preventDefault();
}
}
});
}
Focus visible:
/* ❌ Nigdy nie usuwaj outline całkowicie */
*:focus { outline: none; }
/* ✅ Custom focus indicator */
:focus {
outline: 2px solid #005fcc;
outline-offset: 2px;
}
/* ✅ Focus-visible dla mouse vs keyboard */
:focus:not(:focus-visible) {
outline: none;
}
:focus-visible {
outline: 3px solid #005fcc;
outline-offset: 2px;
}
Skip link:
<a href="#main-content" class="skip-link">
Przejdź do głównej treści
</a>
ARIA
19. Czym jest ARIA i jakie są jej główne kategorie?
Odpowiedź w 30 sekund:
ARIA (Accessible Rich Internet Applications) to atrybuty HTML dodające informacje o dostępności. Trzy kategorie: Roles (definiują typ elementu: button, alert, dialog), States (aktualny stan: aria-expanded, aria-pressed), Properties (stałe cechy: aria-label, aria-describedby). Zasada: "No ARIA is better than bad ARIA".
Odpowiedź w 2 minuty:
Roles - co to za element:
<!-- Landmark roles (lepiej użyć semantycznego HTML) -->
<div role="navigation">...</div> <!-- = <nav> -->
<div role="main">...</div> <!-- = <main> -->
<!-- Widget roles -->
<div role="button">Kliknij</div>
<div role="alert">Błąd!</div>
<div role="dialog">Modal</div>
<div role="tablist">
<div role="tab">Tab 1</div>
</div>
<!-- Live region roles -->
<div role="status">3 wiadomości</div>
<div role="alert">Błąd krytyczny!</div>
States - aktualny stan:
<!-- Expanded/collapsed -->
<button aria-expanded="false" aria-controls="menu">
Menu
</button>
<ul id="menu" hidden>...</ul>
<!-- Pressed (toggle button) -->
<button aria-pressed="false">Bold</button>
<!-- Selected -->
<div role="option" aria-selected="true">Opcja 1</div>
<!-- Disabled -->
<button aria-disabled="true">Niedostępny</button>
<!-- Busy (loading) -->
<div aria-busy="true">Ładowanie...</div>
Properties - stałe cechy:
<!-- Label (gdy brak widocznego tekstu) -->
<button aria-label="Zamknij">×</button>
<!-- Describedby (dodatkowy opis) -->
<input
type="password"
aria-describedby="password-hint"
>
<p id="password-hint">Minimum 8 znaków</p>
<!-- Labelledby (label z innego elementu) -->
<div id="section-title">Ustawienia</div>
<div aria-labelledby="section-title">
...
</div>
<!-- Required -->
<input aria-required="true">
<!-- Invalid -->
<input aria-invalid="true" aria-errormessage="email-error">
<span id="email-error">Nieprawidłowy email</span>
Pierwsza zasada ARIA:
"Jeśli możesz użyć natywnego elementu HTML z wbudowaną semantyką i zachowaniem, użyj go."
<!-- ❌ ARIA -->
<div role="button" tabindex="0">Click</div>
<!-- ✅ Natywny HTML -->
<button>Click</button>
20. Jak używać aria-live dla dynamicznych treści?
Odpowiedź w 30 sekund:
aria-live ogłasza zmiany treści dla screen readers. Wartości: polite (czeka na przerwę), assertive (natychmiast). Używaj dla: powiadomień, błędów formularzy, aktualizacji statusu. Unikaj assertive dla nieistotnych rzeczy. Zawsze testuj z prawdziwym screen reader.
Odpowiedź w 2 minuty:
Podstawowe użycie:
<!-- Polite: ogłosi gdy screen reader skończy -->
<div aria-live="polite">
3 produkty w koszyku
</div>
<!-- Assertive: przerywa i ogłasza natychmiast -->
<div aria-live="assertive">
Błąd: sesja wygasła
</div>
<!-- Off: wyłączone (domyślne) -->
<div aria-live="off">...</div>
Live regions z role:
<!-- role="status" = aria-live="polite" -->
<div role="status">Zapisano pomyślnie</div>
<!-- role="alert" = aria-live="assertive" -->
<div role="alert">Błąd krytyczny!</div>
<!-- role="log" = aria-live="polite" + aria-relevant="additions" -->
<div role="log">
<p>Użytkownik dołączył</p>
<p>Użytkownik opuścił</p>
</div>
Aria-relevant - co ogłaszać:
<div
aria-live="polite"
aria-relevant="additions text"
>
<!-- additions: nowe elementy -->
<!-- removals: usunięte elementy -->
<!-- text: zmiana tekstu -->
<!-- all: wszystko -->
</div>
Praktyczne przykłady:
<!-- Licznik produktów w koszyku -->
<div aria-live="polite" aria-atomic="true">
<span class="visually-hidden">Koszyk: </span>
<span id="cart-count">3</span> produkty
</div>
<!-- Błąd formularza -->
<input
type="email"
aria-describedby="email-error"
aria-invalid="true"
>
<span id="email-error" role="alert">
Nieprawidłowy adres email
</span>
<!-- Toast notification -->
<div id="toast" role="status" aria-live="polite" hidden>
<!-- JS zmienia treść i pokazuje -->
</div>
function showToast(message) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.hidden = false;
setTimeout(() => toast.hidden = true, 3000);
}
21. Jak prawidłowo ukrywać elementy dla różnych odbiorców?
Odpowiedź w 30 sekund:
display: none / hidden - ukrywa dla wszystkich. visibility: hidden - ukrywa wizualnie, zajmuje miejsce, niedostępne dla AT. aria-hidden="true" - widoczny, ale ukryty dla screen readers. .visually-hidden (clip) - niewidoczny, ale dostępny dla AT. Wybór zależy od celu.
Odpowiedź w 2 minuty:
Metody ukrywania:
| Metoda | Wizualnie | Screen reader | Fokus |
|---|---|---|---|
display: none |
Ukryty | Ukryty | Niedostępny |
hidden attribute |
Ukryty | Ukryty | Niedostępny |
visibility: hidden |
Ukryty | Ukryty | Niedostępny |
aria-hidden="true" |
Widoczny | Ukryty | Dostępny ❌ |
.visually-hidden |
Ukryty | Dostępny | Dostępny |
opacity: 0 |
Ukryty | Dostępny | Dostępny ⚠️ |
Visually hidden (tylko dla AT):
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Focusable version (skip links) */
.visually-hidden-focusable:not(:focus):not(:active) {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
Praktyczne użycie:
<!-- Ukryj dekoracyjną ikonę -->
<button>
<span aria-hidden="true">🛒</span>
Koszyk
</button>
<!-- Dodaj tekst tylko dla AT -->
<a href="/products">
Zobacz wszystkie
<span class="visually-hidden">produkty w kategorii laptopy</span>
</a>
<!-- Ukryj całą sekcję -->
<div hidden>To jest ukryte</div>
<!-- Ikona z tekstem dla AT -->
<button aria-label="Zamknij">
<svg aria-hidden="true">...</svg>
</button>
aria-hidden uwagi:
<!-- ❌ Nie ukrywaj focusable elementów -->
<div aria-hidden="true">
<button>Ten button jest problematyczny</button>
</div>
<!-- ✅ Jeśli musisz, dodaj inert -->
<div aria-hidden="true" inert>
<button>Teraz OK</button>
</div>
WCAG i Testowanie
22. Jakie są poziomy zgodności WCAG i co oznaczają?
Odpowiedź w 30 sekund:
WCAG (Web Content Accessibility Guidelines) definiuje trzy poziomy: A (minimum, krytyczne), AA (standard prawny w wielu krajach), AAA (najwyższy, często nierealistyczny dla całej strony). Cztery zasady: Postrzegalność, Funkcjonalność, Zrozumiałość, Solidność (POUR). Większość organizacji celuje w AA.
Odpowiedź w 2 minuty:
Poziomy:
| Poziom | Opis | Przykłady kryteriów |
|---|---|---|
| A | Minimum | Alt text, keyboard navigation, no auto-play |
| AA | Standard | Kontrast 4.5:1, resize text 200%, captions |
| AAA | Najwyższy | Kontrast 7:1, sign language, extended audio description |
POUR - zasady WCAG:
Postrzegalność (Perceivable):
- Tekst alternatywny dla obrazów
- Napisy dla wideo
- Kontrast kolorów
- Możliwość powiększenia tekstu
Funkcjonalność (Operable):
- Nawigacja klawiaturą
- Wystarczający czas na interakcję
- Brak treści wywołujących napady padaczkowe
- Skip links
Zrozumiałość (Understandable):
- Czytelny język
- Przewidywalna nawigacja
- Pomoc przy błędach
- Instrukcje dla formularzy
Solidność (Robust):
- Poprawny HTML
- Kompatybilność z AT
- Parsable przez różne user agents
Kluczowe kryteria AA:
<!-- 1.4.3 Kontrast (minimum) - 4.5:1 dla tekstu -->
<p style="color: #767676; background: #fff;">
❌ Kontrast 4.48:1 - nie przechodzi
</p>
<p style="color: #757575; background: #fff;">
✅ Kontrast 4.6:1 - przechodzi
</p>
<!-- 1.4.4 Resize text - 200% bez utraty funkcjonalności -->
<!-- Używaj jednostek względnych -->
<p style="font-size: 16px;">❌ px nie skaluje</p>
<p style="font-size: 1rem;">✅ rem skaluje</p>
<!-- 2.4.6 Headings and Labels - opisowe -->
<h2>❌ Kliknij tutaj</h2>
<h2>✅ Nasze usługi</h2>
23. Jak testować dostępność strony internetowej?
Odpowiedź w 30 sekund:
Automatyczne narzędzia (Lighthouse, axe, WAVE) wykrywają ~30% problemów. Manualne testy: nawigacja klawiaturą, test z screen reader (NVDA, VoiceOver), sprawdzenie kontrastu, test zoom 200%. User testing z osobami niepełnosprawnymi jest najcenniejszy. Automatyzuj w CI/CD ale nie polegaj tylko na tym.
Odpowiedź w 2 minuty:
Automatyczne narzędzia:
// axe-core w testach
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
test('should have no accessibility violations', async () => {
const { container } = render(<MyComponent />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
Lighthouse w CI:
# GitHub Actions
- name: Lighthouse CI
uses: treosh/lighthouse-ci-action@v9
with:
urls: |
https://example.com
budgetPath: ./lighthouse-budget.json
Manualna checklist:
-
Keyboard navigation:
- Tab przez całą stronę
- Czy fokus jest widoczny?
- Czy można aktywować wszystkie kontrolki?
- Czy można wyjść z modali (Escape)?
-
Screen reader:
- NVDA (Windows, free)
- VoiceOver (Mac, built-in)
- Czy struktura jest logiczna?
- Czy obrazy mają sensowny alt?
-
Visual:
- Kontrast (narzędzie: WebAIM Contrast Checker)
- Zoom 200% - czy coś się psuje?
- Tryb wysokiego kontrastu Windows
-
Forms:
- Czy labels są połączone?
- Czy błędy są ogłaszane?
- Czy można submitować klawiaturą?
Browser extensions:
- axe DevTools
- WAVE
- Accessibility Insights
- HeadingsMap
Screen reader testing:
NVDA (Windows):
- Insert + F7: lista elementów
- H: następny nagłówek
- D: następny landmark
- Tab: następny focusable
VoiceOver (Mac):
- VO + U: rotor
- VO + Left/Right: nawigacja
- VO + Space: aktywacja
24. Jakie są najczęstsze błędy dostępności na stronach?
Odpowiedź w 30 sekund:
Według WebAIM Million: 96% stron ma błędy WCAG. Top błędy: brak tekstu alternatywnego (58%), niski kontrast (83%), puste linki (50%), brak labels formularzy (46%), brak języka dokumentu (28%). Większość to proste do naprawienia problemy.
Odpowiedź w 2 minuty:
Top błędy i rozwiązania:
1. Niski kontrast (83% stron):
/* ❌ Kontrast 2.5:1 */
color: #999;
background: #fff;
/* ✅ Kontrast 4.5:1+ */
color: #595959;
background: #fff;
2. Brak alt text (58% stron):
<!-- ❌ -->
<img src="product.jpg">
<!-- ✅ Opisowy -->
<img src="product.jpg" alt="Czerwona sukienka, rozmiar M">
<!-- ✅ Dekoracyjny -->
<img src="decoration.png" alt="">
3. Puste linki (50% stron):
<!-- ❌ -->
<a href="/cart"><i class="icon-cart"></i></a>
<!-- ✅ -->
<a href="/cart" aria-label="Koszyk (3 produkty)">
<i class="icon-cart" aria-hidden="true"></i>
</a>
4. Brak labels (46% stron):
<!-- ❌ -->
<input type="email" placeholder="Email">
<!-- ✅ -->
<label for="email">Email</label>
<input type="email" id="email">
5. Brak języka dokumentu (28% stron):
<!-- ❌ -->
<html>
<!-- ✅ -->
<html lang="pl">
6. Brak skip links:
<!-- ✅ -->
<a href="#main" class="skip-link">
Przejdź do treści głównej
</a>
7. Keyboard traps:
// ❌ Modal bez możliwości wyjścia
// ✅ Obsłuż Escape
modal.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeModal();
});
8. Missing focus indicator:
/* ❌ */
:focus { outline: none; }
/* ✅ */
:focus-visible {
outline: 3px solid #005fcc;
}
Pytania Praktyczne
25. Jak stworzyć dostępny modal (dialog)?
Odpowiedź w 30 sekund:
Użyj <dialog> element (natywny) lub: role="dialog", aria-modal="true", aria-labelledby. Fokus trap wewnątrz modala, Escape zamyka, fokus wraca do triggera po zamknięciu. Ukryj resztę strony (aria-hidden lub inert).
Odpowiedź w 2 minuty:
Natywny dialog (zalecany):
<button id="openBtn">Otwórz</button>
<dialog id="myDialog" aria-labelledby="dialog-title">
<h2 id="dialog-title">Tytuł modala</h2>
<p>Treść modala</p>
<button id="closeBtn">Zamknij</button>
</dialog>
<script>
const dialog = document.getElementById('myDialog');
const openBtn = document.getElementById('openBtn');
const closeBtn = document.getElementById('closeBtn');
openBtn.addEventListener('click', () => {
dialog.showModal(); // Automatyczny focus trap!
});
closeBtn.addEventListener('click', () => {
dialog.close();
});
// Escape automatycznie zamyka
dialog.addEventListener('cancel', (e) => {
// Opcjonalnie: prevent default lub custom logic
});
</script>
Custom dialog (gdy nie możesz użyć <dialog>):
<button id="openBtn" aria-haspopup="dialog">Otwórz</button>
<div
id="myDialog"
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
hidden
>
<h2 id="dialog-title">Tytuł</h2>
<p>Treść</p>
<button id="closeBtn">Zamknij</button>
</div>
<script>
let previouslyFocused;
function openDialog() {
previouslyFocused = document.activeElement;
dialog.hidden = false;
document.body.setAttribute('inert', '');
dialog.removeAttribute('inert');
dialog.querySelector('button, [href], input').focus();
dialog.addEventListener('keydown', trapFocus);
dialog.addEventListener('keydown', handleEscape);
}
function closeDialog() {
dialog.hidden = true;
document.body.removeAttribute('inert');
previouslyFocused.focus();
}
function handleEscape(e) {
if (e.key === 'Escape') closeDialog();
}
</script>
26. Jak zaimplementować dostępne zakładki (tabs)?
Odpowiedź w 30 sekund:
Użyj role="tablist", role="tab", role="tabpanel". Aktywna tab ma aria-selected="true". Powiąż tab z panelem przez aria-controls i aria-labelledby. Nawigacja: strzałki między tabs, Tab przechodzi do panelu. Tylko aktywna tab jest w tab order.
Odpowiedź w 2 minuty:
Dostępne taby wymagają odpowiedniej struktury ARIA i obsługi klawiatury. Poniższy przykład pokazuje kompletną implementację z nawigacją strzałkami i prawidłowym zarządzaniem fokusem.
<div class="tabs">
<div role="tablist" aria-label="Informacje o produkcie">
<button
role="tab"
aria-selected="true"
aria-controls="panel-1"
id="tab-1"
tabindex="0"
>
Opis
</button>
<button
role="tab"
aria-selected="false"
aria-controls="panel-2"
id="tab-2"
tabindex="-1"
>
Specyfikacja
</button>
<button
role="tab"
aria-selected="false"
aria-controls="panel-3"
id="tab-3"
tabindex="-1"
>
Opinie
</button>
</div>
<div
role="tabpanel"
id="panel-1"
aria-labelledby="tab-1"
tabindex="0"
>
Opis produktu...
</div>
<div
role="tabpanel"
id="panel-2"
aria-labelledby="tab-2"
tabindex="0"
hidden
>
Specyfikacja techniczna...
</div>
<div
role="tabpanel"
id="panel-3"
aria-labelledby="tab-3"
tabindex="0"
hidden
>
Opinie klientów...
</div>
</div>
<script>
const tabs = document.querySelectorAll('[role="tab"]');
const panels = document.querySelectorAll('[role="tabpanel"]');
tabs.forEach(tab => {
tab.addEventListener('click', () => switchTab(tab));
tab.addEventListener('keydown', (e) => {
const index = Array.from(tabs).indexOf(tab);
let newIndex;
switch (e.key) {
case 'ArrowRight':
newIndex = (index + 1) % tabs.length;
break;
case 'ArrowLeft':
newIndex = (index - 1 + tabs.length) % tabs.length;
break;
case 'Home':
newIndex = 0;
break;
case 'End':
newIndex = tabs.length - 1;
break;
default:
return;
}
e.preventDefault();
tabs[newIndex].focus();
switchTab(tabs[newIndex]);
});
});
function switchTab(newTab) {
tabs.forEach(tab => {
tab.setAttribute('aria-selected', 'false');
tab.setAttribute('tabindex', '-1');
});
panels.forEach(panel => panel.hidden = true);
newTab.setAttribute('aria-selected', 'true');
newTab.setAttribute('tabindex', '0');
const panelId = newTab.getAttribute('aria-controls');
document.getElementById(panelId).hidden = false;
}
</script>
27. Jak obsłużyć błędy formularzy w sposób dostępny?
Odpowiedź w 30 sekund:
Błędy muszą być: powiązane z polem (aria-describedby), ogłaszane (aria-live lub role="alert"), widoczne (nie tylko kolor). Po submit ustaw fokus na pierwszy błędny element lub podsumowanie błędów. Używaj aria-invalid="true" na błędnych polach.
Odpowiedź w 2 minuty:
Dostępna walidacja formularzy wymaga wielowarstwowego podejścia - od podsumowania błędów przez live regions do indywidualnych komunikatów przy polach. Poniższy przykład pokazuje kompletną implementację z prawidłowym zarządzaniem fokusem.
<form id="myForm" novalidate>
<!-- Podsumowanie błędów na górze -->
<div
id="error-summary"
role="alert"
aria-live="assertive"
hidden
>
<h2>Formularz zawiera błędy:</h2>
<ul id="error-list"></ul>
</div>
<div class="form-group">
<label for="email">Email *</label>
<input
type="email"
id="email"
name="email"
required
aria-describedby="email-hint email-error"
>
<span id="email-hint" class="hint">
np. jan@example.com
</span>
<span id="email-error" class="error" hidden>
</span>
</div>
<div class="form-group">
<label for="password">Hasło *</label>
<input
type="password"
id="password"
name="password"
required
minlength="8"
aria-describedby="password-hint password-error"
>
<span id="password-hint" class="hint">
Minimum 8 znaków
</span>
<span id="password-error" class="error" hidden>
</span>
</div>
<button type="submit">Wyślij</button>
</form>
<script>
const form = document.getElementById('myForm');
form.addEventListener('submit', (e) => {
e.preventDefault();
const errors = validateForm();
if (errors.length > 0) {
showErrors(errors);
} else {
submitForm();
}
});
function validateForm() {
const errors = [];
const email = document.getElementById('email');
const password = document.getElementById('password');
if (!email.validity.valid) {
errors.push({
field: email,
message: 'Podaj prawidłowy adres email'
});
}
if (!password.validity.valid) {
errors.push({
field: password,
message: 'Hasło musi mieć minimum 8 znaków'
});
}
return errors;
}
function showErrors(errors) {
// Reset poprzednich błędów
document.querySelectorAll('.error').forEach(el => {
el.hidden = true;
el.textContent = '';
});
document.querySelectorAll('[aria-invalid]').forEach(el => {
el.removeAttribute('aria-invalid');
});
// Pokaż podsumowanie
const summary = document.getElementById('error-summary');
const list = document.getElementById('error-list');
list.innerHTML = '';
errors.forEach(error => {
// Podsumowanie
const li = document.createElement('li');
const link = document.createElement('a');
link.href = `#${error.field.id}`;
link.textContent = error.message;
li.appendChild(link);
list.appendChild(li);
// Pole
error.field.setAttribute('aria-invalid', 'true');
const errorSpan = document.getElementById(`${error.field.id}-error`);
errorSpan.textContent = error.message;
errorSpan.hidden = false;
});
summary.hidden = false;
summary.focus();
}
</script>
<style>
.error {
color: #d32f2f;
display: block;
margin-top: 4px;
}
input[aria-invalid="true"] {
border-color: #d32f2f;
border-width: 2px;
}
#error-summary {
background: #ffebee;
padding: 16px;
border-left: 4px solid #d32f2f;
margin-bottom: 16px;
}
#error-summary a {
color: #d32f2f;
}
</style>
Podsumowanie
HTML5 i Accessibility to fundamenty profesjonalnego frontend developmentu. Na rozmowie rekrutacyjnej w 2026 roku musisz znać:
HTML5:
- Semantyczne elementy i ich znaczenie
- Formularze z walidacją
- Multimedia (video, audio, track)
- Storage (localStorage, sessionStorage, IndexedDB)
Accessibility:
- WCAG poziomy i zasady POUR
- ARIA roles, states, properties
- Nawigacja klawiaturowa
- Testowanie (automatyczne i manualne)
Zobacz też
- Kompletny Przewodnik - Rozmowa Frontend Developer - pełny przewodnik przygotowania
- CSS Flexbox i Grid - Przewodnik - layout i responsive design
Chcesz Więcej Pytań z HTML5?
Mamy kompletny zestaw 37 pytań z HTML5 - każde z odpowiedzią w formacie "30 sekund / 2 minuty". Idealne do szybkiego przygotowania przed rozmową.
Chcesz więcej pytań rekrutacyjnych?
To tylko jeden temat z naszego kompletnego przewodnika po rozmowach rekrutacyjnych. Uzyskaj dostęp do 800+ pytań z 13 technologii.
