<divelopers>
1.14K subscribers
23 photos
1 video
1 file
305 links
Рандомные мысли про HTML, CSS, доступность, пользовательские интерфейсы, производительность, браузеры и веб-стандарты.

Автор: @alexnozer
Download Telegram
15й Всемирный День Осведомлённости о Доступности

Сегодня, 21 мая 2026 — всемирный день осведомлённости о доступности (Global Accessibility Awareness Day — GAAD). Это отличный повод напомнить о важности доступности и поделиться материалами на эту тему.

Я регулярно пишу в канале разные посты на тему доступности в вебе и отмечаю эти посты тегом #a11y@alexnozer_dev. К сожалению, теги в канале появились не сразу, поэтому часть материалов о доступности не помечена.

В конце марта вышел ежегодный отчёт The WebAIM Million с результатами анализа доступности миллиона сайтов с помощью инструмента WAVE. Также данные о состоянии доступности можно найти в отчёте Web Almanac 2025 от HttpArchive.

По итогам WebAIM 2026 в топе проблем из года в год один и те же, только некоторые пункты меняются местами. В этом году список такой:

- Недостаточный уровень контрастности текста на 83.9% сайтов;
- Отсутствующий альтернативный текст у изображений на 53.1% сайтов;
- Отсутствующие подписи у полей ввода на 51% сайтов;
- Пустые ссылки на 46.3% сайтов;
- Пустые кнопки на 30.6% сайтов;
- Отсутствующий язык документа на 13.5% сайтов.

Это лишь автоматически обнаруживаемые проблемы. Часть проблем не может быть обнаружена автоматическими средствами анализа. По каждой проблеме из топа планирую сделать отдельный пост, их не сложно исправить.

Есть папки с телеграм-каналами о доступности, собранные Верой Шингарёвой:

- Accessibility-папка — каналы, которые пишут про доступность или около;
- a11y_person — блоги людей с инвалидностью и без, которые пишут про доступность, инклюзию свою жизнь;
- a11y_org — организации в сфере доступности и инклюзии.

