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

Автор: @alexnozer
Download Telegram
Прямая трансляция Я 💛 Фронтенд 2026

Сегодня состоится конференция Я 💛 Фронтенд 2026, на которой я вступлю с докладом про веб-компоненты. Присоединяйтесь к трансляции, начало в 11:00 по Москве, мой доклад в 11:50.
👍136🥰4🔥2😍1🌚1
50 оттенков веб-компонентов.pptx
29.5 MB
50 оттенков веб-компонентов

Только что выступил с докладом «50 оттенков веб-компонентов». Как и обещал, делюсь презентацией с выступления.
18👍6🔥2🌚1😘1
You don't know HTML: категория и контентная модель

Как разработчики определяют, какие HTML-элементы можно вкладывать в другие HTML-элементы? В целом, на уровне интуиции и логики. Понятно, что <li> должен быть в <ul> или <ol>, <td> в <tr>, а тот в <table>.

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

Категории — это группы элементов, объединённые какими-то общими свойствами. Один HTML-элемент может принадлежать сразу к нескольким категориям или не принадлежать ни к одной из них. Всего есть 18 категорий.

Контентная модель — это описание того, какие конкретные элементы или категории элементов допустимы. Соответствие контентной модели родительского элемента и категории дочернего — и есть правила вложенности.

Контентная модель неупорядоченного списка <ul> — ноль или более элементов <li> и script-supported elements. Это значит, что внутрь списка могут быть вложены только элементы <li>, <script> и <template>.

Контентная модель элемента списка <li>flow content, значит можно вложить любые элементы этой категории. Сам <li> при этом не относится к какой-либо категорий, поэтому его нельзя никуда вкладывать, если не указано иное.

В некоторых случаях контентная модель жёстко ограничена конкретными элементами, а не категориями, как у таблиц. Иногда есть правила, что конкретный элемент должен быть в определённом месте, как у <details>.

Есть контентная модель под названием transparent. Внутрь «прозрачных» элементов можно вкладывать то, что можно вкладывать в их прямого родителя. Такая модель присуща некоторым элементам, среди которых <a>.

Помочь с определением вложенности может сайт Can I Include. Он как раз сравнивает категорию и контентную модель. Другой способ проверить корректность вложенности — использовать HTML Validator.

У неверной вложенности есть последствия:
- В некоторых случаях браузеры перестраивают DOM;
- Нарушается семантика, что влияет на доступность;
- Может серьёзно ухудшаться производительность;
- Могут ломаться инструменты, которые анализируют HTML (браузерные плагины, онлайн-сервисы).

#ydkhtml
👍132🌚1🤝1
Оптимизация загрузки встраиваемых видео без JS

При встраивании видео и других виджетов через <iframe> есть проблема: ресурсы загружаются даже если пользователь не взаимодействует с виджетом. Для решения проблемы используется техника фасадов, о которой есть отдельный пост.

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

Для плееров YouTube и Vimeo есть веб-компоненты, которые реализуют технику фасадов: отображают обложку видео с фейковой кнопкой воспроизведения, а при нажатии подменяют всё на реальный <iframe> с плеером.

Это хорошо работает и оптимизирует загрузку ресурсов, но требует реализации на JS или подключения веб-компонента. Штефан Бауэр предлагает альтернативный способ реализации фасадов с помощью <details> и loading="lazy":

<details>
<summary>
<img
src="cover.jpg"
alt="Смотреть видео"
>
<svg aria-hidden="true">
<!-- фейковая кнопка -->
</svg>
</summary>
<div>
<iframe
src="..."
loading="lazy"
>
</iframe>
</div>
</details>


<iframe> с атрибутом lodaing="lazy" работает по аналогии с <img>. Содержимое фрейма и связанные ресурсы не загружаются до тех пор, пока он не окажется в области просмотра или достаточно близко к ней.

Если поместить такой фрейм в <details>, то он будет скрыт. Это значит, что фрейм окажется в области просмотра только в момент открытия <details>. В <summary> можно поместить обложку и фейковую кнопку, а сам элемент скрывать при открытии <details>.

details[open] > summary {
visibility: hidden;
}


Пока пользователь не нажмёт на <summary> c обложкой, фрейм не будет загружаться. После нажатия <details> раскрывается, фрейм начинает загружаться, а <summary> скрывается. Получается ленивая загрузка виджетов без JS.

#html #performance
👍13🔥94🤯3🤩1🌚1
Современный CSS

Делюсь сайтом, на котором собраны сниппеты современного CSS, которые заменяют решения на JS, функции препроцессоров, хаки и более громоздкие конструкции самого CSS. Есть фильтрация по уровню поддержки.

Помимо сниппетов на сайте есть разделы со статьями, ссылками на ресурсы, новинками CSS и песочницами (пока только для резиновой типографики). Сайт обновляется, поэтому имеет смысл подписаться на рассылку или RSS.

#css
🔥105👍42🌚1
Подборка ссылок

Неделя выдалась напряжённой, не успел подготовить посты. Чтобы совсем не оставить канал без постов, собрал небольшую подборку ссылок, которые попались на этой неделе. Часть ссылок взята из Веб-стандартов, спасибо Вадиму.


Standard HTML Video & Audio Lazy-loading is Coming!

