HTML & CSS

Доступність у CSS (CSS Accessibility)

Практичне керівництво з CSS-доступності: :focus-visible та кастомний outline, prefers-reduced-motion, prefers-color-scheme, forced-colors, sr-only техніка, контрастність кольорів, розміри touch targets та print styles.

Доступність у CSS

15% вашої аудиторії

Коли ми кажемо «доступність» (Accessibility, a11y), перша асоціація — сліпі користувачі зі скринридерами. Але реальність набагато ширша: 15% населення Землі мають ту чи іншу форму обмеження — моторну, зорову, слухову або когнітивну. Серед інших — користувачі, що тимчасово не можуть натиснути кнопку (зламана рука), люди похилого віку, або просто ті, хто дивиться контент на сонці без можливості збільшити яскравість.

CSS — один із головних інструментів доступності. Правильно написані стилі роблять інтерфейс читабельним при будь-яких умовах, зручним при навігації клавіатурою, безпечним для людей з епілепсією, та шанобливим до системних налаштувань користувача.

Loading diagram...
mindmap
  root((CSS a11y))
    Фокус
      :focus-visible
      Кастомний outline
    Колір
      Контраст WCAG
      forced-colors
      color-contrast
    Рух
      prefers-reduced-motion
      Безпечні анімації
    Розміри
      rem замість px
      Touch targets 44px
      Масштабування
    Приховування
      sr-only техніка
      aria-hidden
      visibility vs display
    Друк
      @media print
      page-break

Фокус: найчастіша помилка у CSS

Чому outline: none — злочин

Перегляньте будь-який CSS-проєкт 2015–2020 років і ви майже напевно знайдете цей рядок:

/* ⛔ Так НЕ робити — ламає навігацію клавіатурою */
* {
  outline: none;
}

Або ще гірше:

/* ⛔ Так теж НЕ робити */
:focus {
  outline: none;
}

Цей код прибирає єдиний візуальний індикатор, що показує людям, які навігують клавіатурою (Tab, Shift+Tab, Enter), де зараз знаходиться фокус. Без нього навігація клавіатурою стає неможливою.

Хто використовує навігацію клавіатурою:

  • Люди з моторними порушеннями (не можуть використовувати мишу)
  • Сліпі та слабозорі (скринридери + клавіатура)
  • Досвідчені розробники та power-users (значно швидше)
  • Будь-хто з тимчасовою травмою руки

:focus-visible — правильне рішення

:focus-visible — псевдоклас, що активується лише тоді, коли браузер вважає, що візуальний індикатор фокусу необхідний. Зазвичай це:

  • Навігація клавіатурою (Tab)
  • Будь-яка взаємодія, якщо UserAgent вирішив показати індикатор

При кліку мишею :focus-visible зазвичай не активується — і це правильно. Outline при кліку мишею — лише візуальний шум.

🔒localhost:3000

Глобальний скид outline — правильний спосіб

/* ✅ Правильний скид: прибираємо дефолт, зберігаємо власний */
:focus {
  outline: none;
}

:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 3px;
}

/* Або для power-users: скинути дефолт та додати власний на рівні компонентів */
:focus:not(:focus-visible) {
  outline: none;
}

Практична робота: Створення кросбраузерного індикатора фокусу з використанням :focus-visible та кастомного outline-ring

🎯 Очікуваний результат: Розробка повністю доступного меню навігації з кастомними індикаторами фокусування. Ми налаштуємо поведінку елементів таким чином, щоб:

  1. При кліку мишкою по кнопках фокусний обідок не з'являвся (оскільки це дратує звичайних користувачів).
  2. При навігації з клавіатури за допомогою клавіші Tab активна кнопка виділялася красивим контрастним кільцем, яке плавно анімується та має відступ від меж самої кнопки.

Крок 1: Створення структури HTML

Створіть файл focus-ring.html у вашому робочому каталозі та додайте наступну розмітку:

<!DOCTYPE html>
<html lang="uk">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Практична робота: Кастомний Focus Ring</title>
    <link rel="stylesheet" href="focus-ring.css">
</head>
<body>
    <div class="navigation-panel">
        <h2>Особистий кабінет 👤</h2>
        <p class="instruction">Натисніть клавішу <strong>Tab</strong> на клавіатурі для перевірки кастомного фокусу.</p>
        
        <nav class="nav-links">
            <a href="#profile" class="nav-item">Профіль</a>
            <a href="#security" class="nav-item">Безпека</a>
            <a href="#billing" class="nav-item">Платежі</a>
            <button class="logout-btn">Вийти</button>
        </nav>
    </div>
</body>
</html>

Крок 2: Додавання стилів CSS

Створіть у тій же папці файл focus-ring.css та додайте такі стилі:

/* Базове оформлення інтерфейсу */
body {
    background-color: #0b1329;
    color: white;
    font-family: system-ui, sans-serif;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    margin: 0;
}

.navigation-panel {
    background-color: #111a36;
    border: 1px solid #1e294b;
    border-radius: 16px;
    padding: 2rem;
    width: 100%;
    max-width: 380px;
    box-shadow: 0 15px 35px rgba(0, 0, 0, 0.4);
}

h2 {
    margin-top: 0;
    margin-bottom: 0.5rem;
    font-size: 1.3rem;
}

.instruction {
    font-size: 0.8rem;
    color: #64748b;
    margin-bottom: 1.5rem;
    line-height: 1.4;
}