У Стаса Мельникова есть интересная серия статей про HTML и CSS ошибки, которые влияют на доступность (вся серия доступна по тегу #html_css_a11y_story_melnik909). Стас вместе со своим знакомым Ильёй, незрячим человеком, делятся распространёнными ошибками и как их исправить.

#a11y
1🔥42🥰1🎉1
Недостаточный уровень контрастности текста

Контрастность определяет соотношение между цветом текста и цветом фона, что влияет на читаемость, утомляемость глаз и способность визуального восприятия контента людьми с нарушениями зрения. Это самая частая проблема доступности по данным WebAIM.

Существует два алгоритма измерения контрастности: WCAG и APCA. Первый существует с момента появления стандарта WCAG и используется во всех текущих версиях стандарта, но даёт ложно-позитивные результаты для некоторых сочетаний цветов.

APCA более новый и надёжный алгоритм. Он учитывает восприятие человеческого глаза, поэтому более точно отражает контрастность. APCA заменит текущий алгоритм в стандарте WCAG 3, а пока массовым остаётся старый алгоритм WCAG.

Есть 3 критерия WCAG, которые затрагивают контрастность:

- 1.4.3 Contrast (Minimum);
- 1.4.6 Contrast (Enhanced);
- 1.4.11 Non-text Contrast.

Я сосредоточусь на 1.4.3. Потому что 1.4.6 выдвигает повышенные требования и исходит из того, что 1.4.3 удовлетворён. А 1.4.11 рассматривает нетекстовый контент, там иные требования и эта проблема не входит в топ самых распространённых.

Критерий 1.4.3 выдвигает следующие требования к уровню контрастности текста:

- 4.5:1 для текста размером до 24px или полужирного текста размером до 19px;
- 3:1 для текста размером от 24px или полужирного текста размером от 19px.

Проблема со стороны разработчиков, которые задают цвета в CSS, в том, что они получают от дизайнеров готовые макеты. Какие цветам там есть, такие переносятся в код. То есть контрастность текста — зона ответственности дизайнеров.

Поэтому исправление проблемы стоит начать с дизайна: установить требования и добавить в чеклист проверки. Инициатива может исходить и от разработки на этапе передачи макета в виде комментариев о недостаточной контрастности.

Для проверки контрастности текста в Figma есть плагины. Я пользуюсь плагином Contrast, но есть много других. Также рекомендую Polychrome, хоть он и работает по алгоритму APCA, он точнее отражает реальный уровень контрастности.

Если продукт большой, над ним работают несколько команд и есть дизайн-система, то проблему контрастности стоит решать на уровне дизайн-системы. Проверить палитру, гайды по использованию цветов, модули типографики.

А если это маленький проект, дизайнер сдал макет, получил оплату и ушёл? Частое явление в заказной разработке сайтов. Тут я предлагаю брать ситуацию в свои руки и отходить от цветов в макете ради пользователей. То есть исправлять неконтрастные цвета.

Процесс такой: сверстайте с цветами из макета, откройте в Chrome Dev Tools вкладку CSS Overview и запустите тест. Он покажет все используемые на странице цвета и уровни контрастности. Затем можно исправить цвет в палитре выбора и перенести в код.

А если дизайнер всё же вернётся и что-то скажет? Ну, это упущение дизайнера, его ответственность. А если заказчик скажет? Тут сложнее, но можно объяснять это как умышленную правку ради UX. Но обычно цвет не сильно меняется.

Есть другая проблема — текст на фоне изображений. Проблема в том, что фото или что-то сложное состоит из огромного количества цветов. Из-за многообразия экранов, невозможно предсказать поверх какой части будет находиться текст.

WCAG на этот счёт говорит, что нужно осуществить проверку контрастности с каждым цветом той части изображения, поверх которой находится текст. Это сложно и многие инструменты такое проверять не умеют.

Если у сайта ко всему прочему есть CMS, то одно изображение может быть заменено на другое и контент-менеджер проверять контрастность в сотне точек точно не будет. Лучше вообще избегать наложения текста поверх изображений.

Но если очень хочется, то простой совет — добавьте одноцветную подложку для текста. Вариант два: затемните место наложения текста через градиент. Вариант три: добавьте эффект «гало» тексту через тень. Но лучше всего подложка.

#a11y
👍83🔥1🤔1🌚1
Решение проблемы с уровнями заголовков

Есть одна давняя и нерешённая проблема: неверная иерархия заголовков. А именно пропущенные уровни заголовков, когда за <h1> следует <h3>. Ухудшается структура страницы и навигация с помощью вспомогательных технологий.

С компонентным подходом может быть трудно учесть контекст. Страница с <h1> рисует компонент «популярные товары» с заголовком <h2>, а он рисует компонент «список товаров» с карточками, у каждой из которых заголовок <h3>.

// Код для иллюстрации
function HomePage() {
return `
<main>
<h1>Магазин</h1>
${BestSellers()}
</main>
`;
}

function BestSellers() {
return `
<section>
<h2>Популярные товары</h2>
${ProductList()}
</section>
`;
}

function ProductList() {
return `
<ul>
${products.map(product => `
<li>
<article>
<h3>${product.title}</h3>
</article>
</li>
`).join('')}
</ul>
`;
}

document.body.innerHTML += HomePage();


В таком сценарии структура будет верной:

Магазин
└── Популярные товары
├── Товар 1
├── Товар 2
└── ...


Если использовать тот же компонент «список товаров» на странице каталога, где в <h1> обычно название категории, а дальше идёт сетка с карточками товаров, то получится пропуск заголовка <h2>. Карточки не учитывают контекст.

function CollectionPage() {
return `
<main>
<h1>Холодильники</h1>
${ProductList()}
</main>
`;
}

function ProductList() {
// как в предыдущем примере
}

document.body.innerHTML += CollectionPage();


Проблему пытаются решить уже давно. На канале есть пост про структуру заголовков и outline, где попытки подробно описаны. Если кратко, то много лет пытались, не смогли, удалили алгоритм из стандарта и отказались от реализации.

Тут на днях Джейк Арчибальд рассказал, что в Firefox экспериментируют с решением проблемы пропуска уровней заголовков. То есть вновь предпринимается попытка решить проблему. И решение планируют стандартизировать.

Дисклеймер: это обзор раннего предложения новых функций. Синтаксис может измениться в будущем или от функций могут отказаться


Предлагается новый глобальный атрибут headingoffset. Он задаёт смещение уровня для всех вложенных заголовков. С этим вновь предлагается использовать для заголовков только <h1> и его уровень будет вычисляться на основе смещения.

<main>
<h1>Магазин</h1>
<!--
задано смещение
уровня заголовков
-->
<section headingoffset="1">
<!-- тут будет уровень 2 -->
<h1>Популярные товары</h1>
<!--
задано смещение
уровня заголовков
-->
<ul headingoffset="1">
<li>
<article>
<!-- тут будет уровень 3 -->
<h1>Товар 1</h1>
</article>
</li>
<li>
<article>
<!-- тут будет уровень 3 -->
<h1>Товар 1</h1>
</article>
</li>
...
</ul>
</section>
</main>


В комментариях подтвердили, что это также работает с элементами <h2>-<h6>. Если задано смещение, то уровень заголовка меняется на совокупное смещение. При смещении в 1 уровень <h2> становится <h3> и так далее.

Может возникнуть вопрос: а что будет с <h6>? На это также дали ответ: он станет <h7> и так вплоть до 9 уровня максимум. В HTML нет <h7>, но с помощью атрибута aria-level="7" заголовку можно задать более глубокий уровень.

Заголовки иногда стилизуют по селектору элемента, который привязывает стили к уровню в названии элемента. Как тогда стилизовать заголовки по уровнями, если все они <h1>? Об этом тоже подумали и добавили селектор :heading():

:heading(2) {
/*
Выберет все заголовки у которых
вычисленный уровень 2
*/
}


Реализация появится в Firefox Nightly 153 за флагом экспериментальных технологий. С одной стороны крутая функция, с другой это возврат к использованию <h1> для всех заголовков, что сломает текущие инструменты анализа дерева заголовков.

#html #a11y
👍74😱4👨‍💻1
Спецификация для сайтов

Йост де Валк, известный по плагину Yoast SEO для WordPress, создал спецификацию для сайтов — набор правил, руководств и чеклистов для проверки качества сайтов. Спецификация адаптирована для агентов, отдаёт страницы в .md, есть MCP.

Спецификация охватывает базовые вещи, SEO, доступность, безопасность, Well-Known URL, доступ агентов, производительность, приватность, устойчивость и интернационализацию. Всего 128 пунктов на данный момент.

Я, как человек небезразличный к качеству сайтов, сам хотел создать похожий ресурс и начал с чеклиста. Спецификация идёт дальше и не просто говорит, как должно быть, а описывает причины, реализацию и проверки, даёт дополнительные ссылки.

Спецификация для сайтов — хороший и современный источник хороших практик. Рекомендую взять на вооружение, если вы хотите создать качественный сайт. Вот бы ещё появился инструмент автоматического анализа этого всего…

#tools
🔥9👍21❤‍🔥1
Слоты без Shadow DOM

В HTML есть элемент <slot> и атрибут с таким же названием — slot. Работают они только с Shadow DOM. Цель элемента <slot> — обозначить стандартную или именованную ячейку для перемещения элементов из DOM в Shadow DOM.

Цель атрибута slot — указать имя ячейки, которую создаёт <slot name="…">, куда должен попасть элемент. Там ещё хитрые правила проникновения стилей в Shadow DOM и селекторы :slotted() и :has-slotted. Но это всё про Shadow DOM.

Я тут подумал: а если использовать атрибут slot без Shadow DOM? Так он не работает, но современный CSS на многое способен. Можно создать grid с именованными ячейками и распределить по ним элементы с помощью attr() и grid-area.

input-wrapper {
display: inline-grid;
grid-template-areas:
'label label label'
'before input after'
'info info info'
;

> :not([slot]) {
display: none !important;
}

> [slot] {
--slot: attr(
slot
type(label | before | input | after | info),
invalid
);
display: if(
style(--slot: invalid): none !important;
else: revert;
);
grid-area: if(
style(--slot: invalid): auto;
else: var(--slot);
);
}
}



<input-wrapper>
<label
for="pwd"
slot="label"
>
Пароль
</label>
<input
type="password"
id="pwd"
autocomplete="new-password"
slot="input"
>
<button
type="button"
commandfor="pwd"
command="--toggle-password"
slot="after"
aria-pressed="false"
>
<!-- ... -->
</button>
<p slot="info">
Пароль должен содержать...
</p>
</input-wrapper>


Сейчас объясню что тут происходит. Внутри input-wrapper объявлена сетка из трёх колонок с именованными ячейками label, before, input, after и info. Прямые потомки без атрибута slot скрываются, потому что он нужен.

Функция attr() считывает значение из атрибута slot и приводит его к заданному типу. Функция type() определяет CSS-тип значения, который представлен в виде перечисления доступных имён ячеек (как Union Type в Typescript).

Если в slot указано неподдерживаемое значение, то его не получится привести к типу, указанному в type(). В таком случае используется значение по умолчанию invalid. Результат attr() записывается в пользовательское свойство --slot.

Функция if() в свойстве display проверяет значение свойства --slot. Если там invalid, то есть в атрибуте задано неподдерживаемое имя ячейки, то такой элемент скрывается. Иначе revert возвращает заданное ранее значение.

В свойстве grid-area функция if() так же проверяет свойство --slot. При invalid устанавливается значение auto, но это не важно, потому что элемент будет скрыт. А если там допустимое значение, то элемент попадает в нужную ячейку.

Это работает в браузерах с поддержкой расширенной функции attr() и if(). Элементы можно менять местами и они всё равно попадут в нужные ячейки сетки. Ячейки могут быть пустыми, в примере нет элемента для слота before.

Это решение можно слегка деградировать, отказавшись от if() и attr() . Кода будет больше, но браузерная поддержка расширится и применить можно будет уже сейчас. Допустимые значения атрибута и ячейки нужно задать вручную.


input-wrapper {
/* ... */

> :is(
:not([slot]),
[slot]:not(
[slot="label"],
[slot="before"],
...
)
)
{
display: none !important;
}

> [slot="label"] {
grid-area: label;
}

> [slot="before"] {
grid-area: before;
}

...
}


Я использую пользовательский элемент <input-wrapper> как обёртку, без JS. Удобно и логично, что имя контейнера намекает на суть. Можно использовать <div class="input-wrapper">, если так удобнее и привычнее.

Так на HTML и CSS можно создавать компоненты с некоторым API «слотов». И использование для этого существующего атрибута slot выглядит логично, хотя и не по назначению. Можно в случае чего заменить на data-slot.

#css
🔥4🤯41😱1🗿1
Forwarded from enable design - о дизайне и доступности (Анжелика Герман)
AI и доступность

Делаем блиц-интервью про AI и доступность. Задаем одинаковые вопросы и получаем разные ответы ) Наш третий гость — Алексей Назаренко @alexnozer_dev — фронтенд-разработчик, специализирующийся на вёрстке, доступности, производительности и веб-стандартах. Леша, спасибо 💜