Скотт Йель поделился новостью, что его команде удалось стандартизировать ленивую загрузку аудио и видео. Работа началась ещё в 2024 году: описание и обсуждение предложения, тесты веб-платформы (WPT), патчи в Firefox и WebKit.

<video
loading="lazy"
autoplay
playsinline
muted
controls
src="path/to/sloth.webm"
poster="path/to/sloth.jpg"
></video>



aria-haspopup might not do what you think it does

Мануэль Матузович пишет о неверном использовании атрибута aria-haspopup. У попапа должна быть роль menu, listbox, tree, grid или dialog. Не во всех случаях aria-haspopup уместен. Я на днях с этим столкнулся на реальном проекте.


An in-depth guide to customising lists with CSS

Подробный гайд по стилизации списков с использованием разных возможностей CSS от Ричарда Раттера. Я тоже писал об этом:

- You don't know HTML: нумерованные списки
- Стилизация счётчиков списка
- counters() и counter-style


Practical guide to the <img> element: from the basics to LCP

Джоан Леон рассказывает об элементе <img> с точки зрения производительности: атрибуты width и height, <picture> и современные форматы изображений, decoding, loading, fetchpriority оптимизацию LCP и CDN.


Stylable Select

Я писал про стилизуемый <select> и работы по дальнейшему развитию возможностей стилизации. Брехт Де Рёйте, который активно вовлечён в эту тему, поделился коллекцией примеров стилизуемого селекта на Codepen.


csskit

Кит Сиркель, браузерный инженер, работающий над Firefox, поделился своим проектом csskit. Это написанный на Rust инструмент для работы с CSS: форматтер, линтер, минификатор, транспилятор, бандлер и анализатор в одном флаконе.

#html #css #a11y #performance
1👍8🥴1🌚1
Создание сайтов с использованием LLMS*

На пост именно с таким заголовком я наткнулся в блоге Джима Нильсена. Вопреки, речь пойдёт не об ИИ, поэтому добавил звёздочку. Речь о подходе, который автор назвал «Lots of little HTML pages», сокращённо LLMS.

С помощью Cross-document View Transition можно создавать плавные переходы между разными страницами на CSS. BFCache делает переходы «вперёд»/«назад» мгновенными. Маленькие и лёгкие страницы загружаются быстро.

Эти три особенности позволяют вместо кнопки бургер-меню на JS сделать ссылку на отдельную страницу с меню, которая быстро загружается за счёт малого размера и плавно отображается за счёт View Transition.

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

Именно так бургер-меню, поиск и фильтрация реализованы в блоге Джима. Чтобы наглядно увидеть суть, советую пощёлкать сайт. А реализация предельно простая: несколько отдельных страниц и обычные ссылки <a>.

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

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

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

#html #css #ui
🔥7👍4🤔32🌚1🤨1
Кампания Build Awesome

Font Awesome запустила кампанию Build Awesome на Kickstarter. Ранее они уже проводили подобное с Web Awesome: собрали денег и выпустили ребрендинг библиотеки Shoelace с улучшениями и новыми PRO функциями.

На этот раз планируется ребрендинг Eleventy — популярного генератора статических сайтов. Его создатель — Зак Лезерман, как и создатель Shoelace — Кори ЛаВиска, перешли работать в Font Awesome вместо со своими проектами.

Учитывая историю с Web Awesome и данные на странице кампании, получается следующее:

- Следующая мажорная версия Eleventy (4.0) выходит под новым названием Build Awesome. Просто новое название, проект по-прежнему останется с открытым исходным кодом, сохранит синтаксис, API и будет совместим с существующими плагинами;
- Появится платформа, которая должна упростить создание и поддержку сайтов. Предположительно это веб-интерфейс с админкой, где можно будет собрать сайт с помощью Eleventy, выкачать статику, опубликовать на CDN и управлять контентом;
- Появятся шаблоны для блога, портфолио и персонального сайта для быстрого старта, а также плагины для использования иконок Font Awesome и веб-компонентов Web Awesome;
- Появятся платные PRO функции: коллаборативное редактирование, платформа для превью и публикации, премиум-шаблоны, DevTools для проверки качества сайтов, инструменты миграции, премиум поддержка и, конечно же, ИИ функции.

Также в зависимости от суммы сборов заявлены дополнительные функции:

- Система темизации
- Плагин для SEO/Favicon/OpenGraph
- Инструменты для новостной рассылки
- Интеграция со Stripe
- Инструменты синдикации для социальных сетей
- Аналитика
- Формы
- Плагин для шрифтов
- PWA с поддержкой local-first/offline-first
- Медиа-библиотека аудио/видео

Чтож, посмотрим, что из этого выйдет. Для любителей Eleventy это, возможно, тревожный звоночек. С другой стороны, это обеспечит проекту стабильность и долговечность, авторам тоже нужно кормить себя и семью.

Ядро Web Awesome и основные компоненты остались доступными в виде проекта с открытым исходным кодом при ребрендинге Shoelace. Аналогично должно быть с Eleventy. PRO функции не обязательны, а ядро сохраняется как есть.

#tools
1🤔21💩1🌚1💯1👨‍💻1
Применение новых функций веб-платформы с учётом кроссбраузерности

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