.nav-links {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}

/* Базові стилі елементів меню */
.nav-item {
    display: block;
    color: #94a3b8;
    text-decoration: none;
    padding: 0.75rem 1rem;
    background-color: #1e294b;
    border-radius: 8px;
    font-weight: 600;
    font-size: 0.9rem;
    transition: background-color 0.15s, color 0.15s;
}

.nav-item:hover {
    background-color: #27375e;
    color: white;
}

.logout-btn {
    padding: 0.75rem 1rem;
    background-color: #ef4444;
    color: white;
    border: none;
    border-radius: 8px;
    font-weight: 700;
    font-size: 0.9rem;
    cursor: pointer;
    font-family: inherit;
    transition: background-color 0.15s;
}

.logout-btn:hover {
    background-color: #dc2626;
}

/* Крок 3: Налаштування кастомного :focus-visible */

/* 1. Скидаємо дефолтний системний outline при класичному фокусі (:focus) */
.nav-item:focus,
.logout-btn:focus {
    outline: none;
}

/* 2. Додаємо виразний кастомний фокус ТІЛЬКИ при навігації з клавіатури (:focus-visible) */
.nav-item:focus-visible {
    /* Робимо витончене двоколірне кільце, що добре помітно на будь-кому тлі */
    outline: 3px solid #3b82f6; /* Синє кільце */
    outline-offset: 3px; /* Робимо відступ від самого елемента */
    background-color: #27375e;
    color: white;
}

/* Для кнопки виходу робимо червоний фокусний обідок */
.logout-btn:focus-visible {
    outline: 3px solid #f87171; /* Червоний колір фокусу */
    outline-offset: 3px;
    background-color: #dc2626;
}

Крок 3: Перевірка та аналіз результату

  1. Відкрийте файл focus-ring.html у браузері.
  2. Клацніть мишкою по посиланнях або кнопці "Вийти". Зверніть увагу: жодних фокусних рамок або обідків навколо елементів не з'являється. Користувач мишки бачить стандартні ефекти :hover.
  3. Тепер клацніть у порожню область сторінки та почніть натискати клавішу Tab.
  4. Спостерігайте, як фокус переміщується по елементах навігації. Завдяки селектору :focus-visible навколо активного посилання створюється красиве товсте синє кільце з відступом у 3 пікселі, а навколо кнопки виходу — червоне кільце.
  5. Натисніть Shift + Tab для руху у зворотному напрямку. Це і є правильний, професійний підхід до UX та доступності у сучасному вебі!

Контрастність кольорів

WCAG вимоги

WCAG (Web Content Accessibility Guidelines) — міжнародний стандарт доступності. Він визначає мінімальний контраст між кольором тексту та фону:

РівеньТекст < 18ptТекст ≥ 18pt (великий)
AA (мінімум)4.5:13:1
AAA (ідеал)7:14.5:1
🔒localhost:3000

forced-colors та @media (prefers-contrast)

/* Стилі для режиму підвищеного контрасту (Windows High Contrast) */
@media (forced-colors: active) {
  .button {
    /* forced-colors змінює кольори автоматично, але border залишається видимим */
    border: 2px solid ButtonText;
    forced-color-adjust: none; /* вимкнути авто-зміну для окремих елементів */
  }

  /* Приховані елементи через opacity — можуть бути видимі у forced-colors */
  .decorative {
    forced-color-adjust: none;
    opacity: 0;
  }
}

/* Підвищений контраст на прохання користувача */
@media (prefers-contrast: more) {
  :root {
    --text: #000000;
    --bg: #ffffff;
    --border: 2px solid currentColor;
  }
}

Практична робота: Реалізація адаптивної теми з підвищеним контрастом через @media (prefers-contrast)

🎯 Очікуваний результат: Створення інформаційного блока з картками, який за замовчуванням має м'які пастельні кольори та приглушений сірий текст (для стильного сучасного дизайну). Але у випадку, якщо користувач увімкнув у налаштуваннях операційної системи режим підвищеної контрастності (Accessibility -> Display -> Increase Contrast), CSS автоматично перевизначає змінні, роблячи межі абсолютно чорними (або білими для темної теми), текст — насиченим, а також додає чіткі розділювачі.

Крок 1: Створення структури HTML

Створіть файл high-contrast.html у вашому робочому каталозі та додайте наступну розмітку:

<!DOCTYPE html>
<html lang="uk">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Практична робота: Адаптивний контраст</title>
    <link rel="stylesheet" href="high-contrast.css">
</head>
<body>
    <div class="system-card">
        <span class="category-badge">Оновлення</span>
        <h2>Доступність у кожному рядку коду 🔐</h2>
        <p class="card-desc">Ми оновили наші стандарти розробки інтерфейсів, щоб кожна лючина відчувала себе комфортно при користуванні продуктом.</p>
        
        <div class="card-footer">
            <span class="read-time">3 хв читання</span>
            <a href="#more" class="read-more-btn">Читати далі</a>
        </div>
    </div>
</body>
</html>

Крок 2: Додавання стилів CSS

Створіть у тій же папці файл high-contrast.css та додайте такі стилі:

/* Крок 2.1: Оголошуємо дизайн-токени у вигляді Custom Properties */
:root {
    --bg-primary: #1e293b;
    --bg-card: #0f172a;
    --text-title: #f1f5f9;
    --text-muted: #64748b; /* М'який сірий колір (контрастність низька) */
    --accent: #6366f1;
    --border-color: #334155;
    --border-width: 1px;
    --btn-bg: #3b82f6;
    --btn-text: #ffffff;
}