Ты пробовал использовать Claude или ChatGPT со скринридером или с клавиатуры? Как это работает на практике?

— Лично я не пробовал. Но читал о том, что в интерфейсах таких платформ полно проблем с доступностью.

Ты знаешь кейсы, где AI улучшил доступность продукта?

— Есть несколько фич, которые кажутся мне хорошим использованием AI в доступности:

- Автогенерация субтитров на видео в TikTok, YouTube и подобных сервисах;
- Возможность сгенерировать описание изображения (alt) в Firefox или Shopify;
- Функция распознавания изображения с камеры в реальном времени в ChatGPT или Seeing AI;

Ещё знаю, что AI используют при аудите для анализа контента и поиска нарушений WCAG. Но пока о каких-то серъёзных успеха в этом направлении не слышал.

Есть ли риск, что AI-инструменты для доступности обучены на данных, которые не отражают реальный опыт людей с ограничениями по здоровью? Что должно измениться в том, как мы обучаем модели?

— Все современные модели знают о доступности много, потому что это гигантские базы знаний. Сложность в том, чтобы эти знания оттуда извлечь. По умолчанию модели выдают статистически наиболее вероятный код. Дела с доступностью обстоят плохо, что подтверждают отчёты WebAIM. Поэтому в среднем сгенерированный код будет содержать проблемы доступности.

С обучением моделей, скорее всего, уже ничего не сделать. Разве что в мире резко большинство сайтов и сервисов станут доступными и появятся новые хорошие кодовые базы, где это всё учтено. Но такого не будет. А учитывая волну нового сгенерированного кода за последние годы, всё только хуже.

Поэтому нужно обкладывать агентов скиллами, правилами, инструкциями для работы, давать примеры доступного кода, проводить код ревью людьми, которые разбираются. ИИ волшебным образом не сделает мир доступнее, нужна инициатива и эксперты.

70% новых приложений создаётся на low-code и no-code платформах. Люди без технической базы делают продукты для миллионов. AI научит их паттернам доступности?

— Я не слышал, чтобы без технической базы с помощью AI создавали продукты на миллионы пользователей. Истории «я за два вечера навайбкодил SaaS и заработал много денег» мне кажутся пылью в глаза. Десятки, сотни или тысячи пользователей, но не миллионы. Если говорить о крупных корпорациях, которые массово внедряют AI в процессы разработки, то там, всё же, есть техническая база.

Не думаю, что AI научит доступности. Он способен воспроизводить шаблоны доступности и выдавать неплохие решения при должной настройке и в руках опытных разработчиков. Тех, кто росто генерирует код и не вникает в него, AI ничему не научит.

В России активно пользуются технологиями с Ai — зарубежные скринридеры, программы по типу Open your eyes, есть ли надежда, что подобные сервисы будут разрабатываться российскими разработчиками?

— Думаю, что надежда есть. Однако стимулов меньше, чем в странах Евросоюза или США. Нужно развивать культуру доступности и работать над законодательной базой, по аналогии с ADA или EAA. Культура будет способствовать информационной осведомлённости о важности доступности, а законы обяжут выделять на это ресурсы.
👍62🥰1
Фреймворки не нужны?

Недавно видел фрагмент со стрима Тимура Шемсединова, затем реакцию на этот фрагмент автора канала «Фронтенд как проблема». Тимур высказал мнение: «в современном мире фронтенд фреймворки не нужны, это инерция».

Громкое заявление, но основано на том, что в современных браузерах появилось много API, с помощью которых можно создавать приложения без фреймворков. А массовое использование фреймворков — это инерция и сила умолчания.

Также речь идёт о паразитной сложности. С одной стороны, фреймворки созданы для уменьшения сложности. Они прячут от нас готовые реализации сложных концепции. На разработку этого с нуля приходилось бы тратить много ресурсов.

С другой стороны, необходимы знания фреймворка, иногда специфические (хуки, мемоизация и борьба с ререндерами в React). С фреймворком приходят тысячи зависимостей. Нужно их проверять, обновлять, защищаться от атак.

Кроме того, фреймворки соревнуются между собой за внимание разработчиков. Они стараются быть как можно более универсальными, решать широкий спектр задач. Но это влечёт за собой больше и больше абстракций.

Можно выбрать Vue и использовать только 20% возможностей. Остальные 80% живут в кодовой базе (node_modules), скачиваются, обновляются, проходят процесс сборки (анализ графа зависимостей, Tree Shaking, удаление неиспользуемого кода).

Мало кто об этом задумывается, потому что это вопросы инфраструктуры. Всё это имеет свою цену. Паразитная сложность — цена удобства абстракций. И в этом, как мне кажется, основной посыл Тимура против фреймворков.

«Если не используется фреймворк, то будет написан свой». Абстракции всё равно будут написаны, это неизбежно. Но это свои, полностью контролируемые абстракции, решающие только нужную задачу, а не гипотетические задачи кого-то ещё.

Современные браузерные API забирают на себя часть функциональности. Многое можно перенести с клиента на сервер, сняв с браузера ответственность за роутинг, загрузку и хранение данных, управление состоянием и так далее.