Знакомо? Это как мем наоборот: пока кто-то живёт в 2026, мы живём в 2018. То есть используем только стабильные функции которые давно и широко поддерживаются. Как тогда использовать все эти классные новые функции?

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

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

View Transition можно добавить прямо сейчас. Если браузер не умеет, функция будет пропущена (при наличии проверки) и ничего не сломается, просто не будет плавного перехода. Многие функции можно использовать по такому принципу.

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

.section {
background-image:
url('section-gradient.jpg')
;
background-image:
conic-gradient(/*...*/)
;
}


Директива @supports и JS API CSS.supports() позволяют проверять поддержку новых функций CSS и реализовывать резервные. Аналогично в JS можно проверить наличие того или иного свойства, метода или объекта.

Для более сложных функций, как Popover API, Anchor Positioning или Invoker Commands, требуются полифилы. Нужно решить, готовы ли вы на частичное замещение функции у той части аудитории, у которой она не доступна.

Если функция отсутствует у 5-10% пользователей, кажется, что уместно добавить для них полифил и оставить 90-95% пользователей со встроенной функцией. Если соотношение другое, возможно, стоит повременить.

С оценкой зрелости функций поможет Baseline. У каждой функции есть один из трёх статусов:

- Limited Availabity — функция внедрена в один или несколько основных браузеров, но не во все;
- Newly Available — функция внедрена во все основные браузеры относительно недавно, поддержка узкая;
- Widely Available — функция внедрена во все основные браузеры как минимум 2.5 года и широко поддерживается.

Функции в статусе Widely Available можно использовать на проектах и они будут доступны для подавляющего большинства пользователей. 2.5 года это время, которого достаточно для широкого распространения функций.

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

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

#html #css #js #web_api
👍62🌚1🤝1
Бюджеты ресурсов для разработки производительных сайтов на 2026 год

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

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

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

Тестовые устройства, которые отражают 75й перцентиль пользовательского опыта:

Смартфоны:

- Samsung Galaxy A24 4G
- Samsung Galaxy A16 4G
- Samsung Galaxy A07 4G
- Xiaomi Redmi Note 13 Pro 4G
- Samsung Galaxy A51
- Motorola Moto E15

Ноутбуки:

- HP 14 dq3500nr
- Или аналоги в пределах 250$, на процессоре Celeron, c модулем eMMC и работающий на WIndows.

(сравните их характеристики со своим рабочим сетапом)

Тестовые параметры сети:

- Входящая скорость 9 Мбит/с;
- Исходящая скорость 3 Мбит/с;
- Задержка (RTT) 100 мс.

Эти устройства и параметры сети — базовый уровень, на который стоит ориентироваться при анализе производительности. Бюджеты рассчитаны на загрузку страницы за 3 и 5 секунд. Это приемлемые показатели для данных условий.

Из-за разных подходов к разработке выделяются две категории сайтов для формирования бюджетов:

- Сайты с «лёгким JS», где JS составляет 15% от объёма всех ресурсов на критическом пути. Это традиционные MPA с точечным JS для прогрессивного улучшения или подходы с островной архитектурой;
- Сайты с «тяжёлым JS», где JS составляет 50% от объёма всех ресурсов на критическом пути. Это соответствует подходам SPA и SSR с гидратацией на стороне клиента.

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

Разделение ресурсов на JS и всё остальное обусловлено тем, что JS — самый тяжёлый с точки зрения производительности ресурс. 500 кб JS не то же самое, что 500 кб изображения. «Остальное» включает в себя HTML, CSS, изображения и данные.

Теперь, когда все пояснения даны, собственно, сами бюджеты:

Загрузка за 3 секунды сайтов с «лёгким JS»:

- 0.3 мб JS
- 1.7 мб остальные ресурсы
- 2.0 мб всего ресурсов

Загрузка за 3 секунды сайтов с «тяжёлым JS»:

- 0.62 мб JS
- 0.62 мб остальные ресурсы
- 1.2 мб всего ресурсов

Загрузка за 5 секунд сайтов с «лёгким JS»:

- 0.57 мб JS
- 3.2 мб остальные ресурсы
- 3.7 мб всего ресурсов

Загрузка за 5 секунд сайтов с «тяжёлым JS»:

- 1.15 мб JS
- 1.15 мб остальные ресурсы
- 2.3 мб всего ресурсов

Как трактовать эти бюджеты? Если речь о сайте электронной коммерции (отрисовка HTML на сервере, точечный прогрессивный JS для интерактивности, категория «лёгкий JS»), то для загрузки за 3 секунды на целевом устройстве и сети нужно:

- чтобы HTML страницы, блокирующие стили и LCP-изображение в сумме составляли 1.7 мб;
- чтобы весь блокирующий JS (не defer или async) в сумме составлял 0.3 мб.
- чтобы общий вес этих ресурсов не превышал 2 мб.

Если речь о SPA, то для загрузки за 3 секунды нужно:

- чтобы начальный HTML, стили и LCP-изображение в сумме составляли 0.62 мб;
- чтобы весь JS приложения с данными в сумме составляли 0.62 мб.
- чтобы общий вес ресурсов не превышал 1.2 мб.

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

#js #performance
1👍53🔥1🥰1🌚1
Abusing Customizable Select