/* Базові стилі */
body {
    background-color: var(--bg-primary);
    color: var(--text-title);
    font-family: system-ui, sans-serif;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    margin: 0;
}

.system-card {
    background-color: var(--bg-card);
    border: var(--border-width) solid var(--border-color);
    border-radius: 12px;
    padding: 1.75rem;
    width: 100%;
    max-width: 360px;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}

.category-badge {
    align-self: flex-start;
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    color: var(--accent);
    letter-spacing: 0.05em;
}

h2 {
    margin: 0;
    font-size: 1.25rem;
    line-height: 1.3;
}

.card-desc {
    margin: 0;
    font-size: 0.9rem;
    color: var(--text-muted); /* Напівпрозорий за замовчуванням */
    line-height: 1.5;
}

.card-footer {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-top: 0.75rem;
}

.read-time {
    font-size: 0.8rem;
    color: var(--text-muted);
}

.read-more-btn {
    padding: 0.5rem 1rem;
    background-color: var(--btn-bg);
    color: var(--btn-text);
    text-decoration: none;
    border-radius: 6px;
    font-size: 0.85rem;
    font-weight: 600;
    transition: background-color 0.15s;
}

.read-more-btn:hover {
    filter: brightness(1.1);
}

/* Крок 2.2: Робота з медіа-запитом prefers-contrast */

@media (prefers-contrast: more) {
    :root {
        /* Перевизначаємо змінні на максимально контрастні для полегшення читання */
        --bg-card: #000000; /* Абсолютно чорний фон */
        --text-title: #ffffff; /* Чистий білий колір */
        --text-muted: #e2e8f0; /* Світло-сірий замість темного (контрастність злітає до >10:1) */
        --accent: #ffff00; /* Жовтий акцент — класика високого контрасту */
        --border-color: #ffffff; /* Біла жирна рамка навколо всього блоку */
        --border-width: 2.5px; /* Збільшуємо товщину рамки */
        --btn-bg: #ffffff; /* Контрастна біла кнопка */
        --btn-text: #000000; /* Чорний текст на ній */
    }

    .system-card {
        box-shadow: none; /* Прибираємо тіні, фокусуємось на чіткості меж */
    }

    .read-more-btn {
        outline: 2px solid #ffffff; /* Додатковий розділювач */
        outline-offset: 2px;
    }
}

Крок 3: Перевірка та аналіз результату

  1. Відкрийте файл high-contrast.html у браузері. За замовчуванням ви побачите красиву темну картку з приглушеним сірим описом та фіолетовими/синіми елементами.
  2. Тепер відкрийте налаштування вашої ОС (наприклад, на Mac: System Settings -> Accessibility -> Display та увімкніть тумблер Increase Contrast).
  3. Поверніться до браузера. Зверніть увагу: картка миттєво перебудувала свій інтерфейс! Колір тла став чисто чорним, опис картки тепер яскраво-білий, рамка навколо стала білою та товстою, а кнопка стала супер-контрастною.
  4. Медіа-запит @media (prefers-contrast: more) дозволяє підтримувати трендові приглушені дизайни, водночас піклуючись про людей з катарактою, порушеннями зору чи тих, хто працює за монітором під палючим сонцем.

prefers-reduced-motion — безпечні анімації

Деякі людини страждають на вестибулярні розлади або епілепсію — для них мерехтливі анімації можуть бути фізично небезпечними. Операційні системи надають налаштування «Зменшити рух» (Reduce Motion), і CSS дозволяє його зчитувати.

🔒localhost:3000

Правильний паттерн для анімацій:

/* Підхід 1: анімація за замовчуванням, вимикаємо для reduce */
.card {
  transition: transform 0.3s ease, box-shadow 0.3s ease;
}

@media (prefers-reduced-motion: reduce) {
  .card {
    transition: none;
  }
}

/* Підхід 2: анімація лише при no-preference (рекомендований) */
@media (prefers-reduced-motion: no-preference) {
  .hero {
    animation: slide-in 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) both;
  }

  .card:hover {
    transform: translateY(-4px);
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.12);
  }
}

Практична робота: Інтеграція безпечного інтерфейсу з урахуванням вестибулярної чутливості користувача (Reduce Motion)

🎯 Очікуваний результат: Створення преміальної вітальної секції (Hero section) з плавними ефектами появи тексту та легким безкінечним пульсуванням фону. Але якщо в налаштуваннях системи користувача активовано режим "Зменшити рух" (Reduce Motion), наш CSS повністю та елегантно перебудовує анімацію: замість масштабних рухів у просторі (які викликають запаморочення у чутливих людей) ми залишаємо лише плавне, уповільнене перетікання прозорості (fade-in) або повністю фіксуємо елементи.

Крок 1: Створення структури HTML

Створіть файл reduced-motion.html у вашому робочому каталозі та додайте наступну розмітку:

<!DOCTYPE html>
<html lang="uk">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Практична робота: Безпечний рух</title>
    <link rel="stylesheet" href="reduced-motion.css">
</head>
<body>
    <div class="hero-container">
        <div class="animated-bg"></div>
        <div class="hero-content">
            <h1 class="hero-title">Розвивайте навички без меж 🌠</h1>
            <p class="hero-subtitle">Сучасні інтерактивні курси з веб-розробки та дизайну для кожного студента.</p>
            <button class="hero-action-btn">Почати навчання</button>
        </div>
    </div>