Ну и золотое правило, о котором говорят, но которое забывают: для каждой задачи есть свой инструмент. Если проект не сложный, то нужны соответствующие инструменты. Многие проекты не такие сложные, какими кажутся.

Поэтому идея Тимура хоть и громко звучит, но точно не лишена смысла. Я писал про доклад «отказаться от зависимостей и не умереть» и делился сайтом Plain Vanilla, которые показывают, что это возможно, хотя и непривычно.

#js
👍144🔥1😎1
Вредные компоненты без JS

Мне нравится идея создания компонентов без JS. Видео Native CSS Components vs Modern UI Libraries показывает реализацию нескольких компонентов на HTML/CSS и сравнивает их с аналогами из Shadcn.

Компоненты в Shadcn действительно переусложнены. Но у показанных в видео аналогов есть проблемы. Рассмотрим, что не так с каждой реализацией и как можно сделать лучше (сейчас или в будущем).

Диалог

Диалог реализован с помощью Popover API. popover не задаёт нужную семантику. <div popover> остаётся контейнером с role="group". У диалога должна быть соответствующая семантика, состояния, поведение и UX.

Есть <dialog>, который обладает нужной семантикой и свойствами. Он работает с popover для немодальных диалогов, а с command для модальных и немодальных. Атрибут closedby="any" добавляет закрытие по нажатию вне диалога.

<!-- немодальный диалог с popover -->
<button
popovertarget="dlg"
popovertargetaction="show"
>
Open dialog
</button>
<dialog id="dlg" popover>
...
<button
popovertarget="dlg"
popovertargetaction="hide"
>
Close
</button>
</dialog>

<!-- немодальный диалог с command -->
<button
commandfor="dlg"
command="show-popover"
>
Open dialog
</button>
<dialog id="dlg" popover>
...
<button
commandfor="dlg"
command="hide-popover"
>
Close
</button>
</dialog>

<!-- модальный диалог с command и closedby -->
<button
commandfor="dlg"
command="show-modal"
>
Open dialog
</button>
<dialog id="dlg" closedby="any">
...
<button
commandfor="dlg"
command="close"
>
Close
</button>
</dialog>


Выпадающее меню


В видео выпадающее меню реализовано с помощью popover и Anchor Positioning. Как и с диалогом, меню не обладает нужной семантикой и поведением. Dropdown menu в Shadcn реализован как элемент с ролью menu.

Виджет menu предназначен для команд, а не навигационных ссылок. Внутри допустимы только определённые роли. Требуется навигация стрелками и Home/End. Для этого нужен JS. Всё может измениться с приходом focusgroup, но пока это экспериментальный API.

<button
commandfor="menu"
command="toggle-popover"
>
Open menu
</button>
<div
id="menu"
role="menu"
popover
focusgroup="menu"
>
<button role="menuitem">
Profile
</button>
<button role="menuitem">
Settings
</button>
<button role="menuitem">
Sign out
</button>
</div>


Всплывающая подсказка


В видео отображение подсказки реализовано через :hover/:focus и комбинатор ~. Роль подсказки — tooltip, а кнопка ссылается на неё через aria-describedby. Такая всплывающая подсказка нарушает критерий WCAG 1.4.13 Content on Hover or Focus.

Реализация поведения для соответствия 1.4.13 требует JS. Можно использовать popover с его закрытием по Esc, но отображение потребует JS. Interest Invokers API и popover="hint" позволят в будущем реализовать это без JS.

<button
interestfor="copy"
aria-describedby="copy"
>
Copy
</button>
<span
id="copy"
popover="hint"
role="tooltip"
>
Hover or focus me
</span>


Вкладки


Вкладки реализованы как набор радио-кнопок, которые в сочетании с псевдо-классом :checked и селектором :has() у родителя переключают дочерние панели. Вновь не хватает нужной семантики и поведения и в целом это хак.

На канале есть разбор вкладок с использованием ссылок <a> и псевдо-класса :target и почему это не вкладки. То же с радио-кнопоками и :has(). Реализовать вкладки без JS сейчас нельзя, но, возможно, появятся новые элементы или примитивы.

Аккордеон

На последок виджет аккордеона. В видео он сделан из нескольких <details> и <summary>. Также демонстрируется новый псевдо-элемент ::details-content и свойство interpolate-size: allow-keywords для анимации раскрытия.

Вот аккордеон можно использовать вместо решений на JS, это не хак. Тут ещё можно углубиться в различия аккордеона и раскрываемых панелей, но в другой раз. А примеры диалога, меню, подсказки и вкладок из видео использовать не стоит.

#html #css #js #ui
👍71🤝1
Пример приложения на чистом Node.js и Web API

Я не так давно делился мнением по поводу высказывания Тимура Шемсединова о том, что фронтенд фреймворки не нужны и это инерция. Я неоднократно видел, что подход разработки с использованием встроенных Web API вполне возможен.

Тимур поделился репозиторием с proof of concept своей мысли. Это приложение редактирования профиля: простой сервер с роутером на чистом Node.js и фронтенд в SPA-стиле на ES-модулях, веб-компонентах c Shadow DOM и шаблонами.

Приложение позволяет смотреть, искать, создавать, изменять и удалять профили экспертов с валидацией данных на клиенте и сервере, вычисляемыми полями и общей выделенной доменной логикой для клиента и сервера, не простой CRUD.

Особенность проекта в том, что он целиком построен на чистом Node.js и браузерных API и не использует рантайм-зависимости, фреймворки и сборщики. Это пример ровно того подхода, о котором говорил Тимур. Ограничения прописаны в репозитории:

- Запросы данных напрямую из компонентов запрещены;
- Только встроенные API;
- Никаких рантайм-зависимостей из npm, dev-зависимости допустимы, но опциональны (проект использует ESLint и Prettier);
- Чистый Node.js для серверной части;
- Стандартные ES-модули на клиенте;
- Веб-компоненты, Template API и Navigation API на клиенте;
- <template> для фрагментов UI и веб-компонентов с Shadow DOM, где уместно.

В репозитории проекта есть подробный README с описанием архитектуры сервера и клиента, выделенной доменной логики, валидации, роутинга, декларативного рендеринга, шаблонов, сборки страниц, API, хранилища, пользовательских сценариев.

Я изучил код. Мне всё понятно, код аккуратный и лаконичный, есть чёткая структура, понятно, где что находится и как это расширять. И нет никаких монструозных абстракций, которых все боятся, когда речь о разработке на чистом Web API.

Да, это не похоже на общепринятый «индустриальный стандарт». Тут нет декларативных шаблонов с реактивностью, сложного клиентского состояния, а само приложение ориентировано на сервер.