Патрик Броссет экспериментирует с новым стилизуемым <select> и некоторыми другими возможностями CSS. В статье он показывает 3 демо и описывает шаги и методы их реализации:

- Изогнутое меню выбора директории;
- Выбор карты из колоды в виде веера;
- Круговой выбор эмодзи.

Для каждого демо есть видео (на случай если браузер не поддерживает стилизуемый <select>), а также Codepen с полной реализацией. Все демо по своему интересные и демонстрируют возможности нового API.

#html #css #ui
👍92🔥1🌚1🤗1
Проблемы карточек, обёрнутых в ссылки

Карточки, обёрнутые в <a>, достаточно распространённый шаблон в Интернете. Дизайнеры хотят, чтобы нажатие в любое место карточки вызывало переход на соответствующую страницу. Самый простой способ — обернуть всё в <a>.

<a href="...">
<article class="card">
<img
src="..."
class="card__image"
alt="Чёрные беспроводные наушники Sony WH‑1000XM5, вид сбоку."
>
<h2 class="card__title">
Беспроводные наушники Sony WH‑1000XM5 (Чёрные)
</h2>
<p class="card__vendor">
Sony
</p>
<p class="card__price">
299 $
</p>
</article>
</a>


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

Чёрные беспроводные наушники Sony WH‑1000XM5, вид сбоку. Беспроводные наушники Sony WH‑1000XM5 (Чёрные) Sony 299 $


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

Быстро исправить проблему можно путём добавления на ссылку атрибута aria-label или aria-labelledby с привязкой к заголовку. Оба способа переопределяют имя, генерируемое из тестового контента. Всё содержимое не будет зачитываться.

<a 
href="..."
aria-label="Беспроводные наушники Sony WH‑1000XM5 (Чёрные)"
>
<!-- ... -->
</a>

<a
href="..."
aria-labelledby="heading"
>
<!-- ... -->
<h2
id="heading"
class="card__title"
>
Беспроводные наушники Sony WH‑1000XM5 (Чёрные)
</h2>
<!-- ... -->
</a>


При использовании aria-label важно, чтобы текст в атрибуте совпадал с видимыми текстом заголовка согласно критерию 2.5.3 Label in Name. С aria-labelledby это гарантируется за счёт связи с заголовком.

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

<a href="...">
<!-- ... -->
<a
href="..."
class="card__vendor"
>
Sony
</a>
<!-- ... -->
</a>


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

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

<article class="card">
<!-- ... -->
<h2 class="card__title">
<a
href="..."
clasd="card__link"
>
Беспроводные наушники Sony WH‑1000XM5 (Чёрные)
</a>
</h2>
<a
href="..."
class="card__vendor"
>
Sony
</a>
<!-- ... -->
</article>


.card {
position: relative;
}

.card__link::before {
content: '';
position: absolute;
inset: 0;
z-index: 1;
}

.card__vendor {
position: relative;
z-index: 2;
}


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

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

Более подробно эту технику описывает Хейдон Пикеринг в онлайн книге Inclusive Components. Также тему проблем с карточками затрагивает Адриан Розелли в своей статье Block Links, Cards, Clickable Regions, Rows, Etc.

В будущем, надеюсь, решать эту задачу можно будет с помощью Link Area Delegation API, разрабатываемого в рамках OpenUI. Это механизм делегации кликов с других элементов на ссылку, навеянный проблемами с карточками.

#html #css #a11y
222👍2🌚1💯1🆒1
CSS Web Components

Будучи набором браузерных API, веб-компоненты можно использовать самыми разными способами. Некоторые из таких способов применения получили в сообществе собственные названия. Один из них — CSS Web Components.

Суть подхода в том, чтобы взять от Custom Elements API минимум возможностей: никаких Shadow DOM, классов, методов жизненного цикла и JS в целом. Только пользовательские HTML-элементы с атрибутами в разметке и стили.

Любой несуществующий в HTML элемент обрабатывается браузером как HTMLUnknownElement. Он отображается, попадает в DOM со всеми дочерними узлами, доступен через DOM API, стилизуется с помощью CSS.

Но HTMLUnknownElement не валиден с точки зрения стандарта. Валидатор покажет ошибку и проигнорирует неизвестный элемент и всё его поддерево. В свою очередь пользовательские элементы — это валидные экземпляры HTMLElement.

<!--
HTMLUnknownElement, не валиден
-->
<sidebarpanel>
<!-- ... -->
</sidebarpanel>

<!--
HTMLElement, валиден
-->
<sidebar-panel>
<!-- ... -->
</sidebar-panel>


Кроме того, у пользовательских элементов можно задавать произвольные атрибуты без префикса data-*, если они не пересекаются со стандартными глобальными атрибутами, например id, class, translate, lang и так далее.

У пользовательских элементов по умолчанию встроенная роль generic, как у <div> или <span>. Поэтому они сами по себе ничего не значат и не ломают семантику. Если нужно, можно указать глобальные aria-* атрибуты и role.

Всё это в совокупности можно использовать как альтернативу <div> и <span> для более осмысленного именования контейнеров, обёрток и компонентов, для которых нет подходящих семантических HTML-элементов.

<!--
Контейнер для ограничения
ширины контента и центровки
-->
<page-container width="lg">
<!-- ... -->
</page-container>