</body>
</html>

Крок 2: Додавання стилів CSS

Створіть у тій же папці файл reduced-motion.css та додайте такі стилі:

/* Базові стилі */
body {
    background-color: #030712;
    color: white;
    font-family: system-ui, sans-serif;
    margin: 0;
    overflow: hidden;
}

.hero-container {
    position: relative;
    width: 100vw;
    height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    text-align: center;
}

/* 1. Динамічний анімований фон */
.animated-bg {
    position: absolute;
    inset: 0;
    background: radial-gradient(circle at 30% 30%, #312e81 0%, #030712 60%);
    opacity: 0.8;
    z-index: 1;
    /* Запускаємо повільну пульсацію фону */
    animation: pulseBg 8s ease-in-out infinite alternate;
}

.hero-content {
    position: relative;
    z-index: 2;
    padding: 1.5rem;
    max-width: 500px;
}

/* 2. Анімація польоту тексту при завантаженні (зсув + прозорість) */
.hero-title {
    font-size: 2.2rem;
    margin-bottom: 1rem;
    line-height: 1.2;
    
    /* За замовчуванням елемент прихований до початку анімації */
    opacity: 0;
    transform: translateY(25px);
    animation: slideUpFade 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) forwards 0.2s;
}

.hero-subtitle {
    font-size: 1.05rem;
    color: #9ca3af;
    margin-bottom: 2rem;
    line-height: 1.6;
    
    opacity: 0;
    transform: translateY(20px);
    animation: slideUpFade 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) forwards 0.4s;
}

.hero-action-btn {
    padding: 0.85rem 1.75rem;
    background-color: #3b82f6;
    color: white;
    border: none;
    border-radius: 9999px;
    font-size: 0.95rem;
    font-weight: 700;
    cursor: pointer;
    box-shadow: 0 4px 15px rgba(59, 130, 246, 0.3);
    
    opacity: 0;
    transform: scale(0.85);
    animation: popIn 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) forwards 0.6s;
    transition: transform 0.2s, background-color 0.2s;
}

.hero-action-btn:hover {
    background-color: #2563eb;
    transform: scale(1.05);
}

/* Ключові кадри складних анімацій */
@keyframes pulseBg {
    0% { transform: scale(1); }
    100% { transform: scale(1.15); }
}

@keyframes slideUpFade {
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

@keyframes popIn {
    to {
        opacity: 1;
        transform: scale(1);
    }
}

/* Крок 3: Оптимізація для користувачів із порушеннями вестибулярного апарату */

@media (prefers-reduced-motion: reduce) {
    /* 1. Повністю зупиняємо пульсацію масштабного фону */
    .animated-bg {
        animation: none;
        transform: scale(1);
    }
    
    /* 2. Замінюємо різкі зсуви по вертикалі та зуми на плавне розчинення */
    .hero-title,
    .hero-subtitle,
    .hero-action-btn {
        transform: none !important; /* Відміняємо зсуви */
        opacity: 0;
        animation-name: simpleFadeIn !important; /* Перевизначаємо анімацію */
        animation-duration: 1.2s !important; /* Робимо її повільнішою та безпечнішою */
    }
    
    .hero-title { animation-delay: 0.1s !important; }
    .hero-subtitle { animation-delay: 0.3s !important; }
    .hero-action-btn { animation-delay: 0.5s !important; }
    
    /* Ховер ефекти на кнопках також робимо миттєвими або без руху */
    .hero-action-btn:hover {
        transform: none; /* Жодного макро-руху при наведенні */
        background-color: #2563eb;
    }
}

/* Безпечна проста анімація прозорості */
@keyframes simpleFadeIn {
    to {
        opacity: 1;
    }
}

Крок 3: Перевірка та аналіз результату

  1. Відкрийте файл reduced-motion.html у браузері. Ви побачите ефектну появу тексту з плавною траєкторією польоту вгору та легке дихання фону.
  2. Тепер зайдіть у системні налаштування вашої ОС (наприклад, на Mac: System Settings -> Accessibility -> Display -> Reduce Motion та активуйте тумблер).
  3. Поверніться у браузер та перезавантажте сторінку.
  4. Оцініть результат: весь текст плавно проявляється на екрані без жодного зсуву по вертикалі, а фонові коливання повністю завмерли. Це виключає ризик виникнення нудоти чи запаморочення у людей із вестибулярними захворюваннями, зберігаючи водночас естетику дизайну!

Техніка .sr-only — видимо для скринридера, невидимо візуально

Іноді потрібно надати контекст скринридеру, не показуючи текст візуально. Класичний приклад — кнопка «X» (закрити) без текстового пояснення:

<button class="close-btn">
  <svg>...</svg>
  <span class="sr-only">Закрити діалог</span>
</button>
/* .sr-only — «visually hidden» технніка */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}

/* Варіант з можливістю показу при фокусі (for skip links) */
.sr-only-focusable:not(:focus) {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}
🔒localhost:3000