Зато ноль зависимостей, не нужно думать о tree shaking, смотреть bundle analyzer, искать дубликаты, регулярно обновляться, следить за релизами и уязвимостями, слать issue и ждать релиз с исправлением бага, бороться с ограничениями, конфигурировать сборку.

Весь код полностью доступен, его можно доработать под потребности, он решает только проектные задачи, лишнее можно удалить, чтобы не висело мёртвым грузом, недостающие абстракции можно дописать, с LLM это довольно быстро.

Многие команды застревают в поддержке. Проект работает на старых версиях, уходят месяцы на обновления и оптимизацию бандлов, выделяются отдельные специалисты или инфраструктурные команды (кто-то называет их FrontOps).

Я вижу тут компромисс и trade off: DX, некоторые удобства, синтаксический сахар и «индустриальный стандарт» меняется на простоту поддержки, устойчивость, долговечность и полное владение кодовой базой.

Снижается паразитная сложность, система становится менее громоздкой, упрощается деплой и поддержка. Сложность становится контролируемой. Но это требует опыта, дисциплины, хорошего знания стандартов и Web API.

В процессе возникает «фреймворк», хотя это набор специфичных для проекта решений, что лично я бы фреймворком не назвал. Пусть будет «свой велосипед». React тоже был «своим велосипедом» для решения проблем команды Facebook.

Возможно, я предвзят, потому что сам предпочитаю работать со стандартными API и не люблю лишние зависимости. В любом случае, советую посмотреть проект и заложенные там идеи. Без фреймворков писать можно и это не так страшно, сколько непривычно.

#js #web_api #architecture
👍11🥱10🤡4🤮3💅31❤‍🔥1
Пустые кнопки и ссылки

По данным отчёта WebAIM Million 2026 на 46.3% сайтов встречаются пустые ссылки и на 30.6% сайтов — пустые кнопки. Обе эти проблемы входят в топ 6 самых частых нарушений доступности, обнаруживаемых с помощью WAVE.

Пустые кнопки и ссылки — это те, у которых имя не задано ни одним из способов и браузер вычисляет его как пустую строку. Вспомогательные технологии используют имя для идентификации элементов, а люди полагаются на это при взаимодействии.

- Программы чтения с экрана озвучивают тип элемента и его имя: «Каталог, ссылка» или «Добавить в корзину, кнопка». Пользователь слышит и понимает, что за элемент и что он делает;
- Программы голосового управления ищут элементы по имени: «Нажми Каталог» или «Нажми Добавить в корзину» программа найдёт элемент на экране и нажмёт на него;
- Программы чтения с экрана и специальные браузерные расширения выводят все ссылки или кнопки со страницы в виде списка, имя используется при формировании этого списка.

При отсутствии имени элементы будут анонимными. Просто какая-то «Ссылка» или «Кнопка». Не понятно назначение, сложно идентифицировать, не удобно взаимодействовать. Всё это создаёт препятствия для пользователей.

Чаще всего пустые ссылки и кнопки получаются, когда они отображаются в виде иконок: три горизонтальные линии, тележка, крестик, урна, стрелка, плюс и так далее. Визуально может быть понятно, но программно — нет.

<button type="button">
<svg>
<!-- иконка с тремя линиями -->
</svg>
</button>

<a
href="/cart"
class="icon icon--cart"
>
<!-- иконка тележки в CSS -->
</a>

<button type="button">
<i class="fa-solid fa-trash">
<!-- иконка мусорки из шрифта -->
</i>
</button>

<a href="/catalog?page=3">
<!-- внешняя иконка стрелки -->
<img
src="/assets/arrow-left.svg"
alt=""
>
</a>


Это примеры «пустых» кнопок и ссылок без имени. Базовое правило таково: у кнопок и ссылок должно быть короткое, лаконичное и понятное имя, заданное одним из способов:

- атрибут aria-labelledby;
- атрибут aria-label;
- элемент <label> (только для <button>);
- текстовый контент;
- атрибут title.

Порядок указывает приоритет применения. aria-label переопределяет имя, заданное через <label>, а aria-labelledby, в свою очередь, переопределяет имя, заданное в aria-label. Стоит выбрать какой-то один способ и не смешивать во избежание неожиданностей.

Наилучшим из способов будет видимый текстовый контент. Он соответствует первому правилу ARIA: не полагаться на атрибуты ARIA и использовать встроенные способы. Также видимый текст можно прочесть в отличие от скрытого.

То есть кнопка или ссылка с иконкой и текстом — лучшее из возможных решений. Глядя на текст становится понятно, что нужно сказать для активации элемента голосом. Также люди с когнитивными нарушениями могут не понимать метафору иконки.

<button type="button"> 
<svg aria-hidden="true">
<!-- иконка с тремя линиями -->
</svg>
Меню
</button>

<a
href="/cart"
class="icon icon--cart"
>
<!-- иконка тележки в CSS -->
Корзина
</a>

<label for="delete">
Удалить
</label>
<button type="button" id="delete">
<i
class="fa-solid fa-trash"
aria-hidden="true"
>
<!-- иконка мусорки из шрифта -->
</i>
</button>

<a href="/catalog?page=3">
<!-- внешняя иконка стрелки -->
<img
src="/assets/arrow-left.svg"
alt=""
>
Предыдущая страница
</a>


При наличии видимого текста стоит скрыть иконки, если они выполняют только функцию визуальной подсказки и текст уже описывает это. Подойдёт атрибут aria-hidden="true" или пустой alt="" у <img>, что скрывает элементы из дерева доступности.

Видимый контент это хорошо, но не всегда возможно, потому что часто дизайнеры против лишнего визуального шума и загромождения интерфейса, особенно на мобильных. Поэтому распространены именно ссылки и кнопки только с иконками.

В таком случае придётся прибегнуть к скрытым подписям, которые задают имя, но не видны в интерфейсе. Если следовать всё тому же правилу ARIA, то есть несколько способов задать скрытое имя без использования атрибутов ARIA:

- Элемент <title> внутри <svg> задаёт альтернативный текст для изображения-иконки (работает как alt у <img>) и используется как текстовый контент для вычисления имени;
- Специальный класс visually-hidden делает элемент визуально невидимым, но всё ещё доступным как текстовый контент, который задаёт имя;
- Атрибут title задаёт имя элементу при отсутствии контента, но текст в атрибуте отображается в виде системной всплывающей подсказки только при наведении (и при фокусе в Edge);
- Атрибут alt задаёт альтернативный текст для изображения и его значение также используется как текстовый контент и задаёт имя элементу.

Элемент <title> и атрибут title создают системные всплывающие подсказки. C ними много проблем: появляются только при наведении мыши, не реагируют на настройки размера текста и так далее. Не рекомендуется их использовать.

<button type="button">
<svg>
<title>
Меню
</title>
<!-- иконка с тремя линиями -->
</svg>
</button>

