Files
ipal-kit/docs/accessibility.md
T

6.0 KiB

Dostępność — widget a11y (WCAG)

Widget dostępności: pływający przycisk otwierający panel z opcjami dla osób z niepełnosprawnościami (rozmiar tekstu, kontrast, skala szarości, podkreślone linki, czytelna czcionka, wyłączenie animacji, duży kursor).

OPCJONALNY — nie dodawaj domyślnie. Widget a11y jest wymagany prawnie TYLKO dla niektórych stron (podmioty publiczne, część e-commerce/usług objętych European Accessibility Act). Dla większości stron komercyjnych to OPCJA, nie obowiązek. Dodawaj GDY klient/projekt tego wymaga — nie na każdej stronie z automatu. W razie wątpliwości: zapytaj, czy strona podlega wymogom dostępności.

Wzorzec jak CookieBanner: provider + widget, wpinasz raz. Preferencje w cookie (bez flash), stosowane jako atrybuty data-a11y-* na <html>.

Granica plugin/projekt: plugin dostarcza MECHANIZM (widget, stan, cookie, atrybuty na html). Projekt dostarcza CSS reagujący na atrybuty — bo style zależą od designu projektu (kolory, czcionki, Tailwind). Plugin NIE narzuca stylów, żeby nie kolidować. Gotowy CSS do skopiowania niżej.


1. Wpięcie — Provider + Widget

// app/(frontend)/[locale]/layout.tsx
import { AccessibilityProvider, AccessibilityWidget } from '@intecion/ipal-kit/client'

<AccessibilityProvider>
  <body>
    {children}
    <AccessibilityWidget
      classNames={{
        button: 'a11y-button',
        panel: 'a11y-panel',
        row: 'a11y-row',
        label: 'a11y-label',
        control: 'a11y-control',
        active: 'a11y-active',
        resetButton: 'a11y-reset',
        closeButton: 'a11y-close',
      }}
      texts={{ title: 'Dostępność', reset: 'Resetuj' }}   // opcjonalne, PL domyślnie
    />
  </body>
</AccessibilityProvider>

Widget jest bez stylów (jak CookieBanner) — classNames dopasowujesz do designu.


2. CSS reagujący na atrybuty (OBOWIĄZKOWE — projekt)

Widget ustawia atrybuty na <html>. Bez tego CSS nic się nie dzieje. Skopiuj do globalnego CSS projektu (dostosuj do designu):

/* Rozmiar tekstu */
html[data-a11y-text="1"] { font-size: 112.5%; }
html[data-a11y-text="2"] { font-size: 125%; }
html[data-a11y-text="3"] { font-size: 150%; }

/* Odstęp między liniami */
html[data-a11y-line="1"] * { line-height: 1.8 !important; }
html[data-a11y-line="2"] * { line-height: 2.2 !important; }

/* Kontrast wysoki */
html[data-a11y-contrast="high"] {
  filter: contrast(1.4);
}
/* Kontrast odwrócony */
html[data-a11y-contrast="inverted"] {
  filter: invert(1) hue-rotate(180deg);
}
html[data-a11y-contrast="inverted"] img,
html[data-a11y-contrast="inverted"] video {
  filter: invert(1) hue-rotate(180deg);   /* przywróć media */
}

/* Skala szarości */
html[data-a11y-grayscale="on"] { filter: grayscale(1); }
/* Uwaga: filter na html nie kumuluje się — jeśli łączysz kontrast+szarość,
   zastosuj na body albo połącz w jednej regule. */

/* Podkreślone linki */
html[data-a11y-underline="on"] a { text-decoration: underline !important; }

/* Czytelna czcionka (podmień na swoją dyslexia-friendly / prostą) */
html[data-a11y-font="readable"] * {
  font-family: Verdana, Tahoma, sans-serif !important;
  letter-spacing: 0.02em;
}

/* Wyłączenie animacji */
html[data-a11y-motion="reduce"] *,
html[data-a11y-motion="reduce"] *::before,
html[data-a11y-motion="reduce"] *::after {
  animation-duration: 0.001ms !important;
  transition-duration: 0.001ms !important;
  scroll-behavior: auto !important;
}

/* Duży kursor */
html[data-a11y-cursor="big"] * {
  cursor: url('/cursors/big.svg') 4 4, auto !important;
}

Dostosuj wartości do projektu (kolory kontrastu, czcionka, kursor). To Twój CSS — plugin tylko ustawia atrybuty.

Kontrast + filter: wiele opcji używa filter na <html>. CSS filter na jednym elemencie NIE kumuluje wielu wartości z różnych reguł — ostatnia wygrywa. Jeśli chcesz łączyć (np. szarość + kontrast), zastosuj filtry na body z pełną wartością, albo zbuduj reguły kombinowane. Dla pojedynczych opcji działa bez problemu.


3. Bez flash (SSR) — opcjonalne

Domyślnie widget stosuje atrybuty po hydratacji (krótki flash przy ładowaniu, jeśli użytkownik miał ustawienia). Żeby tego uniknąć, odczytaj cookie server-side i ustaw atrybuty na <html> w SSR:

// layout.tsx (server) — odczytaj cookie i ustaw atrybuty od razu
import { cookies } from 'next/headers'
import { A11Y_COOKIE, parseA11y, a11yAttributes } from '@intecion/ipal-kit'

const raw = (await cookies()).get(A11Y_COOKIE)?.value
const attrs = a11yAttributes(parseA11y(raw))
const htmlAttrs = Object.fromEntries(
  Object.entries(attrs).filter(([, v]) => v !== null),
)

return <html lang={locale} {...htmlAttrs}>...</html>

Provider i tak re-aplikuje na kliencie i synchronizuje. To tylko eliminuje flash.


4. Osobny przycisk otwierający (opcjonalnie)

Widget ma wbudowany pływający przycisk. Jeśli chcesz otwierać panel z innego miejsca (np. stopka „Dostępność"), użyj hooka:

'use client'
import { useAccessibility } from '@intecion/ipal-kit/client'
// stan otwarcia trzymaj sam, albo rozbuduj widget — hook daje state/set/reset

5. Compliance — kiedy dostępność jest wymagana

Dostępność (WCAG) jest wymagana prawnie TYLKO dla części stron:

  • Podmioty publiczne (urzędy, szkoły, instytucje) — ustawa o dostępności cyfrowej
  • Duże e-commerce / usługi objęte European Accessibility Act (2019/882, od 2025)
  • Strony, gdzie klient sam tego wymaga (polityka firmy, przetarg)

Dla większości stron komercyjnych (wizytówka, mała firma, katalog) widget a11y to opcja, nie obowiązek — dodawaj gdy klient wymaga, nie z automatu.

Gdy dodajesz: widget sam w sobie NIE czyni strony w pełni dostępną — to pomoc dla użytkownika. Pełna dostępność to też semantyczny HTML, alt teksty, nawigacja klawiaturą, kontrast bazowy. Widget uzupełnia, nie zastępuje. Nie sprzedawaj klientowi „mamy widget = jesteśmy zgodni z WCAG". Patrz wymagania-prawne.md.