Доступність у CSS (CSS Accessibility)
Доступність у CSS
15% вашої аудиторії
Коли ми кажемо «доступність» (Accessibility, a11y), перша асоціація — сліпі користувачі зі скринридерами. Але реальність набагато ширша: 15% населення Землі мають ту чи іншу форму обмеження — моторну, зорову, слухову або когнітивну. Серед інших — користувачі, що тимчасово не можуть натиснути кнопку (зламана рука), люди похилого віку, або просто ті, хто дивиться контент на сонці без можливості збільшити яскравість.
CSS — один із головних інструментів доступності. Правильно написані стилі роблять інтерфейс читабельним при будь-яких умовах, зручним при навігації клавіатурою, безпечним для людей з епілепсією, та шанобливим до системних налаштувань користувача.
Фокус: найчастіша помилка у 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 при кліку мишею — лише візуальний шум.
<div class="focus-demo">
<p class="fd-note">Натискайте Tab для навігації між елементами. При кліку мишею — outline не з'являється.</p>
<div class="focus-grid">
<button class="bad-btn">⛔ outline: none<br><small>(клавіатура зламана)</small></button>
<button class="old-btn">⚠️ :focus завжди<br><small>(outline при кліку теж)</small></button>
<button class="good-btn">✅ :focus-visible<br><small>(тільки клавіатура)</small></button>
<a href="#" class="link-good">Посилання ✅</a>
<input class="input-good" type="text" placeholder="Input ✅">
</div>
<div class="focus-style-showcase">
<p class="fss-label">Варіанти кастомного focus-visible стилю:</p>
<button class="fs-ring">Ring style</button>
<button class="fs-underline">Underline style</button>
<button class="fs-glow">Glow style</button>
<button class="fs-contrast">High contrast</button>
</div>
</div>
.focus-demo {
padding: 1rem;
background: #f8fafc;
font-family: system-ui, sans-serif;
font-size: 0.875rem;
color: #1e293b;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.fd-note { margin: 0; color: #64748b; font-style: italic; font-size: 0.8rem; }
.focus-grid { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
/* Базові стилі кнопок */
.bad-btn, .old-btn, .good-btn {
padding: 0.6rem 0.9rem;
border-radius: 8px;
border: 1.5px solid;
font-size: 0.8rem;
font-family: inherit;
cursor: pointer;
line-height: 1.3;
text-align: center;
}
/* ⛔ Найгірший варіант */
.bad-btn {
background: #fee2e2; color: #991b1b; border-color: #fca5a5;
outline: none; /* НІКОЛИ ТАК НЕ РОБІТЬ */
}
/* ⚠️ Старий підхід — outline завжди */
.old-btn {
background: #fef3c7; color: #92400e; border-color: #fcd34d;
}
.old-btn:focus {
outline: 3px solid #f59e0b;
outline-offset: 2px;
}
/* ✅ Правильно — :focus-visible */
.good-btn {
background: #dcfce7; color: #166534; border-color: #86efac;
}
.good-btn:focus { outline: none; } /* скидаємо дефолт */
.good-btn:focus-visible {
outline: 3px solid #10b981;
outline-offset: 2px;
border-radius: 8px;
}
.link-good {
color: #6366f1; font-weight: 600;
padding: 0.5rem;
border-radius: 4px;
text-decoration: underline;
}
.link-good:focus { outline: none; }
.link-good:focus-visible {
outline: 2px solid #6366f1;
outline-offset: 3px;
}
.input-good {
padding: 0.5rem 0.75rem;
border: 1.5px solid #d1d5db;
border-radius: 7px;
font-size: 0.875rem;
font-family: inherit;
outline: none;
}
.input-good:focus-visible {
border-color: #6366f1;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
}
/* Варіанти кастомного focus-visible */
.focus-style-showcase { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
.fss-label { flex-basis: 100%; margin: 0; font-size: 0.78rem; color: #64748b; font-weight: 600; }
.fs-ring, .fs-underline, .fs-glow, .fs-contrast {
padding: 0.5rem 1rem;
border-radius: 7px;
border: 1.5px solid #e2e8f0;
background: white;
color: #1e293b;
font-size: 0.85rem;
font-family: inherit;
cursor: pointer;
}
.fs-ring:focus { outline: none; }
.fs-underline:focus { outline: none; }
.fs-glow:focus { outline: none; }
.fs-contrast:focus { outline: none; }
.fs-ring:focus-visible {
outline: 3px solid #6366f1;
outline-offset: 3px;
}
.fs-underline:focus-visible {
outline: none;
box-shadow: 0 3px 0 0 #6366f1;
}
.fs-glow:focus-visible {
outline: none;
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.35), 0 0 12px rgba(99, 102, 241, 0.2);
}
.fs-contrast:focus-visible {
outline: 3px solid #000;
outline-offset: 2px;
background: #ffff00;
color: #000;
}
Глобальний скид 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
🎯 Очікуваний результат: Розробка повністю доступного меню навігації з кастомними індикаторами фокусування. Ми налаштуємо поведінку елементів таким чином, щоб:
- При кліку мишкою по кнопках фокусний обідок не з'являвся (оскільки це дратує звичайних користувачів).
- При навігації з клавіатури за допомогою клавіші
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: Перевірка та аналіз результату
- Відкрийте файл
focus-ring.htmlу браузері. - Клацніть мишкою по посиланнях або кнопці "Вийти". Зверніть увагу: жодних фокусних рамок або обідків навколо елементів не з'являється. Користувач мишки бачить стандартні ефекти
:hover. - Тепер клацніть у порожню область сторінки та почніть натискати клавішу Tab.
- Спостерігайте, як фокус переміщується по елементах навігації. Завдяки селектору
:focus-visibleнавколо активного посилання створюється красиве товсте синє кільце з відступом у 3 пікселі, а навколо кнопки виходу — червоне кільце. - Натисніть Shift + Tab для руху у зворотному напрямку. Це і є правильний, професійний підхід до UX та доступності у сучасному вебі!
Контрастність кольорів
WCAG вимоги
WCAG (Web Content Accessibility Guidelines) — міжнародний стандарт доступності. Він визначає мінімальний контраст між кольором тексту та фону:
| Рівень | Текст < 18pt | Текст ≥ 18pt (великий) |
|---|---|---|
| AA (мінімум) | 4.5:1 | 3:1 |
| AAA (ідеал) | 7:1 | 4.5:1 |
<div class="contrast-demo">
<p class="cd-title">Порівняння контрастності:</p>
<div class="contrast-pairs">
<div class="cp-item fail">
<span class="cp-ratio">1.6:1 ❌</span>
<p>Майже невидимо — сірий на білому</p>
</div>
<div class="cp-item warn">
<span class="cp-ratio">3.2:1 ⚠️</span>
<p>Тільки для великого тексту (AA Large)</p>
</div>
<div class="cp-item pass-aa">
<span class="cp-ratio">5.9:1 ✅ AA</span>
<p>Проходить WCAG AA для будь-якого тексту</p>
</div>
<div class="cp-item pass-aaa">
<span class="cp-ratio">7.5:1 ✅ AAA</span>
<p>Відмінна контрастність — ідеал для читання</p>
</div>
</div>
<div class="bg-contrast-demo">
<div class="bc-item bc-dark">
<p>Темний фон + білий текст<br><small>Контраст ~13:1 ✅</small></p>
</div>
<div class="bc-item bc-indigo">
<p>Indigo #6366f1 + білий<br><small>Контраст ~4.6:1 ✅ AA</small></p>
</div>
<div class="bc-item bc-yellow">
<p style="color: #1e293b;">Жовтий + темний текст<br><small>Контраст ~9.5:1 ✅</small></p>
</div>
<div class="bc-item bc-bad">
<p>Поганий вибір 😰<br><small>Контраст ~2.1:1 ❌</small></p>
</div>
</div>
</div>
.contrast-demo {
padding: 1rem;
background: #f8fafc;
font-family: system-ui, sans-serif;
font-size: 0.875rem;
color: #1e293b;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.cd-title { margin: 0; font-weight: 700; color: #64748b; }
.contrast-pairs { display: flex; flex-direction: column; gap: 4px; }
.cp-item {
padding: 0.5rem 0.75rem;
border-radius: 6px;
display: flex;
align-items: center;
gap: 1rem;
}
.cp-item p { margin: 0; font-size: 0.82rem; }
.cp-ratio { font-size: 0.78rem; font-weight: 700; white-space: nowrap; flex-shrink: 0; min-width: 90px; }
.fail { background: white; color: #c8c8c8; border: 1px solid #e2e8f0; }
.warn { background: white; color: #94a3b8; border: 1px solid #e2e8f0; }
.pass-aa { background: white; color: #374151; border: 1px solid #e2e8f0; }
.pass-aaa { background: white; color: #111827; border: 1px solid #e2e8f0; }
.bg-contrast-demo { display: flex; gap: 4px; flex-wrap: wrap; }
.bc-item {
flex: 1; min-width: 120px;
padding: 0.75rem;
border-radius: 8px;
}
.bc-item p { margin: 0; font-size: 0.8rem; color: white; line-height: 1.4; }
.bc-item small { font-size: 0.7rem; opacity: 0.85; }
.bc-dark { background: #0f172a; }
.bc-indigo { background: #6366f1; }
.bc-yellow { background: #fbbf24; }
.bc-bad { background: #a78bfa; }
.bc-bad p { color: #c4b5fd; }
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: Перевірка та аналіз результату
- Відкрийте файл
high-contrast.htmlу браузері. За замовчуванням ви побачите красиву темну картку з приглушеним сірим описом та фіолетовими/синіми елементами. - Тепер відкрийте налаштування вашої ОС (наприклад, на Mac: System Settings -> Accessibility -> Display та увімкніть тумблер Increase Contrast).
- Поверніться до браузера. Зверніть увагу: картка миттєво перебудувала свій інтерфейс! Колір тла став чисто чорним, опис картки тепер яскраво-білий, рамка навколо стала білою та товстою, а кнопка стала супер-контрастною.
- Медіа-запит
@media (prefers-contrast: more)дозволяє підтримувати трендові приглушені дизайни, водночас піклуючись про людей з катарактою, порушеннями зору чи тих, хто працює за монітором під палючим сонцем.
prefers-reduced-motion — безпечні анімації
Деякі людини страждають на вестибулярні розлади або епілепсію — для них мерехтливі анімації можуть бути фізично небезпечними. Операційні системи надають налаштування «Зменшити рух» (Reduce Motion), і CSS дозволяє його зчитувати.
<div class="motion-demo">
<p class="md-note">Якщо у вашій ОС увімкнено «Зменшити рух» — анімації будуть вимкнені або сповільнені.</p>
<div class="motion-grid">
<div class="motion-card bad-motion">
<div class="spinning-box">⛔<br><small>без prefers-reduced-motion</small></div>
</div>
<div class="motion-card good-motion">
<div class="safe-box">✅<br><small>з prefers-reduced-motion</small></div>
</div>
<div class="motion-card">
<div class="fade-reveal">
<p>Fade-in при скролі<br><small>безпечний, якщо повільний</small></p>
</div>
</div>
</div>
<div class="media-badges">
<div class="mb-item">
<code>@media (prefers-reduced-motion: no-preference)</code>
— повна анімація ✅
</div>
<div class="mb-item">
<code>@media (prefers-reduced-motion: reduce)</code>
— мінімум руху 🛡️
</div>
</div>
</div>
.motion-demo {
padding: 1rem;
background: #f8fafc;
font-family: system-ui, sans-serif;
font-size: 0.875rem;
color: #1e293b;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.md-note { margin: 0; font-size: 0.78rem; color: #64748b; font-style: italic; }
.motion-grid { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.motion-card {
flex: 1; min-width: 120px;
background: white;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 1rem;
display: flex;
align-items: center;
justify-content: center;
min-height: 80px;
}
/* Анімація БЕЗ урахування налаштувань — небезпечно */
.spinning-box {
width: 60px; height: 60px;
background: linear-gradient(135deg, #ef4444, #f97316);
border-radius: 8px;
display: flex; align-items: center; justify-content: center;
font-size: 1.2rem;
text-align: center;
line-height: 1.2;
animation: spin-danger 1.5s linear infinite;
}
@keyframes spin-danger { to { transform: rotate(360deg); } }
/* ПРАВИЛЬНО: анімація зупиняється при Reduce Motion */
.safe-box {
width: 60px; height: 60px;
background: linear-gradient(135deg, #10b981, #059669);
border-radius: 8px;
display: flex; align-items: center; justify-content: center;
font-size: 1.2rem;
text-align: center;
line-height: 1.2;
animation: pulse-safe 2s ease-in-out infinite;
}
@keyframes pulse-safe {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
/* Вимикаємо для тих, хто просить менше руху */
@media (prefers-reduced-motion: reduce) {
.safe-box { animation: none; }
/* Анімації дозволені якщо вони повільні та не обертові */
.fade-reveal { animation-duration: 0.01ms !important; }
/* Загальне правило — вимкнути всі transitions та animations */
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
.fade-reveal {
padding: 0.75rem;
background: #ede9fe;
border-radius: 6px;
text-align: center;
line-height: 1.4;
animation: fade-in-safe 0.6s ease;
}
@keyframes fade-in-safe {
from { opacity: 0; }
to { opacity: 1; }
}
.media-badges { display: flex; flex-direction: column; gap: 0.3rem; }
.mb-item {
background: white;
border: 1px solid #e2e8f0;
border-radius: 6px;
padding: 0.5rem 0.75rem;
font-size: 0.78rem;
line-height: 1.4;
}
.mb-item code { background: #f1f5f9; padding: 0.1em 0.3em; border-radius: 3px; font-size: 0.82em; color: #6366f1; }
Правильний паттерн для анімацій:
/* Підхід 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: Перевірка та аналіз результату
- Відкрийте файл
reduced-motion.htmlу браузері. Ви побачите ефектну появу тексту з плавною траєкторією польоту вгору та легке дихання фону. - Тепер зайдіть у системні налаштування вашої ОС (наприклад, на Mac: System Settings -> Accessibility -> Display -> Reduce Motion та активуйте тумблер).
- Поверніться у браузер та перезавантажте сторінку.
- Оцініть результат: весь текст плавно проявляється на екрані без жодного зсуву по вертикалі, а фонові коливання повністю завмерли. Це виключає ризик виникнення нудоти чи запаморочення у людей із вестибулярними захворюваннями, зберігаючи водночас естетику дизайну!
Техніка .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;
}
<div class="sronly-demo">
<p class="sd-note">Кнопки нижче виглядають однаково, але скринридер читає різний текст:</p>
<div class="btn-row">
<button class="icon-btn bad-icon-btn" title="Видалити">
🗑️
<!-- Скринридер: "button" або нічого корисного -->
</button>
<button class="icon-btn good-icon-btn" title="Видалити">
🗑️
<span class="sr-only-ex">Видалити елемент</span>
<!-- Скринридер: "Видалити елемент" ✅ -->
</button>
</div>
<div class="skip-link-demo">
<a href="#main-content" class="skip-link">Перейти до основного контенту</a>
<p class="sd-note" style="margin-top:0">↑ Skip Link — з'являється при Tab (спробуйте)</p>
</div>
<div class="visually-hidden-compare">
<p><code>display: none</code> — прихований від всіх, включно зі скринридером</p>
<p><code>visibility: hidden</code> — прихований візуально + від скринридера, зберігає місце</p>
<p><code>opacity: 0</code> — невидимий, але скринридер ЧИТАЄ його</p>
<p><code>.sr-only</code> — невидимий, але скринридер ЧИТАЄ ✅</p>
</div>
</div>
.sronly-demo {
padding: 1rem;
background: #f8fafc;
font-family: system-ui, sans-serif;
font-size: 0.875rem;
color: #1e293b;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.sd-note { margin: 0; color: #64748b; font-style: italic; font-size: 0.8rem; }
.btn-row { display: flex; gap: 0.5rem; }
.icon-btn {
width: 40px; height: 40px;
border-radius: 8px;
border: 1.5px solid #e2e8f0;
background: white;
font-size: 1.1rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
}
.bad-icon-btn { border-color: #fca5a5; background: #fff5f5; }
.good-icon-btn { border-color: #86efac; background: #f0fdf4; }
.icon-btn:hover { transform: scale(1.05); }
/* .sr-only техніка */
.sr-only-ex {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
/* Skip Link */
.skip-link-demo { display: flex; flex-direction: column; gap: 0.3rem; }
.skip-link {
display: inline-block;
padding: 0.5rem 1rem;
background: #6366f1;
color: white;
border-radius: 6px;
text-decoration: none;
font-size: 0.85rem;
font-weight: 600;
transform: translateY(-120%);
transition: transform 0.15s;
}
.skip-link:focus {
transform: translateY(0);
outline: 3px solid #000;
outline-offset: 2px;
}
.visually-hidden-compare {
background: white;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.visually-hidden-compare p { margin: 0; font-size: 0.78rem; line-height: 1.4; }
.visually-hidden-compare code { background: #f1f5f9; padding: 0.1em 0.3em; border-radius: 3px; color: #6366f1; font-size: 0.85em; }
Практична робота: Розробка повністю доступної кнопки-іконки та прихованої навігаційної системи (Skip Link)
🎯 Очікуваний результат: Створення доступної навігаційної панелі, яка містить:
- Повністю приховане посилання Skip Link ("Перейти до вмісту"). Воно невидиме візуально, але миттєво з'являється у верхній частині екрана при натисканні клавіші
Tab. Це дозволяє користувачам клавіатури пропускати довгі списки посилань шапки сайту та одразу переходити до читання статті. - Кнопку видалення у вигляді іконки кошика 🗑️. За допомогою техніки
.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: Перевірка та аналіз результату
- Відкрийте файл
sr-only.htmlу браузері. Зверніть увагу: на екрані немає жодного натяку на посилання "Перейти до вмісту". Кнопка кошика містить лише іконку 🗑️ без тексту. - Клацніть у будь-яке порожнє місце сторінки, а потім натисніть клавішу Tab.
- На екрані миттєво випаде яскраве синє посилання Skip Link. Якщо натиснути Enter, браузер плавно перенесе фокус на блок
<main id="main-content">, оминаючи всі елементи navbar. - Продовжуйте натискати Tab до виділення кнопки кошика. Оскільки всередині кнопки міститься прихований спан
.sr-only, користувач екранного диктора почує корисний опис: "EduPlatform. Кнопка. Видалити всі завантажені курси", а не просто пусте слово "Кнопка".
Розміри та масштабування
Чому px для font-size на :root — помилка
Браузери дозволяють користувачам встановлювати базовий розмір шрифту у налаштуваннях (за замовчуванням 16px). Люди з вадами зору встановлюють 20px або 24px. Якщо ви задаєте font-size: 16px на :root — ви перевизначаєте їх налаштування.
<div class="font-scale-demo">
<div class="fsd-bad">
<p class="fsd-label">❌ font-size: 16px на :root</p>
<p class="text-px">Цей текст завжди 16px — ігнорує налаштування браузера</p>
</div>
<div class="fsd-good">
<p class="fsd-label">✅ Без font-size на :root (або 100%)</p>
<p class="text-inherit">Цей текст поважає налаштування браузера — масштабується разом з ним</p>
</div>
<div class="touch-targets">
<p class="fsd-label">Touch Targets — мінімум 44×44px (WCAG 2.5.5)</p>
<div class="tt-grid">
<button class="tt-bad">Замалий<br><small>24×24px ❌</small></button>
<button class="tt-ok">Нормальний<br><small>36×36px ⚠️</small></button>
<button class="tt-good">Правильний<br><small>44×44px ✅</small></button>
<button class="tt-great">Зручний<br><small>48×48px 🎯</small></button>
</div>
</div>
</div>
.font-scale-demo {
padding: 1rem;
background: #f8fafc;
font-family: system-ui, sans-serif;
color: #1e293b;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.fsd-label { margin: 0 0 0.4rem; font-size: 0.78rem; font-weight: 700; color: #374151; }
.fsd-bad, .fsd-good {
background: white;
border-radius: 8px;
padding: 0.75rem;
border: 1.5px solid;
}
.fsd-bad { border-color: #fca5a5; }
.fsd-good { border-color: #86efac; }
.text-px { margin: 0; font-size: 16px; line-height: 1.5; } /* ❌ fіксований */
.text-inherit { margin: 0; font-size: 1rem; line-height: 1.5; } /* ✅ відносний */
.touch-targets { background: white; border-radius: 8px; padding: 0.75rem; border: 1px solid #e2e8f0; }
.tt-grid { display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: flex-end; }
.tt-bad, .tt-ok, .tt-good, .tt-great {
border-radius: 8px;
border: none;
font-family: inherit;
cursor: pointer;
font-size: 0.7rem;
text-align: center;
line-height: 1.3;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.tt-bad { width: 60px; height: 44px; background: #fee2e2; color: #991b1b; }
.tt-ok { width: 60px; height: 48px; background: #fef3c7; color: #92400e; }
.tt-good { width: 60px; height: 56px; background: #dcfce7; color: #166534; }
.tt-great { width: 60px; height: 60px; background: #ede9fe; color: #5b21b6; }
.tt-bad small, .tt-ok small, .tt-good small, .tt-great small { font-size: 0.65rem; margin-top: 0.15rem; }
Практична робота: Створення доступної сітки карток з масштабованими текстами та ергономічними кнопками (Touch Targets)
🎯 Очікуваний результат: Розробка сітки адаптивних карток товарів чи послуг, яка повністю відповідає стандартам доступності WCAG:
- Всі розміри шрифтів, відступи (paddings, margins) та межі карток задано у відносних одиницях виміру
rem(абоem), що дозволяє інтерфейсу ідеально та пропорційно збільшуватися при масштабуванні шрифтів у браузері до 200%. - Розміри інтерактивних кнопок ("До кошика") спроєктовані таким чином, щоб їхня мінімальна активна область натискання (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: Перевірка та аналіз результату
- Відкрийте файл
touch-targets.htmlу браузері. - Спробуйте збільшити масштаб сторінки у налаштуваннях браузера (або натисканням комбінації Ctrl + (Cmd +) чи Ctrl - (Cmd -)).
- Зверніть увагу: завдяки використанню
remдля всіх розмірів (font-size,padding,margin,height,border-radius), картка та кнопка збільшуються пропорційно. Макет не "ламається", текст не вилазить за межі картки, а кнопки розширюються разом із текстом. - Дослідіть кнопку за допомогою інструментів розробника (F12 -> Inspect Element). Переконайтеся, що її висота становить щонайменше 48 пікселів. На мобільних девайсах така кнопка натискається надзвичайно легко та безпомилково, що кардинально підвищує конверсію магазину!
@media print — стилі для друку
<div class="print-demo">
<p class="prd-note">Нижче показано, як виглядатиме сторінка при друку (симуляція):</p>
<div class="simulated-print">
<div class="print-hide">🎨 Навігація та декоративні елементи приховані ✅</div>
<article class="print-article">
<h2 class="print-heading">Заголовок статті</h2>
<p>Основний текст — чорний на білому, без фонів та тіней. Шрифт serif для кращого читання на папері.</p>
<a href="https://example.com" class="print-link">Посилання (URL показується при друці)</a>
</article>
<div class="print-hide">🎯 Бокова панель прихована ✅</div>
</div>
<p class="print-code-note">CSS для print:</p>
<pre class="print-code">@media print {
nav, aside, footer { display: none; }
body { font: 12pt/1.5 Georgia, serif; color: #000; }
a::after { content: " (" attr(href) ")"; font-size: 10pt; }
h1, h2 { page-break-after: avoid; }
img { max-width: 100%; }
}</pre>
</div>
.print-demo {
padding: 1rem;
background: #f8fafc;
font-family: system-ui, sans-serif;
font-size: 0.875rem;
color: #1e293b;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.prd-note { margin: 0; color: #64748b; font-style: italic; font-size: 0.78rem; }
.simulated-print {
background: white;
border: 2px dashed #94a3b8;
border-radius: 8px;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
font-size: 0.85rem;
}
.print-hide {
background: #f1f5f9;
border: 1px dashed #cbd5e1;
border-radius: 4px;
padding: 0.4rem 0.6rem;
color: #94a3b8;
font-size: 0.75rem;
text-align: center;
text-decoration: line-through;
}
.print-article { padding: 0.5rem 0; }
.print-heading { margin: 0 0 0.5rem; font-size: 1.1rem; color: #000; }
.print-article p { margin: 0 0 0.5rem; color: #111; line-height: 1.5; }
.print-link { color: #000; }
.print-link::after { content: " (https://example.com)"; font-size: 0.75rem; color: #666; }
.print-code-note { margin: 0; font-size: 0.78rem; font-weight: 700; color: #64748b; }
.print-code {
background: #1e293b;
color: #e2e8f0;
border-radius: 8px;
padding: 0.75rem 1rem;
font-size: 0.78rem;
overflow-x: auto;
margin: 0;
line-height: 1.5;
}
Практична робота: Оптимізація статті блогу для друку на папері
🎯 Очікуваний результат: Створення CSS-стилів друку для статті блогу. Коли користувач вирішує роздрукувати статтю на папері (або зберегти її у форматі PDF за допомогою комбінації Ctrl + P / Cmd + P), сторінка автоматично оптимізується:
- Всі зайві декоративні та навігаційні елементи (шапка сайту, бічні панелі, підвал, рекламні банери, кнопки шарингу) повністю приховуються, щоб економити чорнила принтера.
- Весь кольоровий або темний фон замінюється на чисто білий, а текст стає глибокого чорного кольору.
- Шрифти змінюються на високоякісну гарнітуру із засічками (Serif) для кращої читабельності на фізичному папері.
- Всі інтерактивні посилання у тексті отримують автоматичну приписку з повною URL-адресою у дужках поруч із назвою, наприклад: "наша документація (https://kostyl.dev)", оскільки на папері клікнути посиланням неможливо!
- Запобігається перенесення заголовків на наступну сторінку без самого тексту (уникаємо "оріть" заголовків).
Крок 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>© 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: Перевірка та аналіз результату
- Відкрийте файл
print-styles.htmlу вашому браузері. Ви побачите стильний темний інтерфейс блогу з боковою панеллю та синіми посиланнями. - Тепер викликайте системне діалогове вікно друку браузера, натиснувши комбінацію клавіш Ctrl + P (на Windows) або Cmd + P (на Mac).
- Подивіться на вікно попереднього перегляду друку (Print Preview).
- Оцініть зміни: макет став чисто білим, увесь непотрібний дизайн зник, залишивши лише статтю, текст написаний приємним книжковим шрифтом, а біля посилань "наші стандарти розробки" та "WCAG" у круглих дужках надруковано повні веб-адреси.
- Саме так створюються професійні державні, медичні та освітні ресурси високого гатунку!
Чеклист доступності 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, adsbody { 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-liveregion- Error messages через
.sr-onlyдля скринридерів - Контрастність ≥ 4.5:1 для всього тексту
- Touch targets кнопок ≥ 44px
@media printстиль що показує текст полів без дизайну форм
CSS для форм та інтерактивних станів
Повне керівництво зі стилізації HTML-форм: псевдокласи :focus-visible, :user-valid, :has(), accent-color, кастомні checkbox/radio/select, floating label патерн, валідація без JavaScript та нативний CSS-only дизайн форм.
CSS-функції та сучасні sizing primitives
Повний огляд CSS математичних функцій: calc(), min(), max(), clamp(), round(), mod(), тригонометричні sin/cos/tan, env() для безпечних зон, та сучасні layout primitives — The Stack, The Sidebar, The RAM, The Reel.