HTML5 i Accessibility - Pytania Rekrutacyjne 2026

Sławomir Plamowski 31 min czytania
a11y accessibility aria frontend html5 pytania-rekrutacyjne wcag

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

  1. Podstawy HTML5
  2. Elementy Semantyczne
  3. Formularze HTML5
  4. Multimedia: Audio i Video
  5. Canvas i Grafika
  6. Storage i Offline
  7. Accessibility - Podstawy
  8. ARIA - Accessible Rich Internet Applications
  9. WCAG i Testowanie
  10. 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>&copy; 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:

  1. Keyboard navigation:

    • Tab przez całą stronę
    • Czy fokus jest widoczny?
    • Czy można aktywować wszystkie kontrolki?
    • Czy można wyjść z modali (Escape)?
  2. Screen reader:

    • NVDA (Windows, free)
    • VoiceOver (Mac, built-in)
    • Czy struktura jest logiczna?
    • Czy obrazy mają sensowny alt?
  3. Visual:

    • Kontrast (narzędzie: WebAIM Contrast Checker)
    • Zoom 200% - czy coś się psuje?
    • Tryb wysokiego kontrastu Windows
  4. 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ż


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ą.

Zdobądź Dostęp do Wszystkich Pytań →

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.

Kup pełny dostęp Zobacz bezpłatny podgląd
Powrót do blogu

Zostaw komentarz

Pamiętaj, że komentarze muszą zostać zatwierdzone przed ich opublikowaniem.