🎯 Очікуваний результат: Створення доступної навігаційної панелі, яка містить:

  1. Повністю приховане посилання Skip Link ("Перейти до вмісту"). Воно невидиме візуально, але миттєво з'являється у верхній частині екрана при натисканні клавіші Tab. Це дозволяє користувачам клавіатури пропускати довгі списки посилань шапки сайту та одразу переходити до читання статті.
  2. Кнопку видалення у вигляді іконки кошика 🗑️. За допомогою техніки .sr-only ми приховаємо пояснювальний текст "Видалити елемент" від звичайних користувачів, але зробимо його доступним для читання екранними дикторами (скринридерами).

Крок 1: Створення структури HTML

Створіть файл sr-only.html у вашому робочому каталозі та додайте наступну розмітку:

<!DOCTYPE html>
<html lang="uk">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Практична робота: Skip Link & sr-only</title>
    <link rel="stylesheet" href="sr-only.css">
</head>
<body>
    <!-- 1. Skip Link (має бути найпершим посиланням у розмітці body) -->
    <a href="#main-content" class="skip-link">Перейти до основного контенту</a>

    <header class="navbar">
        <div class="logo">EduPlatform 🚀</div>
        
        <nav class="nav-menu">
            <a href="#home">Головна</a>
            <a href="#courses">Курси</a>
            <a href="#about">Про нас</a>
        </nav>

        <!-- Кнопка очищення з графічною іконкою -->
        <button class="delete-icon-btn">
            🗑️
            <!-- Цей текст приховано візуально, але скринридер зачитає його чітко -->
            <span class="sr-only">Видалити всі завантажені курси</span>
        </button>
    </header>

    <main id="main-content" class="content-area">
        <h1>Основний контент сторінки 📖</h1>
        <p>Ви успішно перейшли безпосередньо до статті, пропустивши верхню навігаційну панель!</p>
    </main>
</body>
</html>

Крок 2: Додавання стилів CSS

Створіть у тій же папці файл sr-only.css та додайте такі стилі:

/* Базові стилі */
body {
    background-color: #0b0f19;
    color: white;
    font-family: system-ui, sans-serif;
    margin: 0;
    padding: 0;
}

.navbar {
    background-color: #111827;
    border-bottom: 1px solid #1e293b;
    padding: 1rem 2rem;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.logo {
    font-weight: 700;
}

.nav-menu a {
    color: #94a3b8;
    text-decoration: none;
    margin-right: 1.5rem;
    font-size: 0.9rem;
    font-weight: 600;

    &:hover { color: white; }
}

.content-area {
    padding: 3rem 2rem;
    max-width: 600px;
    margin: 0 auto;
}

.content-area h1 {
    font-size: 1.8rem;
    margin-bottom: 1rem;
}

.content-area p {
    color: #94a3b8;
    line-height: 1.6;
}

/* Крок 3: Оголошення утиліти .sr-only (visually-hidden) */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0); /* Спеціальна функція для обрізання меж до 0 */
    white-space: nowrap; /* Забороняємо перенесення рядків */
    border-width: 0;
}

/* Стилізація іконки-кнопки */
.delete-icon-btn {
    width: 44px;
    height: 44px;
    background-color: #1e293b;
    border: 1px solid #334155;
    border-radius: 8px;
    cursor: pointer;
    font-size: 1.2rem;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: all 0.15s;

    &:hover {
        background-color: #ef4444;
        border-color: #ef4444;
    }

    &:focus-visible {
        outline: 2px solid #ef4444;
        outline-offset: 3px;
    }
}

/* Крок 4: Створення випадаючого Skip Link */
.skip-link {
    position: absolute;
    top: 1rem;
    left: 1rem;
    background-color: #3b82f6;
    color: white;
    padding: 0.75rem 1.5rem;
    border-radius: 6px;
    text-decoration: none;
    font-weight: 700;
    font-size: 0.85rem;
    z-index: 100;
    
    /* Ховаємо за межі екрану по вертикалі */
    transform: translateY(-200%);
    transition: transform 0.2s ease-in-out;
}

/* При фокусуванні (коли користувач робить Tab на це посилання) зсуваємо його назад */
.skip-link:focus-visible {
    transform: translateY(0);
    outline: 3px solid white;
    outline-offset: 2px;
}

Крок 3: Перевірка та аналіз результату

  1. Відкрийте файл sr-only.html у браузері. Зверніть увагу: на екрані немає жодного натяку на посилання "Перейти до вмісту". Кнопка кошика містить лише іконку 🗑️ без тексту.
  2. Клацніть у будь-яке порожнє місце сторінки, а потім натисніть клавішу Tab.
  3. На екрані миттєво випаде яскраве синє посилання Skip Link. Якщо натиснути Enter, браузер плавно перенесе фокус на блок <main id="main-content">, оминаючи всі елементи navbar.
  4. Продовжуйте натискати Tab до виділення кнопки кошика. Оскільки всередині кнопки міститься прихований спан .sr-only, користувач екранного диктора почує корисний опис: "EduPlatform. Кнопка. Видалити всі завантажені курси", а не просто пусте слово "Кнопка".

Розміри та масштабування

Чому px для font-size на :root — помилка

Браузери дозволяють користувачам встановлювати базовий розмір шрифту у налаштуваннях (за замовчуванням 16px). Люди з вадами зору встановлюють 20px або 24px. Якщо ви задаєте font-size: 16px на :root — ви перевизначаєте їх налаштування.

🔒localhost:3000

Практична робота: Створення доступної сітки карток з масштабованими текстами та ергономічними кнопками (Touch Targets)