<!--
Контейнер для отображения
изображений в виде сетки
-->
<image-grid col="3" gap="sm">
<!-- ... -->
</image-grid>

<!--
Компонент бейджа
-->
<ui-badge variant="primary">
<!-- ... -->
</ui-badge>


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

Примеров можно придумать много. Кто-то делится своими идеями и практическими примерами. А кто-то даже свою CSS-методологию построил на основе CSS Web Components. Всё это в целом выглядит непривычно, но интересно.

- You can make up HTML tags
- Replace Divs With Custom Elements For Superior Markup
- Custom Element Examples (Without Javascript)
- Responsive Columns: Build Amazing Layouts With Custom HTML Tags
- TAC: A new CSS methodology
- 3 Examples of the TAC Methodology In Action

Таким образом CSS Web Components — это подход к вёрстке, когда <div> и <span> заменяются на незарегистрированные (без определения в JS) пользовательские элементы с осмысленными названиями, атрибутами и соответствующими стилями.

#html #css
👍6🔥61🤩1🌚1🙈1
Диалог, форма, список выбора и нарушения WCAG

При аудите столкнулся с компонентом переключения страны/валюты в магазине. Он использует кнопку раскрытия диалога, в котором находится форма со списком выбора (listbox), внутри которого опции в виде кнопок отправки формы:

<button
type="button"
aria-expanded="false"
aria-haspopup="dialog"
aria-controls="popover-id"
aria-label="Change country or currency"
>
United States (USD $)
</button>
<x-popover
role="dialog"
id="popover-id"
>
<form action="...">
<!-- скрытые поля -->
<x-listbox
role="listbox"
aria-activedescendant="option-us"
>
<!-- другие опции -->
<button
type="submit"
id="option-gb"
name="country_code"
value="GB"
role="option"
>
United Kingdom (GBP £)
</button>
<button
type="submit"
id="option-us"
name="country_code"
value="US"
role="option"
aria-selected="true"
>
United States (USD $)
</button>
<!-- другие опции -->
</x-listbox>
</form>
</x-popover>

Достаточно маленький компонент, а нарушает сразу несколько критериев WCAG:

- 2.1.1 Keyboard — в списке выбора не реализована соответствующая механика клавиатурного взаимодействия. Фокус должен попадать на listbox, опции переключаться стрелками/Home/End с фокусом через aria-activedescendant;

- 2.5.3 Label in Name — имя кнопки раскрытия диалога не совпадает с видимой подписью. Видимая подпись — «United States (USD $)», а программно заданное с помощью атрибута aria-label имя — «Change country or currency»;

- 3.2.2 On Input — выбор опции из списка приводит к отправке формы и смене контекста. Из-за реализации опций на основе кнопок с атрибутом type="submit" при нажатии происходит отправка формы с перезагрузкой страницы;

- 3.3.2 Labels or Instructions — нет видимой подписи у кнопки раскрытия диалога и поля выбора. В первом случае подпись задана через aria-label, а во втором случае не задана ни видимым элементом, ни программно;

- 4.1.2 Name, Role, Value — у поля выбора нет ассоциированного имени. У listbox должно быть имя, которое задано одним из способов. В данном случае имя не задано ни одним из способов.

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

<p id="label-id">
Change country or currency
</p>
<button
type="button"
id="button-id"
aria-expanded="false"
aria-haspopup="dialog"
aria-controls="popover-id"
aria-labelledby="label-id button-id"
>
United States (USD $)
</button>
<x-popover
role="dialog"
id="popover-id"
>
<form action="...">
<!-- скрытые поля -->
<ul>
<!-- другие кнопки -->
<li>
<button
type="submit"
name="country_code"
value="GB"
>
United Kingdom (GBP £)
</button>
</li>
<li>
<button
type="submit"
name="country_code"
value="US"
aria-current="true"
autofocus
>
United States (USD $)
</button>
</li>
<!-- другие кнопки -->
</ul>
</form>
</x-popover>


Убран listbox, кнопка раскрытия получила видимую подпись и составное имя с помощью aria-labelledby, добавлен обычный список, убрана семантика option и состояние aria-selected, вместо него теперь aria-current, добавлен autofocus для установки фокуса при открытии диалога.

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

#html #ui #a11y
8👍5🤩2🌚1
Крис Койер про область видимости в CSS

Посмотрел запись доклада Криса Койера про область видимости в CSS с прошедшей в июне 2025 года конференции CSS Day. Другие доклады доступны в плейлисте на YouTube-канале конференции. Теперь пару мыслей о докладе.

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

Далее затрагиваются инструменты для автоматического ограничения области видимости через генерацию уникальных классов. Один из таких — CSS Modules. Интересно то, что это набор правил, а не конкретная реализация.

Прозвучала мысль, что если к синтаксису языка добавляется что-то нестандартное, то не стоит использовать расширение .css и мимикрировать под обычный CSS. А CSS Modules добавляют нестандартные composes, :local и :global.

Обычно CSS Modules импортируются с помощью псевдо-импортов, которые не работают в JS и обрабатываются сборщиком. Раз так, то расширение файла могло быть, например, .mcss или .cssm, чтобы подчеркнуть отличие от CSS.

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

Далее упоминается подход CSS-in-JS, который помимо прочего обеспечивает область видимости через уникальные классы. По мнению Криса этот подход скорее CSS-in-React, поскольку тот не предлагает решения для стилей.