<a
href="/cart"
class="icon icon--cart"
>
<!-- иконка тележки в CSS -->
<span class="visually-hidden">
Корзина
</span>
</a>

<button
type="button"
title="Удалить"
>
<i class="fa-solid fa-trash">
<!-- иконка мусорки из шрифта -->
</i>
</button>

<a href="/catalog?page=3">
<!-- внешняя иконка стрелки -->
<img
src="/assets/arrow-left.svg"
alt="Предыдущая страница"
>
</a>


Если прибегнуть к ARIA, то имя кнопок и ссылок можно задать с помощью атрибутов aria-label или aria-labelledby. Можно указать как на самих элементах, так и на иконках в виде альтернативного текста, который будет использован как текстовый контент кнопки.

<button type="button">
<svg
role="img"
aria-label="Меню"
>
<!-- иконка с тремя линиями -->
</svg>
</button>

<a
href="/cart"
class="icon icon--cart"
aria-labelledby="cart-label"
>
<!-- иконка тележки в CSS -->
<span id="cart-label" hidden>
Корзина
</span>
</a>

<span id="del-icon" hidden>
Удалить
</span>
<button type="button">
<i
class="fa-solid fa-trash"
role="img"
aria-labelledby="del-icon"
>
<!-- иконка мусорки из шрифта -->
</i>
</button>

<a
href="/catalog?page=3"
aria-label="Предыдущая страница"
>
<!-- внешняя иконка стрелки -->
<img
src="/assets/arrow-left.svg"
alt=""
>
</a>


- role="img" превращает <svg> в <img>, а aria-label в таком случае работает как alt. Альтернативный текст используется как имя элемента;
- aria-labelledby ссылается на элемент по указанному id и текст этого элемента используется как имя, при этом сам элемент может быть скрыт;
- aria-labelledby также работает как alt для элементов с role="img" и берёт альтернативный текст из указанного элемента;
- самый простой и короткий способ — просто указать ссылке или кнопке атрибут aria-label, который задаёт имя элементу.

Какой бы способ вы ни выбрали, всегда проверяйте значение Name у элемента на панели Accessibility в DevTools и проходитесь программой чтения с экрана. Не лишним будет прогнать страницу через Axe и подключить линтеры.

#html #a11y
👍821🔥1
Вау-эффекты и проблемы гидратации

Прочитал пост в канале Максима Морева о вау-эффектах на сайтах. Контекст: сайт документации drag-n-drop плагина для Vue создан на VitePress и на страницах есть мешающие чтению визуальные эффекты. Красиво, но не практично.

Я решил зайти на сайт со своего телефона. Модель не флагманская, но относится к high-end категории. Я увидел… шапку с логотипом, иконкой поиска и бургером, переливающуюся полоску, пустоту и подвал с копирайтом.

При нажатии на бургер, иконка меняется на крестик, но меню не открывается. При нажатии на поиск перенаправляет на страницу поиска, но поля ввода не видно. Нажатие на логотип возвращает на главную. Везде пустота.

Шанс моего ухода с такого сайта 100%, я не вижу буквально ничего, сайт бесполезен. Причина, как выяснилось, в гидратации. Где-то SSR HTML не сошёлся с клиентским деревом компонентов и у контента не удалилось свойство opacity: 0.

Досадная ошибка, которая сделала сайт бесполезным. Все мы допускаем ошибки и это нормально. Но не будь этот сайт создан на VitePress с гидратацией на клиенте и вау-эффектами, эта ошибка бы технически не могла возникнуть.

Это сайт документации — текст, блоки кода (тоже текст), демо и изображения. Контент статический и требует минимального JS для меню, поиска, переключения вкладок и подобных виджетов. Цель сайта — дать информацию, а не показать эффекты.

Нет ни единого повода добавлять на такой сайт гидратацию, клиентский роутинг, реактивность и компонентную модель Vue. Это всё обходится сайту в 262кб (1.5мб без cжатия) JS и сопутствующими затратами на его обработку без какой-то пользы.

Набор предварительно сгенерированных лёгких HTML-страниц (SSG), точечный JS для виджетов, навигация ссылками <a>, View Transition, предварительная загрузка и Speculation Rules дадут тот же эффект «мгновенной навигации SPA» без JS.

VitePress умеет работать в режиме MPA без JS, но экспериментально. Альтернатива — Astro или любой генератор статических сайтов без JS в рантайме по умолчанию. Это проще, надёжнее, производительнее и не будет никаких ошибок гидратации.

#js #architecture
👍9💯3🤝21🔥1👾1
Prop for that

Адам Аргайл выпустил небольшую JS-библиотеку prop-for-that, которая следит за различными параметрами страницы и записывает значения в пользовательские свойства CSS. Это позволяет писать стили на основе значений этих свойств.

Что библиотека может отслеживать:

- Положение курсора (x, y) в px и % от области просмотра;
- Размер области просмотра;
- Текущее время (часы, минуты, секунды, текущий timestamp);
- Количество кадров в секунду;
- Онлайн-статус;
- Состояние видимости и фокусировки страницы;
- Параметры сети (тип, downlink, rtt, режим экономии данных);
- Состояние батареи (процент заряда, заряжается ли);
- Нагрузку на CPU;
- Прокрутку (направление и скорость);
- Визуальную область просмотра (pinch-zoom, отображение виртуальной клавиатуры);
- Виртуальную клавиатуру (открыта ли, размеры, координаты);
- Ориентацию устройства;
- Параметры акселерометра;
- Геолокацию;
- Плотность пикселей экрана;
- Количество ядер процессора;
- Доступный объём оперативной памяти;
- Ширину полосы прокрутки;
- Тип навигации;
- Мета-данные (theme-color, og-image, color-scheme);
- Размеры элемента;
- Видимость элемента;
- Значение <input type="range">;
- Свойства текстовых полей (длина, пустое ли, валидное ли, лимит символов);
- Состояние валидности полей ввода;
- Состояния полей («чистое», «грязное», тронутое, нетронутое, изменённое, отправленное);
- Количество полей (валидных, не валидных, заполненных);
- Состояния <select> (индекс, количество опций, количество выбранных опций, текущее значение);
- Цвет из <input type="color">;
- Состояния <audio> и <video> (прогресс, текущее время, длительность, пауза, громкость);
- Состояния <img> (физические размеры, загружено ли, ошибка загрузки);
- Цвета <img> (доминантный, акцентный, тёмный, светлый, средний, температура);
- Цвета <video> (доминантный, акцентный);
- Состояние урезанного текста (line-clamp).

Достаточно большой список. Плюс к этому есть JS API и система плагинов, поэтому библиотеку можно расширять. Некоторые свойства глобальные, как состояние сети, процессора или батареи. Такие свойства записываются в :root.