🎯 Очікуваний результат: Розробка сітки адаптивних карток товарів чи послуг, яка повністю відповідає стандартам доступності WCAG:

  1. Всі розміри шрифтів, відступи (paddings, margins) та межі карток задано у відносних одиницях виміру rem (або em), що дозволяє інтерфейсу ідеально та пропорційно збільшуватися при масштабуванні шрифтів у браузері до 200%.
  2. Розміри інтерактивних кнопок ("До кошика") спроєктовані таким чином, щоб їхня мінімальна активна область натискання (Touch Target) становила щонайменше 48px × 48px (згідно з останніми стандартами Google / Apple Accessibility Guidelines). Це дозволяє людям з тремором рук, моторними порушеннями або просто мобільним користувачам на ходу легко натискати кнопки без ризику хибних кліків на сусідні посилання.

Крок 1: Створення структури HTML

Створіть файл touch-targets.html у вашому робочому каталозі та додайте наступну розмітку:

<!DOCTYPE html>
<html lang="uk">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Практична робота: Масштабування та Touch Targets</title>
    <link rel="stylesheet" href="touch-targets.css">
</head>
<body>
    <div class="cards-grid">
        <!-- Картка 1 -->
        <article class="accessible-card">
            <div class="card-image-placeholder">🎨</div>
            <div class="card-body">
                <h2 class="card-title">Основи UI/UX дизайну</h2>
                <p class="card-price">1 200 грн</p>
                <p class="card-description">Практичний інтенсив з побудови сучасних дизайн-систем, інтерактивних прототипів та проведення UX-тестувань.</p>
                
                <div class="card-actions">
                    <button class="add-to-cart-btn" aria-label="Додати 'Основи UI/UX дизайну' до кошика">
                        До кошика 🛒
                    </button>
                </div>
            </div>
        </article>
    </div>
</body>
</html>

Крок 2: Додавання стилів CSS

Створіть у тій же папці файл touch-targets.css та додайте такі стилі:

/* Крок 2.1: НЕ задаємо фіксований px font-size на html або :root, щоб не ламати налаштування браузера користувача. 
   Браузер автоматично візьме базові 16px (або 20px / 24px, якщо це вказав слабозорий користувач). */
:root {
    --bg-page: #0f172a;
    --bg-card: #1e293b;
    --text-main: #f8fafc;
    --text-muted: #94a3b8;
    --accent: #10b981;
    --border-color: #334155;
}

body {
    background-color: var(--bg-page);
    color: var(--text-main);
    font-family: system-ui, sans-serif;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    margin: 0;
    padding: 1rem;
}

.cards-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: 1.5rem;
    width: 100%;
    max-width: 340px;
}

/* Крок 2.2: Всі розміри та відступи виражаємо у rem для безперешкодного масштабування */
.accessible-card {
    background-color: var(--bg-card);
    border: 0.0625rem solid var(--border-color);
    border-radius: 0.75rem;
    overflow: hidden;
    display: flex;
    flex-direction: column;
}

.card-image-placeholder {
    height: 10rem;
    background-color: #334155;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 3rem;
}

.card-body {
    padding: 1.25rem; /* ~20px */
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}

.card-title {
    margin: 0;
    font-size: 1.25rem; /* ~20px */
    line-height: 1.3;
}

.card-price {
    margin: 0;
    font-size: 1.125rem; /* ~18px */
    font-weight: 700;
    color: var(--accent);
}

.card-description {
    margin: 0;
    font-size: 0.875rem; /* ~14px */
    color: var(--text-muted);
    line-height: 1.5;
}

.card-actions {
    margin-top: 0.5rem;
}

/* Крок 2.3: Проєктування Touch Target кнопки. 
   Її візуальна висота може здаватися меншою (наприклад, за рахунок тонких рамок), 
   але ми гарантуємо мінімальні розміри 48px × 48px для надійного натискання пальцем. */
.add-to-cart-btn {
    width: 100%;
    
    /* 1. Задаємо комбінацію padding та font-size так, щоб загальна висота кнопки була не менше 48px (3rem) */
    min-height: 3rem; /* Справжні 48px при базовому шрифті 16px */
    padding: 0.75rem 1rem;
    
    background-color: #3b82f6;
    color: white;
    border: none;
    border-radius: 0.5rem;
    font-weight: 700;
    font-size: 0.9rem;
    cursor: pointer;
    font-family: inherit;
    transition: background-color 0.15s, transform 0.1s;
    
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
}

.add-to-cart-btn:hover {
    background-color: #2563eb;
}

.add-to-cart-btn:active {
    transform: scale(0.98);
}

/* Налаштовуємо виразний фокус */
.add-to-cart-btn:focus-visible {
    outline: 3px solid #60a5fa;
    outline-offset: 3px;
}

Крок 3: Перевірка та аналіз результату

  1. Відкрийте файл touch-targets.html у браузері.
  2. Спробуйте збільшити масштаб сторінки у налаштуваннях браузера (або натисканням комбінації Ctrl + (Cmd +) чи Ctrl - (Cmd -)).
  3. Зверніть увагу: завдяки використанню rem для всіх розмірів (font-size, padding, margin, height, border-radius), картка та кнопка збільшуються пропорційно. Макет не "ламається", текст не вилазить за межі картки, а кнопки розширюються разом із текстом.
  4. Дослідіть кнопку за допомогою інструментів розробника (F12 -> Inspect Element). Переконайтеся, що її висота становить щонайменше 48 пікселів. На мобільних девайсах така кнопка натискається надзвичайно легко та безпомилково, що кардинально підвищує конверсію магазину!