Это породило множество разных реализаций: styled-components, emotion, jss, и так далее. Они ничего особого не привнесли сообществу в глобальном смысле. Сегодня подход runtime CSS-in-JS пришёл в тупик и изжил себя.

Далее Tailwind, который обеспечивает область видимости иным способом. Не уникальными классами, а набором точечных классов, применяемых к конкретному элементу. Как и для Криса, это не для меня, но имеет место быть.

Отдельного говорится про хардкорные способы области видимости в веб-платформе: <iframe> (<object> сюда же) и Shadow DOM. Оба с нюансами и не очень удобные. В конце @scope — новый способ управления областью видимости.

В базовом виде @scope как-будто не имеет особого смысла. Того же эффекта можно добиться сейчас без него. Интересны более продвинутые техники, названные «Donut Scope», «DOM Blasters» и «Proximity». Это уже тема для отдельного поста.

#css
👍103🌚1😈1
«Donut Scope», «Proximity» и «DOM Blasters»

По горячим следам предыдущего поста про область видимости делаю продолжение. Речь пойдёт о директиве @scope в целом и подходах «Donut Scope», «Proximity» и «DOM Blasters» (названы так в докладе Криса Койера) в частности.

@scope — новая директива CSS, доступная на данный момент во всех актуальных версиях основных браузеров (получила статус Baseline Newly Available в декабре 2025). Директива предназначена для управления областью видимости.

В базовом виде @scope создаёт область видимости, за пределы которой селекторы не действуют. Можно писать более простые селекторы и использовать селекторы типа без риска что-то задеть:

@scope (.card) {
:scope {
...
}

.title {
...
}

button {
...
}
}


Стили .title и button применятся внутри .card и не повлияют на элементы вне .card. Селектор :scope позволяет стилизовать корневой элемент области видимости, в данном случае .card. В таком виде это не особо полезно и возможно без @scope:

/* Вложенность даёт тот же эффект */
.card {
...

.title {
...
}

button {
...
}
}


«Donut Scope» позволяет ограничить область видимости «сверху» и «снизу». Так можно создавать «дыры» внутри элементов, в которых селекторы не будут действовать. Полезно для ограничения глобальных стилей в сбросах.

<style>
@scope (body) to (.content) {
a {
text-decoration: none;
}
}
</style>
<nav>
<a href="...">
Ссылка в меню без подчёркивания
</a>
...
<nav>
<div class="content">
...
<a href="...">
Ссылка в тексте с подчёркиванием
</a>
...
</div>


«Proximity» — часть механизма каскада, которая вводит понятие близости элемента к области видимости. При прочих равных применяется тот селектор, который будет ближе к области видимости в DOM-дереве.

<style>
@scope (.light-theme) {
a {
color: blue;
}
}

@scope (.dark-theme) {
a {
color: lightblue;
}
}
</style>
<div class="dark-theme">
<a href="...">Ссылка</a>
<div class="light-theme">
<a href="...">Ссылка</a>
</div>
</div>


С обычной вложенностью ссылка внутри light-theme будет светло-синей, потому что применяется последнее по порядку правило. Со @scope ссылка внутри light-theme будет синей, потому что это правило ближе к области видимости light-theme.

«DOM Blasters» — так Крис Койер назвал применение @scope внутри <style> без указания селектора в скобках. Это режим, в котором ближайший родительский для <style> элемент используется как область видимости.

<div>
<a href="...">Ссылка</a>
</div>
<div>
<a href="...">Ссылка</a>
<style>
@scope {
a {
color: red;
}
}
</style>
</div>


Ссылка внутри первого <div> будет отображаться как обычно. Ссылка во втором <div> будет красной. Потому что второй <div> — ближайший родительский элемент для <style>, и в этом <style> используется @scope.

Крис в своём докладе предложил смелую идею: а что, если все стили компонентов писать таким образом, используя <style> и @scope? Пока непонятны преимущества и недостатки такого подхода в реальных условиях, но это идея уровня «что, если».

Крис провёл замеры на 10000 карточек со стилями в отдельном файле и в <style> для каждой карточки. По количеству памяти и времени отрисовки разницы не было. Увеличился размер HTML, но gzip хорошо справился.

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

#css
10🔥7😱31🌚1
Роли presentation, none, generic и aria-hidden

В ARIA есть несколько сущностей, которые могут вызвать путаницу:

- роль generic;
- роль none;
- роль presentation;
- состояние aria-hidden="true".

Роль generic обозначает общие элементы-контейнеры, которые сами по себе ничего не обозначают. В HTML эта роль встроена в такие элементы, как <div>, <span> и некоторые другие. Она не предназначена для использования в коде.

Разработчики не должны задавать атрибут role="generic" каким-либо элементам. Это внутренняя роль для браузера. Вспомогательные технологии не реагируют на generic элементы и работают с их контентом.

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

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

<ul role="menubar">
<li role="presentation">
<button
type="button"
role="menuitem"
>
...
</button>
</li>
<li role="presentation">
<button
type="button"
role="menuitem"
>
...
</button>
</li>
...
</ul>



В примере виджет menubar создан на основе <ul> и <li>. Семантика list переопределена на menubar. Семантика <li> остаётся listitem. Внутри menubar допустимы только элементы menuitem, поэтому listitem сбрасывается.