Другие свойства работают на уровне элементов. Чтобы библиотека отслеживала свойства, нужно указать атрибут data-props-for у элемента и перечислить через запятую названия отслеживаемых свойств. По умолчанию ничего не отслеживается.

<div
class="box"
data-props-for="size"
style="resize: both"
>
</div>
<style>
.box::after {
counter-reset: w calc(var(--live-w)) h calc(var(--live-h));
content: counter(w) ' × ' counter(h);
}
</style>


В примере указано, что нужно отслеживать размеры (data-props-for="size"). В CSS будут доступны свойства --live-w и --live-h с актуальными значениями. Для примера они выводятся в элементе с помощью счётчиков.

В документации много примеров, где и как это можно применить. Особенно хорошо это работает в сочетании с Container Style Queries и функцией if(), позволяя делать динамическую стилизацию на основе значений свойств.

Счётчики и псевдо-элементы со свойством content можно использовать для вывода значений в интерфейсе. Также можно скрывать или показывать отдельные узлы по условию. В общем, фантазии хватает как это можно применить.

#css #js
👍12😍1
Отсутствующие подписи для полей

Более половины сайтов (51%) по данным отчёта WebAIM Million 2026 содержат поля ввода без подписей. У полей нет имени и нельзя идентифицировать какие данные необходимо ввести. Это одна из 6 самых частых ошибок в WAVE.

Исправить эту ошибку не сложно. Нужно указать подпись (имя) одним из способов. Порядок отражает приоритет применения:

- aria-labelledby
- aria-label
- <label>
- title
- placeholder
- aria-placeholder

Лучше всего указывать видимые подписи в <label> и связывать их с полями через for и id. Или оборачивать в <label> с подписью внутри. Эти варианты не требуют ARIA, что соответствует первому правилу. Поэтому они предпочтительны.

<label for="first_name">
Имя (обязательно)
</label>
<input
type="text"
name="first_name"
id="first_name"
autocomplete="given-name"
autocapitalize="words"
placeholder="Алексей"
required
>

<label>
Имя (обязательно)
<input
type="text"
name="first_name"
autocomplete="given-name"
autocapitalize="words"
placeholder="Алексей"
required
>
</label>


Атрибут title использовать не стоит. Он задаёт имя полю, но также создаёт системную всплывающую подсказку. Эта подсказка не соответствует требованиям доступности по критериям 1.4.4, 1.4.10, 1. 4.12, 1.4.13.

Иногда дизайнеры рисуют подпись внутри поля и это выглядит как заполнитель. В таком случае разработчик может указать атрибут placeholder. Хотя он задаёт имя полю, он предназначен не для этого. Используйте атрибут по назначению.

Когда подпись находится внутри поля, можно прибегнуть к технике плавающих подписей. Это когда при фокусе на поле, подпись подъезжает вверх и остаётся видимой. Техника не лишена проблем, но это лучше, чем подпись в placeholder.

aria-label можно применить для быстрого исправления отсутствующей подписи, но она не будет видна визуально. Это нарушает один из критериев WCAG и ухудшает доступность. Поэтому лучше избегать aria-label для подписей.

aria-labelledby работает как <label>, но наоборот: поле ссылается по id на элемент, который действует как подпись. Такой способ подойдёт для нестандартных полей ввода или если нет возможности добавить <label> в разметку.

Что касается критериев, применимых к подписям, то их пять:

- 1.3.1 Info and Relationship — подпись должна быть программно связана с полем, а её визуальная информация программно обозначена;
- 2.4.6 Headings and Labels — подпись, если она есть, должна быть понятной и описательной;
- 2.5.3 Label in Name — видимая подпись должна полностью или частично соответствовать программно заданному имени поля;
- 3.3.2 Labels or Instructions — у полей должна быть видимая подпись или инструкция для обозначения требуемой информации;
- 4.1.2 Name, Role, Value — у поля должно быть программно заданное имя.

Вариант с <label> соответствует всем критериям, поэтому он предпочтительный. Всем критериям будет соответствовать вариант с aria-labelledby, ссылаясь на элемент с видимым описательным текстом. Это резервный вариант.

<label for="first_name">
Имя (обязательно)
</label>
<input
type="text"
name="first_name"
id="first_name"
autocomplete="given-name"
autocapitalize="words"
placeholder="Алексей"
required
>

<span id="first_name">
Имя (обязательно)
</span>
<input
type="text"
name="first_name"
autocomplete="given-name"
autocapitalize="words"
placeholder="Алексей"
required
aria-labelledby="first_name"
>

#html #a11y
👍121❤‍🔥1
CSS Linked Parameters

При использовании встроенных SVG-иконок в HTML удобно то, что к ним есть доступ в CSS. Можно, например, изменять цвета, скрывать отдельные части иконки, менять толщину линий, радиус окружности, применять анимации и так далее.

<a href="...">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
>
<path d="..."/>
</svg>
Назад
</a>
<style>
a {
color: var(--color-link);
transition: color .3s linear;

svg path {
fill: currentColor;
transition: fill .3s linear;
}
}

a:hover {
color: var(--color-link-hover);
}
</style>


Однако при использовании внешних SVG-иконок доступ в CSS теряется. Отчасти это можно обойти через SVG-спрайты, но у них есть свои нюансы. CSSWG выпустила черновик CSS Linked Parameters, который предлагает решение проблемы.

Суть предложения в том, чтобы передавать через URL специальные параметры и затем получать к ним доступ через переменные окружения в атрибутах и CSS. Так можно будет передать цвет во внешний SVG-файл, подключаемый через <img> или url().

Дисклеймер: это обзор раннего предложения новых функций. Синтаксис может измениться в будущем или от функций могут отказаться


<!--
#param() задаёт параметр
--color со значением grern
-->
<img src="icon.svg#param(--color,green)">

<img src="icon.svg">
<style>
img {
/*
Параметры можно задать в CSS
с помощью link-parameters.
Это добавит к src и
ссылкам на все ресурсы
#param(--color,green)
*/
link-parameters:
param(--color, green)
;
}

.icon {
/*
Параметры можно задать
в функции url() в CSS
*/
background-image:
url(
"icon.svg",
param(--color, green)
)
;
}
</style>

<!-- icon.svg -->
<svg>
<!--
env() считывает переданный
в URL параметр --color, и
устанавливает в атрибут fill,
значение по умолчанию black
-->
<path
fill="env(--color, black)"
d="..."
/>
</svg>

<!-- icon.svg -->
<svg>
<!-- можно задать в CSS -->
<style>
path {
fill: env(--color, black);
}
</style>
<path d="..."/>
</svg>


Можно передавать несколько параметров, перечислив их в URL через &, указав несколько param() через пробел в url() или через запятую в link-parameters. В качестве значения могут быть пользовательские свойства CSS.