@media print — стилі для друку

🔒localhost:3000

Практична робота: Оптимізація статті блогу для друку на папері

🎯 Очікуваний результат: Створення CSS-стилів друку для статті блогу. Коли користувач вирішує роздрукувати статтю на папері (або зберегти її у форматі PDF за допомогою комбінації Ctrl + P / Cmd + P), сторінка автоматично оптимізується:

  1. Всі зайві декоративні та навігаційні елементи (шапка сайту, бічні панелі, підвал, рекламні банери, кнопки шарингу) повністю приховуються, щоб економити чорнила принтера.
  2. Весь кольоровий або темний фон замінюється на чисто білий, а текст стає глибокого чорного кольору.
  3. Шрифти змінюються на високоякісну гарнітуру із засічками (Serif) для кращої читабельності на фізичному папері.
  4. Всі інтерактивні посилання у тексті отримують автоматичну приписку з повною URL-адресою у дужках поруч із назвою, наприклад: "наша документація (https://kostyl.dev)", оскільки на папері клікнути посиланням неможливо!
  5. Запобігається перенесення заголовків на наступну сторінку без самого тексту (уникаємо "оріть" заголовків).

Крок 1: Створення структури HTML

Створіть файл print-styles.html у вашому робочому каталозі та додайте наступну розмітку:

<!DOCTYPE html>
<html lang="uk">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Практична робота: Стилі для друку</title>
    <link rel="stylesheet" href="print-styles.css">
</head>
<body>
    <!-- Цей блок навігації має бути прихований при друку -->
    <header class="site-header">
        <div class="logo">DevBlog 📰</div>
        <button class="share-btn">Поділитися 📢</button>
    </header>

    <div class="container">
        <!-- Основна стаття -->
        <main class="main-content">
            <article class="blog-post">
                <h1 class="post-title">Як писати CSS-код, за який не соромно</h1>
                <p class="post-meta">Опубліковано: 13 Травня, 2026 • Автор: Алекс</p>
                
                <p>Сучасна верстка вимагає не лише відповідності дизайн-макету, але й ретельного тестування доступності та ергономіки. Ми повинні думати про кожного відвідувача нашого ресурсу.</p>
                
                <p>Для глибшого розуміння рекомендуємо переглянути <a href="https://kostyl.dev/guidelines" class="article-link">наші стандарти розробки</a>, а також ознайомитися з офіційним <a href="https://www.w3.org/WAI/standards-guidelines/wcag/" class="article-link">керівництвом WCAG</a>.</p>
                
                <h2 class="sub-title">Чому стилі друку все ще важливі?</h2>
                <p>Багато студентів та розробників роздруковують довгі навчальні статті, інструкції або конспекти лекцій на папері, щоб читати їх офлайн без напруження очей від моніторів.</p>
            </article>
        </main>

        <!-- Бічна панель, яка непотрібна на папері -->
        <aside class="sidebar">
            <h3>Популярні статті</h3>
            <ul>
                <li><a href="#1">Основи Flexbox</a></li>
                <li><a href="#2">Grid за 10 хвилин</a></li>
                <li><a href="#3">Анімації в CSS</a></li>
            </ul>
        </aside>
    </div>

    <!-- Підвал сайту -->
    <footer class="site-footer">
        <p>&copy; 2026 DevBlog. Всі права захищено.</p>
    </footer>
</body>
</html>

Крок 2: Додавання стилів CSS

Створіть у тій же папці файл print-styles.css та додайте такі стилі:

/* Крок 2.1: Екранні стилі (те, що бачить користувач на моніторі) */
body {
    background-color: #0f172a;
    color: #e2e8f0;
    font-family: system-ui, sans-serif;
    margin: 0;
    padding: 0;
}