Нет смысла использовать presentation на элементах, которые по умолчанию не несут смысла: <div>, <span>, <section> без имени, <img alt="">, <a> без href, пользовательских элементах и других со встроенной ролью generic.

Для элементов с важной семантикой использование presentation сбрасывает эту семантику. <h1 role="presentation"> потеряет семантику heading и будет как <span>. Сбрасывать стандартную семантику не рекомендуется.

Роль none — это синоним presentation, то есть они работают одинаково. none была добавлена в ARIA 1.1, чтобы уменьшить путаницу. К тому же none короче по количеству символов и выглядит более понятно.

aria-hidden — это состояние, которое можно применять к любым элементам. Значение true исключает элемент и все его дочерние узлы из дерева доступности, как-будто их нет. При этом визуально все элементы сохраняются.

presentation и none удаляют семантику, элемент по-прежнему остаётся в дереве доступности как незначимый и семантика всех дочерних элементов сохраняется. aria-hidden="true" полностью исключает элемент из дерева доступности.

Итого:

- generic — незначимый контейнер, системная роль для браузера, нельзя использовать в коде;
- presentation и none — незначимый контейнер, можно использовать в коде, но, скорее всего, не пригодится;
- aria-hidden="true" — элемент и его поддерево скрыто от вспомогательных технологий, но отображается визуально.

#a11y
👍144🌚1👀1
Резервное значение font-family

Гарри Робертс написал статью о том, что резервное значение свойства font-family работает не так, как нам кажется. Поэтому при загрузке страницы на какое-то время текст может отображаться шрифтом Times New Roman, что ухудшает CLS.

Обычно базовый шрифт задаётся через :root, html или body для наследования вглубь дерева. Для заголовков часто шрифт переопределяется на акцентный из шрифтовой пары. Шрифт применяется к заголовкам с использованием селекторов типа или класса: h1, .h1, .heading.


:root {
font-family: Inter, system-ui, sans-serif;
}

h1, h2, h3, h4, h5, h6, .heading {
font-family: 'Playfair Display';
}


Все заголовки должны отображаться как Playfair Display. Это пользовательский шрифт, который должен загрузиться и обработаться браузером. Это происходит не мгновенно, при отрисовке страницы шрифт может быть ещё не готов.

В этом случае браузер будет использовать резервный шрифт, что зависит от свойства font-display. Чаще всего используется значение swap, при котором резервный шрифт заменяется пользовательским как только тот загрузится.

Кажется, что резервным шрифтом должен быть system-ui и sans-serif. Но это не так. Резервный шрифт берётся из font-family самого заголовка. Указан только Playfair Display, поэтому резервным будет системный шрифт для этого элемента.

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


:root {
--font-base: Inter, system-ui, sans-serif;
--font-headings: 'Playfair Display', serif;
font-family: var(--font-base);
}

h1, h2, h3, h4, h5, h6, .heading {
font-family: var(--font-headings);
}


#css #performance
🔥106💯3🌚1😎1
Нужен ли visually-hidden?

Шаблон visually-hidden хорошо знаком специалистам по доступности. Это класс со свойствами для визуального сокрытия элемента с сохранением его в дереве доступности. Во многих библиотеках есть компонент <VisuallyHidden>.

Этот шаблон часто применяется для добавления «скрытых» заголовков, подписей к полям и инструкций, которые нужны для улучшения доступности, но не предусмотрены дизайном. Дэвид Бушелл рассказывает историю этого шаблона.

Несмотря на распространённость и удобство, многие специалисты по доступности сходятся во мнении: это хак — следствие проблем при проектировании интерфейсов. Наличие в кодовой базе visually-hidden считается антипаттерном.

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

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

В идеальной ситуации те элементы, что скрыты с помощью visually-hidden, должны отображаться визуально и не должны быть скрыты. Они будут занимать место и загромождать интерфейс, но это уже вопрос удобства против эстетики.

#a11y
👍7🤔32🌚2👾1
Разница между aria-selected, aria-checked, aria-current и aria-pressed

В стандарте ARIA есть состояния, которые можно применять к элементам. Среди них есть несколько довольно похожих на первый взгляд:

- aria-selected;
- aria-checked;
- aria-current;
- aria-pressed.

aria-selected обозначает состояние выбора элемента внутри виджетов с одиночным или множественным выбором. Принимает значения:

- true — элемент выбран;
- false — элемент не выбран;
- undefined — элемент не выбираемый.

Применяется у следующих элементов:

- option в listbox;
- treeitem в tree;
- gridcell или row в grid;
- gridcell или row в treegrid;
- tab в tablist.

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

- true — элемент отмечен;
- false — элемент не отмечен;
- mixed — элемент в смешанном (indeterminate) состоянии и связан с группой других отмечаемых элементов;
- undefined — элемент не отмечаемый.

Применяется к следующим элементам:

- checkbox;
- radio (кроме mixed);
- switch (кроме mixed);
- option в listbox;
- menuitemcheckbox в menu или menubar;
- menuitemradio в menu или menubar (кроме mixed);
- treeitem в tree.