<!-- параметры в URL через & -->
<img src="icon.svg#param(--color,green)&param(--stroke,1)">

<img src="icon.svg">
<style>
img {
/*
несколько param() в link-parameters,
доступны пользовательские свойства
*/
link-parameters:
param(--color, var(--color-primary)),
param(--stroke, 1)
;
}

.icon {
/*
несколько param() в url(),
доступны пользовательские свойства
*/
background-image:
url(
"icon.svg",
param(--color, var(--color-primary))
param(--stroke, 1)
)
;
}
</style>


Таким образом появляется возможность передавать данные во внешние файлы. Основное назначение — <svg>, но также упоминается возможность передачи для <iframe>. Синтаксис фрагментов используется для обратной совместимости.

#html #svg #css
👍12🔥2😍1
Эффект съезда с тротуара

Читая материал о доступности, встретился с феноменом «эффект съезда с тротуара» (Curb cut effect). Его суть показалась мне интересной, чтобы написать и поделиться своими мыслями в виде поста.

Представьте проезжую часть и тротуар с бордюром вдоль неё. В некоторых местах есть съезды: бордюр спущен на уровень проезжей части, а сам тротуар немного наклонён. Это сделано для устранения барьеров людям на инвалидных колясках.

Но также это помогает людям с детской коляской, сумкой на колёсиках, тележкой, велосипедом, самокатом, скейтбордом и другими средствами личной мобильности или вещами с колёсами. Ещё это помогает пожилым людям.

Эффект съезда с тротуара — это социально-экономический феномен, суть которого в том, что решения, изначально созданные для узкой группы людей, в конечном итоге оказываются полезными и эффективными для более широкой группы.

Доступность сайтов и приложений часто воспринимается как специфическая работа ради малой группы пользователей, которой можно пренебречь. Тут как раз действует эффект съезда с тротуара. Улучшение доступности сайта даст больший эффект.

Внедрение практик доступности окажет положительный эффект на всех. Причём эффект распространяется не только на людей, но и на машины. Отмечено влияние на поисковые роботы, агенты ИИ, разные парсеры и программы.

Вот несколько примеров:

- Хороший шрифт с достаточным размером и контрастными цветами улучшает читабельность сайта, пользователям удобнее читать;
- Субтитры позволят пользователям смотреть видео без звука и читать субтитры в шумном помещении, если нет наушников;
- Достаточно большие интерактивные элементы легче нажимать пальцами или стилусом на смартфоне или планшете с сенсорным экраном;
- Подписи, подсказки и автодополнение у полей ввода помогут пользователям быстрее заполнять формы с меньшим количеством ошибок;
- Хорошо размеченная с помощью семантического HTML страница более понятна поисковым работам, агентам, лучше разбирается в режиме чтения;
- Текстовая расшифровка видео позволяет использовать поиск по тексту, переводить его на другой язык и копировать для различных целей;
- Правильно заданный язык страницы и элементов помогает программе-переводчику определить язык и предложить перевод.

Примеров можно ещё много придумать. Суть в том, что доступность можно и нужно рассматривать не как специальную задачу по адаптации сайта, а как общее улучшение качества, которое скажется на разных его аспектах и пользователях.

#a11y
👍16🤗2
Язык документа и его частей

В посте, посвящённому всемирному дню осведомлённости о доступности, упоминалось, что на 13.5% сайтов не задан язык документа. Радует, что это не много по сравнению с другими проблемами. Не радует, что это всё ещё 13.5%.

Исправление крайне простое: нужно задать у корневого элемента страницы <html> атрибут lang со значением, которое соответствует основному языку контента на странице в формате BCP 47.

<!-- Русский -->
<html lang="ru"></html>

<!-- Русский, Россия -->
<html lang="ru-RU"></html>

<!-- Английский -->
<html lang="en"></html>

<!-- Английский, Британия -->
<html lang="en-GB"></html>

<!-- Английский, США -->
<html lang="en-US"></html>


На этом можно было бы и закончить пост. Просто добавьте атрибут и это исправит одну из топ 6 ошибок доступности, которую обнаруживает WAVE. Но я расскажу ещё некоторые особенности и фишки вокруг языка страницы.

От языка страницы зависит отображение некоторых элементов, в частности <q>. Он предназначен для встроенных цитат и обрамляет текст в кавычки. Вид кавычек меняется в зависимости от языка (ёлочки или лапки).

Если сайт мультиязычный и хочется что-то менять в CSS в зависимости от языка, есть селектор :lang(). Он более мощный, чем селектор по атрибуту html[lang="..."] из-за масок и работы с вычисленным языком, а не значением в атрибуте.

ol:lang(en-US) > li::marker {
/*
Маркеры списка 1) 2) 3) для американскго английского
*/
content: counter(list-item) ') ';
}

ol:lang(ru, by, uk) > li::marker {
/*
Маркеры списка 1. 2. 3. для восточнославянских языков
*/
content: counter(list-item) '. ';
}


На странице могут быть элементы, контент которых не соответствует основному языку страницы: цитаты, термины, названия или переключатель языка. Такие элементы также нужно пометить соответствующим атрибутом lang.

Вообще lang — глобальный атрибут и его можно указывать у любых элементов. Язык наследуется вглубь по дереву от ближайшего родителя с lang. Сам атрибут переопределяет унаследованный язык.

<html lang="ru">
...
<body>
...
<button
type="button"
popovertarget="langs"
>
Язык
</button>
<dialog id="langs" popover>
<ul>
<li>
<a
href="/ru"
aria-current="true"
>
Русский
</a>
</li>
<li>
<a
href="/en"
lang="en"
hreflang="en"
>
English
</a>
</li>
<li>
<a
href="/es"
lang="es"
hreflang="es"
>
Español
</a>
</li>
...
</ul>
</dialog>
</body>
</html>


Атрибут hreflang помогает роботам понять, что страница по ссылке на другом языке. Это важно при SEO-продвижении мультиязычного сайта. К этому ещё стоит добавить ссылки на альтернативные версии текущей страницы на других языках.

<head>
...
<link
rel="alternate"
href="/en/catalog"
hreflang="en"
lang="en"
title="Catalog"
>
<link
rel="alternate"
href="/es/catalog"
hreflang="es"
lang="es"
title="Catalogar"
>
...
</head>


Среди контента могут быть общепринятые термины, названия брендов или фразы, которые написаны на другом языке, но не должны переводиться автоматическими переводчиками. На этот случай существует атрибут translate со значением no.

<p><span lang="fr" translate="no">Vite</span> позволяет быстро собирать <span lang="en" translate="no">frontend</span>-проекты различной сложности.</p>


#html #css #a11y
🔥114🤝2🌚1
Please open Telegram to view this post
VIEW IN TELEGRAM
13👍1🤝1