.site-header {
    background-color: #1e293b;
    border-bottom: 1px solid #334155;
    padding: 1rem 2rem;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.logo {
    font-weight: 700;
    font-size: 1.2rem;
}

.share-btn {
    padding: 0.5rem 1rem;
    background-color: #3b82f6;
    color: white;
    border: none;
    border-radius: 6px;
    cursor: pointer;
}

.container {
    max-width: 960px;
    margin: 2rem auto;
    padding: 0 1rem;
    display: grid;
    grid-template-columns: 2fr 1fr;
    gap: 2rem;
}

.blog-post {
    background-color: #1e293b;
    border: 1px solid #334155;
    border-radius: 12px;
    padding: 2rem;
}

.post-title {
    margin-top: 0;
    font-size: 2rem;
}

.post-meta {
    color: #94a3b8;
    font-size: 0.85rem;
    margin-bottom: 1.5rem;
}

.article-link {
    color: #60a5fa;
    text-decoration: underline;
    font-weight: 600;
}

.sidebar {
    background-color: #1e293b;
    border: 1px solid #334155;
    border-radius: 12px;
    padding: 1.5rem;
    height: fit-content;
}

.sidebar h3 {
    margin-top: 0;
}

.sidebar ul {
    padding-left: 1.25rem;
    margin: 0;
}

.sidebar li {
    margin-bottom: 0.5rem;
}

.sidebar a {
    color: #94a3b8;
    text-decoration: none;
}

.site-footer {
    text-align: center;
    padding: 2rem;
    background-color: #0f172a;
    border-top: 1px solid #334155;
    color: #64748b;
    font-size: 0.85rem;
    margin-top: 4rem;
}


/* Крок 2.2: Оптимізація для друку за допомогою @media print */

@media print {
    /* 1. Ховаємо навігацію, бічні панелі, футер та кнопки */
    .site-header,
    .sidebar,
    .share-btn,
    .site-footer {
        display: none !important;
    }

    /* 2. Скидаємо кольоровий фон, тіні та рамки для економії чорнил */
    body {
        background: #ffffff !important;
        color: #000000 !important;
        /* Використовуємо класичний Serif шрифт для кращого друку */
        font-family: "Georgia", "Times New Roman", serif !important;
        font-size: 12pt !important;
        line-height: 1.5 !important;
    }

    .container {
        display: block !important; /* Робимо лінійний макет */
        margin: 0 !important;
        padding: 0 !important;
    }

    .blog-post {
        background: transparent !important;
        border: none !important;
        padding: 0 !important;
    }

    .post-title {
        font-size: 24pt !important;
        color: #000000 !important;
    }

    .post-meta {
        color: #555555 !important;
    }

    /* 3. Оформлення посилань: робимо чорними та показуємо їхні URL */
    .article-link {
        color: #000000 !important;
        text-decoration: underline !important;
    }

    /* Відображаємо атрибут href у дужках після тексту посилання */
    .article-link::after {
        content: " (" attr(href) ")";
        font-size: 10pt !important;
        font-weight: normal !important;
        color: #555555 !important;
    }

    /* 4. Керування розривами сторінок (Page Breaks) */
    
    /* Забороняємо розривати статтю всередині абзаців */
    p {
        orphans: 3; /* Мінімальна кількість рядків у нижній частині сторінки */
        widows: 3;  /* Мінімальна кількість рядків у верхній частині сторінки */
    }

    /* Забороняємо розривати сторінку одразу після заголовків h1, h2, h3 
       (заголовок ніколи не залишиться один наприкінці сторінки) */
    h1, h2, h3 {
        page-break-after: avoid;
        break-after: avoid;
    }
}

Крок 3: Перевірка та аналіз результату

  1. Відкрийте файл print-styles.html у вашому браузері. Ви побачите стильний темний інтерфейс блогу з боковою панеллю та синіми посиланнями.
  2. Тепер викликайте системне діалогове вікно друку браузера, натиснувши комбінацію клавіш Ctrl + P (на Windows) або Cmd + P (на Mac).
  3. Подивіться на вікно попереднього перегляду друку (Print Preview).
  4. Оцініть зміни: макет став чисто білим, увесь непотрібний дизайн зник, залишивши лише статтю, текст написаний приємним книжковим шрифтом, а біля посилань "наші стандарти розробки" та "WCAG" у круглих дужках надруковано повні веб-адреси.
  5. Саме так створюються професійні державні, медичні та освітні ресурси високого гатунку!

Чеклист доступності CSS

🎯 Фокус

  • Ніколи outline: none без заміни
  • Використовуйте :focus-visible замість :focus
  • Skip link на початку сторінки (Tab → перехід до контенту)

🎨 Кольори

  • Текст ≥ 4.5:1 контраст (AA) або ≥ 7:1 (AAA)
  • Не покладайтесь лише на колір для передачі інформації
  • Тестуйте у forced-colors: active режимі

⚡ Анімації

  • @media (prefers-reduced-motion: no-preference) для анімацій
  • Без блимання частотою 3+ Гц (ризик епілептичного нападу)
  • scroll-behavior: smooth — лише при no-preference

📏 Розміри

  • font-size на :root — тільки % або нічого
  • Touch targets ≥ 44px × 44px
  • Інтерфейс масштабується до 200% без горизонтального скролу

👁️ Приховування

  • .sr-only для тексту що потрібен скринридерам
  • display: none = невидимий для всіх включно зі скринридером
  • opacity: 0 ≠ приховано від скринридера!

🖨️ Друк

  • @media print — ховаємо nav, aside, ads
  • body { font: 12pt serif; color: #000; }
  • a::after { content: " (" attr(href) ")"; }

Практика

Рівень 1 — Базовий: аудит доступності

Беремо готовий HTML-компонент навігаційного меню. Знайдіть та виправте 4 проблеми доступності:

/* Знайдіть проблеми: */
* { outline: none; }                      /* Проблема 1 */
nav a { color: #aaaaaa; }                 /* Проблема 2: контраст ~2.3:1 */
.menu-icon { width: 20px; height: 20px; } /* Проблема 3: замалий touch target */
.tooltip { opacity: 0; }                  /* Проблема 4: чи видно скринридеру? */

Рівень 2 — Адаптивна система Motion

Реалізуйте систему анімацій з повним урахуванням prefers-reduced-motion:

  • Landing hero: slide-in + fade при завантаженні
  • Cards: scale + shadow при hover
  • Scroll: reveal animations через @keyframes
  • При reduce: всі переходи ≤ 50ms, без scale і translate

Рівень 3 — Повна доступна форма

Побудуйте форму відправки повідомлень з:

  • Floating labels (CSS-only)
  • Правильний :focus-visible на кожному полі
  • :user-valid/:user-invalid з ARIA-feedback через aria-live region
  • Error messages через .sr-only для скринридерів
  • Контрастність ≥ 4.5:1 для всього тексту
  • Touch targets кнопок ≥ 44px
  • @media print стиль що показує текст полів без дизайну форм
Copyright © 2026