У option и treeitem можно использовать aria-selected и aria-checked. Подойдёт любой из них. Но рекомендуется для одиночного выбора использовать aria-selected, а в случае множественного выбора использовать aria-checked.

aria-current обозначает текущий элемент в контейнере или наборе связанных элементов. Может быть установлен на любой элемент, но принимает в качестве значения один из предустановленных токенов:

- page — элемент визуально выделен как текущая страница из набора страниц, подходит для активной ссылки в меню или хлебных крошках;
- step — элемент визуально выделен как текущий шаг в многошаговом процессе, подходит для визардов и многошаговых форм;
- location — элемент визуально выделен как текущее положение на странице, экране или процессе;
- date — элемент визуально выделен как текущая дата в виджете календаря или другом наборе дат;
- time — элемент визуально выделен как текущее время в таблице времени или другом наборе времени;
- true — элемент визуально выделен как текущий в наборе связанных элементов без дополнительного контекста;
- false — элемент не выделен визуально как текущий в наборе связанных элементов.

Назначение aria-current — программно передать визуальное выделение элемента. При этом только один элемент в наборе может быть отмечен как текущий и это нельзя использовать как альтернативу aria-selected и aria-checked.

aria-pressed обозначает состояние кнопки-переключателя и используется только на кнопках и элементах с ролью button. Требует реализации механизма зацикленной смены значений при нажатии. Принимает следующие значения:

- true — кнопка нажата;
- false — кнопка не нажата;
- mixed — кнопка зависит от смешанного состояния нескольких других кнопок;
- undefined — кнопка не работает как переключатель.

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

- aria-selected — состояние выбора для option, treeitem, gridcell, row и tab, рекомендуется для одиночного выбора в случае с option и treeitem;

- aria-checked — состояние отметки для checkbox, radio, switch, option, treeitem, menuitemradio и menuitemcheckbox, рекомендуется для множественного выбора в случае с option и treeitem, может быть в смешанном состоянии mixed у checkbox, option, treeitem и menuitemcheckbox;

- aria-current — визуально выделенный элемент в наборе связанных страниц, шагов, локаций, дат, времени или других элементов, один на весь набор;

- aria-pressed — состояние кнопки-переключателя, применимо только для кнопок, может быть в смешанном состоянии mixed.

#a11y
👍123🔥2🌚1👨‍💻1
Настоящий mobile-first

Mobile-first многим известен как подход к вёрстке, при котором сначала реализуется версия для мобильных и расширяется до десктопа. Это противоположность подходу desktop-first, при котором вёрстка идёт от десктопа к мобильным.

Разница обычно сводится к организации медиа-запросов и использованию min-width/max-width. Сначала применяются мобильные стили и переопределяются на более широких экранах, или десктопные переопределяются на более узких.


/* Mobile-first */
.product-grid {
--col: 1;
--gap: 12px;
display: grid;
grid-template-columns:
repeat(var(--col), minmax(0, 1fr))
;
gap: var(--gap);
}

@media (min-width: 600px) {
.product-grid {
--col: 2;
--gap: 18px;
}
}

@media (min-width: 900px) {
.product-grid {
--col: 3;
--gap: 24px;
}
}

@media (min-width: 1200px) {
.product-grid {
--col: 4;
}
}

/* Desktop-first */
.product-grid {
--col: 4;
--gap: 24px;
display: grid;
grid-template-columns:
repeat(var(--col), minmax(0, 1fr))
;
gap: var(--gap);
}

@media (max-width: 1200px) {
.product-grid {
--col: 3;
}
}

@media (max-width: 900px) {
.product-grid {
--col: 2;
--gap: 18px;
}
}

@media (max-width: 600px) {
.product-grid {
--col: 1;
--gap: 12px;
}
}


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

Однако сам термин mobile-first говорит о том, что подход ориентирован в первую очередь на мобильные устройства. Суть не в том, в какую сторону переопределяются стили, а в том, чтобы учесть ограничения мобильных устройств.

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

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

Точки Wi-Fi и мобильные сети менее стабильны, чем проводные соединения. Скорость приёма/передачи, потери пакетов, задержки и другие характеристики сети на мобильных устройствах, как правило, уступают десктопам.

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

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

Настоящий mobile-first — это разработка сайтов и веб-приложений с учётом всех особенностей и ограничений мобильных устройств. Он затрагивает архитектуру проекта и выбор инструментов разработки, а не только направление медиа-запросов.

В хорошем mobile-first:

- Элементы управления и контент компактно и логично размещены на ограниченном по размеру экране, интерфейс не перегружен;

- Интерактивные элементы достаточно большие для взаимодействия касаниями и расположены так, что с ними удобно работать;

- Ресурсы оптимизированы для минимального размера и быстрой загрузки;

- Видео, анимации, сложные фоновые вычисления и сетевые операции ограничены, если мало заряда батареи или включен энергосберегающий режим;

- Упрощённая версия без лишних ресурсов загружается, если включен режим экономии трафика;

- Контент кэшируется на устройстве и доступен оффлайн при потере соединения;

- Контент адаптируется к изменению ориентации экрана, режиму разделения экрана и другим возможностям устройства;

- Сайт использует аппаратные возможности устройства (камеру, микрофон, Bluetooth, акселерометр и другие), если это уместно и помогает пользователю;

#css #ux #performance
8👍3😈2🌚1