← HTML & CSS/Доступность: alt, label, aria#14 из 383← ПредыдущийСледующий →+15 XP
Полезно по теме:Гайд: старт в frontendПрактика: DOM и событияТермин: DOMМаршрут: старт с нуля

Доступность: alt, label, aria

В России более 13 миллионов людей с инвалидностью. Многие из них пользуются интернетом через скринридеры (программы, озвучивающие содержимое экрана) или только клавиатурой. Если твой сайт недоступен для них, ты теряешь клиентов. В некоторых странах это ещё и нарушение закона.

Что такое веб-доступность (a11y)

Accessibility (a11y — 11 букв между a и y) — это создание интерфейсов, которыми могут пользоваться все люди вне зависимости от физических возможностей:

  • Незрячие и слабовидящие — скринридеры, высокий контраст
  • Люди с нарушением моторики — только клавиатура, голосовое управление
  • Люди с нарушением слуха — субтитры, текстовые альтернативы
  • Люди с когнитивными нарушениями — простой язык, понятная навигация
  • alt для изображений — уже разбирали, но повторим

    <!-- Скринридер скажет: "Изображение. Кроссовки Nike Air Max 90, белые" -->
    <img src="sneaker.jpg" alt="Кроссовки Nike Air Max 90, белые" />
    
    <!-- Декоративное — скринридер пропустит -->
    <img src="decoration.svg" alt="" />
    
    <!-- ПЛОХО — скринридер скажет "Изображение. sneaker.jpg" -->
    <img src="sneaker.jpg" />

    label для форм

    Скринридер должен знать что означает каждое поле ввода. Без label пользователь слышит только «поле ввода» без контекста.

    <!-- Правильно: label связан с input через for/id -->
    <label for="phone">Номер телефона</label>
    <input type="tel" id="phone" name="phone" />
    
    <!-- Тоже правильно: input внутри label -->
    <label>
      Номер телефона
      <input type="tel" name="phone" />
    </label>
    
    <!-- ПЛОХО: нет label -->
    <input type="tel" name="phone" placeholder="Телефон" />

    aria-label — подпись без видимого текста

    Иногда видимый текст для кнопки невозможен — только иконка. Тогда используй aria-label:

    <!-- Кнопка закрытия — только ×, скринридер скажет "Закрыть" -->
    <button aria-label="Закрыть диалоговое окно">×</button>
    
    <!-- Кнопка поиска с иконкой -->
    <button aria-label="Поиск">🔍</button>
    
    <!-- Ссылка с иконкой -->
    <a href="/cart" aria-label="Корзина, 3 товара">🛒</a>

    aria-labelledby — подпись через другой элемент

    <h2 id="modal-title">Оформление заказа</h2>
    <div role="dialog" aria-labelledby="modal-title">
      <!-- Скринридер объявит: "Диалог. Оформление заказа" -->
    </div>

    aria-describedby — дополнительное описание

    <label for="password">Пароль</label>
    <input type="password" id="password" aria-describedby="password-hint" />
    <p id="password-hint">Минимум 8 символов, одна заглавная буква, одна цифра</p>

    Скринридер сначала зачитает метку («Пароль»), потом описание.

    role — семантическая роль элемента

    <!-- div как кнопка (лучше использовать настоящую button) -->
    <div role="button" tabindex="0" onclick="submitForm()">
      Отправить
    </div>
    
    <!-- Диалоговое окно -->
    <div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
      <h2 id="dialog-title">Подтверждение</h2>
    </div>
    
    <!-- Предупреждение — скринридер озвучит немедленно -->
    <div role="alert">Ошибка: неверный пароль</div>

    tabindex — управление с клавиатуры

    <!-- Добавляем фокус элементу, который обычно не фокусируемый -->
    <div tabindex="0" onclick="handleClick()">Кликабельный div</div>
    
    <!-- Исключаем элемент из Tab-навигации -->
    <button tabindex="-1">Только программный фокус</button>

    Типичные ошибки

    Ошибка 1: Только цвет как индикатор

    <!-- Плохо: дальтоники не различат красный/зелёный -->
    <span style="color: red">Ошибка</span>
    
    <!-- Хорошо: добавляй текстовый или иконочный индикатор -->
    <span>✕ Ошибка: неверный пароль</span>

    Ошибка 2: div вместо button

    <!-- Плохо: div не фокусируется с клавиатуры, нет роли button -->
    <div onclick="buy()">Купить</div>
    
    <!-- Хорошо: button нативно доступен -->
    <button onclick="buy()">Купить</button>

    Ошибка 3: aria-hidden на важном контенте

    <!-- Скринридер пропустит этот элемент — только если он декоративный! -->
    <div aria-hidden="true">Важная информация</div>

    В реальных проектах

    Инструменты проверки: axe DevTools, Lighthouse (вкладка Accessibility), WAVE. Крупные компании (Сбер, Яндекс, Тинькофф) требуют WCAG 2.1 AA стандарт доступности. В React у каждого input должен быть htmlFor/aria-label.

    Примеры

    Создание доступных элементов: кнопки, картинки, форма

    // Доступная кнопка с иконкой
    const closeBtn = document.createElement('button')
    closeBtn.textContent = '×'
    closeBtn.setAttribute('aria-label', 'Закрыть модальное окно')
    closeBtn.type = 'button'
    
    console.log('Текст кнопки:', closeBtn.textContent)
    console.log('aria-label:', closeBtn.getAttribute('aria-label'))
    console.log('Доступна с клавиатуры: да (button нативно фокусируемая)')
    
    // Доступное изображение
    const img = document.createElement('img')
    img.src = 'https://example.com/sneaker.jpg'
    img.alt = 'Кроссовки Nike Air Max 90, белые, вид сбоку'
    img.width = 400
    img.height = 400
    
    console.log('img alt:', img.alt)
    console.log('Alt информативен:', img.alt.length > 10 ? 'да' : 'нет')
    
    // Доступная форма
    const form = document.createElement('form')
    const label = document.createElement('label')
    label.htmlFor = 'email-field'
    label.textContent = 'Электронная почта'
    
    const input = document.createElement('input')
    input.type = 'email'
    input.id = 'email-field'
    input.name = 'email'
    input.placeholder = 'you@example.com'
    input.required = true
    
    const hint = document.createElement('p')
    hint.id = 'email-hint'
    hint.textContent = 'Введи реальный email — на него придёт подтверждение'
    input.setAttribute('aria-describedby', 'email-hint')
    
    form.append(label, input, hint)
    
    console.log('label.for === input.id:', label.htmlFor === input.id)
    console.log('aria-describedby:', input.getAttribute('aria-describedby'))

    Аудит доступности — проверка основных требований

    // Аудит доступности элементов
    function auditA11y(elements) {
      let score = 0
      const totalChecks = elements.length
      const issues = []
    
      elements.forEach(el => {
        const tag = el.tag
        const attrs = el.attrs || {}
    
        if (tag === 'img') {
          if (attrs.alt !== undefined) {
            score++
            if (attrs.alt === '') {
              console.log('OK img: декоративное (alt="")')
            } else {
              console.log('OK img: alt="' + attrs.alt.substring(0, 30) + '..."')
            }
          } else {
            issues.push('ОШИБКА img: нет атрибута alt')
          }
        }
    
        if (tag === 'button') {
          const hasText = attrs.text && attrs.text.trim()
          const hasAriaLabel = attrs['aria-label']
          if (hasText || hasAriaLabel) {
            score++
            console.log('OK button: "' + (attrs['aria-label'] || attrs.text) + '"')
          } else {
            issues.push('ОШИБКА button: нет текста и нет aria-label')
          }
        }
    
        if (tag === 'input') {
          if (attrs['aria-label'] || attrs.labelFor) {
            score++
            console.log('OK input: есть подпись')
          } else {
            issues.push('ОШИБКА input: нет label или aria-label')
          }
        }
      })
    
      issues.forEach(i => console.log(i))
      console.log('Доступность: ' + score + '/' + totalChecks + ' (' + Math.round(score/totalChecks*100) + '%)')
    }
    
    auditA11y([
      { tag: 'img', attrs: { alt: 'Логотип компании', src: 'logo.png' } },
      { tag: 'img', attrs: { src: 'hero.jpg' } },  // Нет alt
      { tag: 'button', attrs: { text: '×', 'aria-label': 'Закрыть' } },
      { tag: 'button', attrs: { text: '' } },  // Нет текста
      { tag: 'input', attrs: { type: 'email', labelFor: 'email' } },
      { tag: 'input', attrs: { type: 'text' } },  // Нет label
    ])

    Доступность: alt, label, aria

    В России более 13 миллионов людей с инвалидностью. Многие из них пользуются интернетом через скринридеры (программы, озвучивающие содержимое экрана) или только клавиатурой. Если твой сайт недоступен для них, ты теряешь клиентов. В некоторых странах это ещё и нарушение закона.

    Что такое веб-доступность (a11y)

    Accessibility (a11y — 11 букв между a и y) — это создание интерфейсов, которыми могут пользоваться все люди вне зависимости от физических возможностей:

  • Незрячие и слабовидящие — скринридеры, высокий контраст
  • Люди с нарушением моторики — только клавиатура, голосовое управление
  • Люди с нарушением слуха — субтитры, текстовые альтернативы
  • Люди с когнитивными нарушениями — простой язык, понятная навигация
  • alt для изображений — уже разбирали, но повторим

    <!-- Скринридер скажет: "Изображение. Кроссовки Nike Air Max 90, белые" -->
    <img src="sneaker.jpg" alt="Кроссовки Nike Air Max 90, белые" />
    
    <!-- Декоративное — скринридер пропустит -->
    <img src="decoration.svg" alt="" />
    
    <!-- ПЛОХО — скринридер скажет "Изображение. sneaker.jpg" -->
    <img src="sneaker.jpg" />

    label для форм

    Скринридер должен знать что означает каждое поле ввода. Без label пользователь слышит только «поле ввода» без контекста.

    <!-- Правильно: label связан с input через for/id -->
    <label for="phone">Номер телефона</label>
    <input type="tel" id="phone" name="phone" />
    
    <!-- Тоже правильно: input внутри label -->
    <label>
      Номер телефона
      <input type="tel" name="phone" />
    </label>
    
    <!-- ПЛОХО: нет label -->
    <input type="tel" name="phone" placeholder="Телефон" />

    aria-label — подпись без видимого текста

    Иногда видимый текст для кнопки невозможен — только иконка. Тогда используй aria-label:

    <!-- Кнопка закрытия — только ×, скринридер скажет "Закрыть" -->
    <button aria-label="Закрыть диалоговое окно">×</button>
    
    <!-- Кнопка поиска с иконкой -->
    <button aria-label="Поиск">🔍</button>
    
    <!-- Ссылка с иконкой -->
    <a href="/cart" aria-label="Корзина, 3 товара">🛒</a>

    aria-labelledby — подпись через другой элемент

    <h2 id="modal-title">Оформление заказа</h2>
    <div role="dialog" aria-labelledby="modal-title">
      <!-- Скринридер объявит: "Диалог. Оформление заказа" -->
    </div>

    aria-describedby — дополнительное описание

    <label for="password">Пароль</label>
    <input type="password" id="password" aria-describedby="password-hint" />
    <p id="password-hint">Минимум 8 символов, одна заглавная буква, одна цифра</p>

    Скринридер сначала зачитает метку («Пароль»), потом описание.

    role — семантическая роль элемента

    <!-- div как кнопка (лучше использовать настоящую button) -->
    <div role="button" tabindex="0" onclick="submitForm()">
      Отправить
    </div>
    
    <!-- Диалоговое окно -->
    <div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
      <h2 id="dialog-title">Подтверждение</h2>
    </div>
    
    <!-- Предупреждение — скринридер озвучит немедленно -->
    <div role="alert">Ошибка: неверный пароль</div>

    tabindex — управление с клавиатуры

    <!-- Добавляем фокус элементу, который обычно не фокусируемый -->
    <div tabindex="0" onclick="handleClick()">Кликабельный div</div>
    
    <!-- Исключаем элемент из Tab-навигации -->
    <button tabindex="-1">Только программный фокус</button>

    Типичные ошибки

    Ошибка 1: Только цвет как индикатор

    <!-- Плохо: дальтоники не различат красный/зелёный -->
    <span style="color: red">Ошибка</span>
    
    <!-- Хорошо: добавляй текстовый или иконочный индикатор -->
    <span>✕ Ошибка: неверный пароль</span>

    Ошибка 2: div вместо button

    <!-- Плохо: div не фокусируется с клавиатуры, нет роли button -->
    <div onclick="buy()">Купить</div>
    
    <!-- Хорошо: button нативно доступен -->
    <button onclick="buy()">Купить</button>

    Ошибка 3: aria-hidden на важном контенте

    <!-- Скринридер пропустит этот элемент — только если он декоративный! -->
    <div aria-hidden="true">Важная информация</div>

    В реальных проектах

    Инструменты проверки: axe DevTools, Lighthouse (вкладка Accessibility), WAVE. Крупные компании (Сбер, Яндекс, Тинькофф) требуют WCAG 2.1 AA стандарт доступности. В React у каждого input должен быть htmlFor/aria-label.

    Примеры

    Создание доступных элементов: кнопки, картинки, форма

    // Доступная кнопка с иконкой
    const closeBtn = document.createElement('button')
    closeBtn.textContent = '×'
    closeBtn.setAttribute('aria-label', 'Закрыть модальное окно')
    closeBtn.type = 'button'
    
    console.log('Текст кнопки:', closeBtn.textContent)
    console.log('aria-label:', closeBtn.getAttribute('aria-label'))
    console.log('Доступна с клавиатуры: да (button нативно фокусируемая)')
    
    // Доступное изображение
    const img = document.createElement('img')
    img.src = 'https://example.com/sneaker.jpg'
    img.alt = 'Кроссовки Nike Air Max 90, белые, вид сбоку'
    img.width = 400
    img.height = 400
    
    console.log('img alt:', img.alt)
    console.log('Alt информативен:', img.alt.length > 10 ? 'да' : 'нет')
    
    // Доступная форма
    const form = document.createElement('form')
    const label = document.createElement('label')
    label.htmlFor = 'email-field'
    label.textContent = 'Электронная почта'
    
    const input = document.createElement('input')
    input.type = 'email'
    input.id = 'email-field'
    input.name = 'email'
    input.placeholder = 'you@example.com'
    input.required = true
    
    const hint = document.createElement('p')
    hint.id = 'email-hint'
    hint.textContent = 'Введи реальный email — на него придёт подтверждение'
    input.setAttribute('aria-describedby', 'email-hint')
    
    form.append(label, input, hint)
    
    console.log('label.for === input.id:', label.htmlFor === input.id)
    console.log('aria-describedby:', input.getAttribute('aria-describedby'))

    Аудит доступности — проверка основных требований

    // Аудит доступности элементов
    function auditA11y(elements) {
      let score = 0
      const totalChecks = elements.length
      const issues = []
    
      elements.forEach(el => {
        const tag = el.tag
        const attrs = el.attrs || {}
    
        if (tag === 'img') {
          if (attrs.alt !== undefined) {
            score++
            if (attrs.alt === '') {
              console.log('OK img: декоративное (alt="")')
            } else {
              console.log('OK img: alt="' + attrs.alt.substring(0, 30) + '..."')
            }
          } else {
            issues.push('ОШИБКА img: нет атрибута alt')
          }
        }
    
        if (tag === 'button') {
          const hasText = attrs.text && attrs.text.trim()
          const hasAriaLabel = attrs['aria-label']
          if (hasText || hasAriaLabel) {
            score++
            console.log('OK button: "' + (attrs['aria-label'] || attrs.text) + '"')
          } else {
            issues.push('ОШИБКА button: нет текста и нет aria-label')
          }
        }
    
        if (tag === 'input') {
          if (attrs['aria-label'] || attrs.labelFor) {
            score++
            console.log('OK input: есть подпись')
          } else {
            issues.push('ОШИБКА input: нет label или aria-label')
          }
        }
      })
    
      issues.forEach(i => console.log(i))
      console.log('Доступность: ' + score + '/' + totalChecks + ' (' + Math.round(score/totalChecks*100) + '%)')
    }
    
    auditA11y([
      { tag: 'img', attrs: { alt: 'Логотип компании', src: 'logo.png' } },
      { tag: 'img', attrs: { src: 'hero.jpg' } },  // Нет alt
      { tag: 'button', attrs: { text: '×', 'aria-label': 'Закрыть' } },
      { tag: 'button', attrs: { text: '' } },  // Нет текста
      { tag: 'input', attrs: { type: 'email', labelFor: 'email' } },
      { tag: 'input', attrs: { type: 'text' } },  // Нет label
    ])

    Задание

    Напиши доступный HTML-блок поиска. Включи: кнопку открытия поиска (только иконка 🔍) с aria-label="Открыть поиск", форму с label (for="search-input") + input (type="search", id="search-input", aria-describedby="search-hint") + p-подсказку (id="search-hint") + кнопку submit с aria-label="Найти".

    Подсказка

    aria-label на кнопке с иконкой: aria-label="Открыть поиск". label for и input id должны совпадать: for="search-input" и id="search-input". aria-describedby указывает на id подсказки: aria-describedby="search-hint". Кнопка submit: aria-label="Найти".

    Загружаем среду выполнения...
    Загружаем AI-помощника...