<divelopers> – Telegram
<divelopers>
1.03K subscribers
21 photos
253 links
Рандомные мысли про HTML, CSS, доступность, пользовательские интерфейсы, производительность, браузеры и веб-стандарты.

Автор: @alexnozer
Download Telegram
You don't know HTML: <time> и <data>

В HTML есть два редких элемента: <time> и <data>. Первый ещё можно встретить в разметке, где думают о семантике. Второй практически не встретить. Оба элемента объединяет возможность добавления машиночитаемых данных.

<time> предназначен для разметки дат, времени, временных зон и длительностей в формате ISO 8601. Это международный стандарт, который могут распознавать различные системы. Вот несколько примеров:

<!-- дата с годом -->
<time>2025-11-14</time>

<!-- месяц и день без года -->
<time>11-14</time>

<!-- время без секунд -->
<time>17:03</time>

<!-- дата и время с секундами -->
<time>2025-11-14T17:03</time>

<!-- временная зона -->
<time>+0300</time>

<!-- длительность -->
<time>2h 13m 42s</time>


Стандарт HTML говорит, что текст внутри <time> должен соответствовать формату (по сути ISO 8601). Если это не так, то дата/время/длительность в нужном формате должна быть указана в атрибуте datetime. Это позволяет парсить данные.

<!-- валидная строка по ISO 8601 -->
<time>2025-11-14 17:03</time>

<!-- невалидная строка по ISO 8601,
но валидная указана в атрибуте -->
<time datetime="2025-11-14 17:03">
Четверг, 14 ноября, 17:03
</time>


Атрибут позволяет сохранить машиночитаемые данные для поисковых роботов, ИИ, и других потребителей, а пользователям показывать человекочитаемые данные или относительные значения «вчера», «5 минут назад», «на прошлой неделе».

У <data> похожая задача: показать человекочитаемые данные и предоставить машиночитаемую альтернативу. Разница в том, что <data> не привязан ни к какому стандарту и формату, поэтому принимает произвольные данные.

Человекочитаемый контент указывается внутри элемента, а машиночитаемая альтернатива в атрибуте value.

<!-- даты допустимы, но лучше
использовать <time> -->
<p>
Дата публикации:
<data value="2025-11-14">
Четверг, 14 ноября 2025
</data>
</p>

<!-- идентификатор, модель, артикул,
SKU, или url handle товара -->
<h2>
<data value="GA-B419SWGL">
Холодильник LG GA-B419SQGL
</data>
</h2>

<!-- код валюты в ISO 4217
и неформатированная цена -->
<p>
Price:
<data value="USD">$</date>
<data value="119999">1.199,99</data>
</p>
<!-- Price: $1.199,99 -->


Есть ведь универсальные data-* атрибуты, которые можно использовать для хранения машиночитаемых данных у любых элементов? Это так, но они семантически нейтральные, в отличие от datetime у <time> и value у <data>.

Добавление data-* атрибута не значит, что это машиночитаемые данные, которые связаны с контентом. Это просто произвольные данные. <time datetime> и <data value> обладают конкретной семантикой.

Может показаться, что элементы помогут программам чтения с экрана правильно обработать данные. Предположение логичное, но ложное. <time> и <data> — элементы со встроенной ролью generic, поэтому работают как <span>.

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

А пока у <time> и <data> есть только теоретическая польза. Также они могут быть полезны для считывания и обработки в JS, как альтернатива data-* атрибутам. Для ИИ и парсеров контент тоже будет чуть более понятным.

#ydkhtml
👍6👌54👾4
Улучшение семантики карточки

Наткнулся в LinkedIn на пост с рецептом пропорционально масштабируемой карточки на основе единиц cqi и математики. Но меня зацепило не это, а разметка карточки:

<div class="card">
<div class="card-inner">
<img
class="card-img"
alt=""
src="..."
>
<div class="card-content">
<div class="card-noscript">
Добро пожаловать!
</div>
<div class="card-desc">
Это масштабируемая карточка.
</div>
<a
class="card-button"
href="..."
target="_blank"
>
Подписаться
</a>
</div>
</div>
</div>


<div> в <div>-е сидит и <div>-ом погоняет. Понятно, что разметка иллюстративная, а суть поста в CSS-технике. Иллюстративная ведь, да? Я не смог пройти мимо и решил сделать из этого пост. Итак, чиним семантику карточки.

Элемент .card может быть размечен как <article>, если карточка представляет независимый контент, который можно вырвать из контекста без потери смысла. Альтернатива — <div role="group">.

Возможен вариант с <section>, если карточка — большой тематический раздел. Если карточка находится в <li>, то <div> остаётся как обёртка компонента или сам <li> берёт на себя эту функцию.

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

С .card-img всё ок. Пустой alt уместен для декоративных изображений, иначе нужно описание. Но можно лучше: добавить адаптивность за счёт атрибутов srcset и sizes, хотя-бы с x-дескриптором.

Можно добавить width и height для предотвращения сдвигов макета. Если карточка находится за пределами начальной области просмотра, loading="lazy" отложит загрузку картинки, а fetchpriority и decoding дадут больше контроля.

Элемент .card-content так же выглядит лишним. Расположить изображение и остальной контент можно с помощью flex или grid у .card, без лишних обёрток. В таких мелочах раздувается размер DOM.

.card-noscript надо заменить на заголовок, noscript в названии класса намекает. Скорее всего, <h3>, карточка обычно находится в разделе с <h2>. Но возможен вариант с <h2>, всё зависит от структуры.

Судя по классу и короткому тексту без дополнительной разметки, элемент .card-desc принимает обычный текст. Тут больше подойдёт элемент <p>. <div> уместен для rich-контента из WYSIWYG.

У ссылки .card-button не хватает информации об открытии страницы в новой вкладке для удобства пользователей вспомогательных технологий. Дополнительная иконка со стрелкой обеспечит визуальную подсказку.

Картинку и заголовок можно поменять местами в DOM и инвертировать порядок через CSS. Чтобы при быстром переходе к заголовку в скринридерах картинка не затерялась. Это имеет смысл для контентных картинок с alt-текстом.

К <article> можно привязать заголовок с помощью aria-labelledby для дополнительного контекста. Можно попробовать aria-roledenoscription для уточнения роли элемента. Но такие моменты желательно тестировать.

<article
class="card"
aria-labelledby="heading"
>
<h3
class="card-noscript"
id="heading"
>
Добро пожаловать!
</h3>
<img
class="card-img"
alt=""
src="..."
srcset="... 1x, ... 2x"
sizes="..."
width="380"
height="380"
loading="lazy"
decoding="async"
>
<p class="card-desc">
Это масштабируемая карточка.
</p>
<a
class="card-button"
href="..."
target="_blank"
>
Подписаться
<noscript
role="img"
aria-label="(откроется в новой вкладке)"
>
<!-- ... -->
</noscript>
</a>
</article>


Лучше? Лучше! Конечно, важно учитывать контекст. Где-то <article> будет не уместен, у изображения должно быть описание, а заголовок должен быть другого уровня. Также это моя версия разметки, кто-то может не согласиться.

В <div> нет ничего плохого. Но когда почти всё — <div>, вот это плохо. Плохо для SEO, вспомогательных технологий, расширений, парсеров, ИИ, специальных браузеров и режимов отображения веб-контента.

(Отредактировано, чтобы влезло в один пост)

#html
17👍10👾4🌚1
Использование ARIA для ИИ

Месяц назад OpenAI запустила свой собственный браузер ChatGPT Atlas. Он создан на базе платформы Chromium со встроенным ChatGPT и агентом, который может ходить по сайтам и выполнять там задачи пользователей.

Это не какая-то выдающаяся новость в современном мире ИИ-хайпа. Подобные браузеры уже есть, а в существующих идёт активное внедрение ИИ. В конце статьи-анонса Atlas есть раздел What's next с одним спорным моментом:

Website owners can also add ARIA tags to improve how ChatGPT agent works for their websites in Atlas.


OpenAI предлагает разработчикам добавлять ARIA-теги для улучшения работы агента с сайтами. Опустим момент, что они употребили слово «теги» вместо «атрибуты», что может говорить о некомпетентности в вопросах ARIA.

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

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

При грамотной реализации глобально повысится уровень доступности сайтов. ARIA станет более популярным и появится новый тренд. Где тренд, там повышенная активность, в том числе авторов стандарта и специалистов по доступности.

С другой стороны, это может ухудшить состояние доступности. Разработчики, не знакомые с ARIA, пойдут внедрять «теги для ИИ» без понимания для чего это нужно и какое влияние оказывает. SEO-шник (AIO-шник) сказал, мы добавили.

Были уже времена alt-ов и пресных «SEO-текстов» со спамом «ключевиков» вместо реального описания изображений и полезного контента. С <meta name="keywords"> так наигрались, что поисковики перестали его учитывать.

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

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

Сейчас доля Atlas на рынке крайне мала и вряд-ли в ближайшее время станет сколько-то большой. Поэтому вряд ли будет ажиотаж вокруг внедрение ARIA для улучшения ИИ в одном специфичном браузере с малой базой пользователей.

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

Не хотелось бы превращать ARIA в инструмент для маркетологов, тем самым извратив изначальную суть и назначение этого стандарта. В HTML хватает других способов расширения, которые могут быть использованы для Atlas.

#a11y
😢93🔥2👍1🌚1👾1
Новый элемент <amount>

В прошлый понедельник я писал про элементы <time> и <data>, основная суть которых — связать человекочитаемый контент в произвольной форме с машиночитаемым эквивалентом в строгой форме. У этих элементов есть ограничения.

<time> предназначен для разметки даты, времени и длительности и не может использоваться для других данных. <data> не привязан к типу данных, но в атрибуте value может быть записано только значение без единиц измерения.

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


В Mozilla предлагают добавить новый элемент <amount> для разметки различных количественных значений в удобном для чтения представлении с учётом локализации. Дополнительно можно будет аннотировать данные.

Предлагается 3 атрибута:

- value — машиночитаемое значение, если контент не представлен в виде числового значения;
- unit — единица измерения в системе СИ, если значение представляет физическую величину;
- currency — код валюты по стандарту ISO 4217, если значение представляет цену.

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

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

<!-- 42.2 валидное число -->
<amount>42.2</amount>

<!--
42,2 невалидное число из-за запятой
вместо точки, как принято в немецком языке
Атрибут value содержит валидное значение
-->
<amount value="42.2" lang="de">42,2</amount>

<!-- km валидная единица измерения в СИ -->
<amount>42.2km</amount>

<!--
Из-за запятой значение не валидно
Атрибут value содержит валидное число
Атрибут unit содержит валидную единицу измерения
-->
<amount value="42.2" unit="km" lang="de">
42,2km
</amount>

<!--
Цена отформатирована в удобном виде с $ и запятой
Атрибут currency содержит международный код валюты
Атрибут value содержит валидное числовое значение
-->
<amount currency="USD" value="64000">
$64,000
</amount>


Автор ссылается на проведённое в 2000-х годах исследование о форматах измерений физических величин от сообщества микроформатов. Оно привело к предложению формата hmeasure, которое в 2010-х годах преобразовано в h-measure.

На сайте The Web We Want, где разработчики могут оставлять свои пожелания новых функций, а другие могут голосовать за них, есть соответствующее пожелание и связанная дискуссия в github-репозитории.

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

Также затрагивается тема доступности <time> и <data>, которые не оказывают эффекта на вспомогательные технологии, о чём я тоже упоминал. По всей видимости, <amount> должен лучше интегрироваться с вспомогательными технологиями.

В целом предложение логичное и полезное. В eCommerce, где много цен c валютами, размерных сеток, габаритов, объёмов и разных количественных значений, элемент <amount> был бы крайне полезен.

#html
👍123🎉3🌚2
Запись круглого стола «Будущее фронтенда: хватит ли веб-платформы?»

Доступна запись круглого стола «Будущее фронтенда: хватит ли веб-платформы?», в котором я участвовал на конференции Frontendconf 2025. Обсуждали, насколько веб-платформа готова предоставить базовые UI-примитивы.

Я выступал сторонником веб-платформы, говорил об инициативах Interop, OpenUI, CSS Form Control Styling, Global Design System и других. В рамках этих инициатив идёт работа по улучшению стандартных HTML-элементов и добавлению новых.

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

Идея кажется амбициозной и утопической, но я настроен оптимистично, рано или поздно мы к этому придём. Лёгкие UI Kit на основе стандартных элементов, API и веб-компонентов заменят специфичные UI-компоненты фреймворков.

Речь не о полной замене. Фреймворки останутся и будут управлять данными, отображением и логикой интерфейса. Заменится слой примитивов пользовательского интерфейса, из которых всё строится: выпадающие списки, вкладки, диалоги, карусели и так далее.

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

#ui
8👍1👾1
Доступны ли фильтры из радио-кнопок?

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

Теги реализованы как радио-кнопки, потому что логика подразумевает один активный тег. Если бы активных тегов могло быть несколько, вместо радио-кнопок были бы флажки. Разметка для описанного интерфейса выглядела бы так:

<form>
<fieldset>
<legend>
Теги
</legend>
<label for="tag-1">
<input
type="radio"
name="tag"
value="tag-1"
>
Тег 1
</label>
<label for="tag-2">
<input
type="radio"
name="tag"
value="tag-2"
>
Тег 2
</label>
<!-- остальные теги -->
</fieldset>
</form>
<ul>
<!-- список статей -->
</ul>


Изменение списка происходит динамически при выборе тега с помощью JS. Это довольно распространённый подход. Возникает вопрос: доступен ли такой интерфейс и поведение? А именно изменение списка при выборе радио-кнопки тега.

Критерий 3.2.2 On Input гласит: изменение настроек любого компонента пользовательского интерфейса не должно автоматически приводить к изменению контекста, если пользователь заранее не предупреждён об этом.

Радио-кнопка по определению считается компонентом пользовательского интерфейса. Настройки — это различные свойства и состояния, которые меняются при взаимодействии. У радио-кнопки это состояние «отмечено»/«не отмечено».

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

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

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

Как это исправить? WCAG предлагает два способа:

- Добавить кнопку отправки формы;
- Добавить предупреждение перед компонентом.

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

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

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

<fieldset>
<legend>
Теги
</legend>
<button
type="submit"
name="tag"
value="tag-1"
>
Тег 1
</button>
<button
type="submit"
name="tag"
value="tag-2"
>
Тег 2
</button>
<!-- остальные теги -->
</fieldset>


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

<fieldset>
<legend>
Теги
</legend>
<p>
При выборе тега список статей
динамически изменяется.
</p>
<!-- теги -->
</fieldset>



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


Всё это релевантно и для реализаций с группой кнопок-переключателей (с состоянием «нажата»/«не нажата») вместо радио-кнопок или флажков. Помимо фильтра тегов статей. проблема актуальна для товарных фильтров в интернет-магазинах.

#a11y
14👍3👾3
Переосмысление контейнера v2

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

Я предложил решение, которое основано на динамическом вычислении полей на основе функции calc() и единиц vw. Решение рабочее, но не очень интуитивное из-за формулы и расчёта --vw для решения проблемы с полосой прокрутки.

Для понимания цели рекомендую ознакомиться с соответствующими постами. А я поделюсь альтернативным решением, которое мне пришло в голову. Оно даёт то же результат, но реализовано с помощью grid и более понятное на мой взгляд.

<style>
.container {
--container-max-width: 1440px;
--container-min-field: 16px;
display: grid;
grid-template-columns:
minmax(var(--container-min-field), 1fr)
minmax(0, var(--container-max-width))
minmax(var(--container-min-field), 1fr)
;
}

.container > * {
grid-column: 2 / 3;
}

.section {
background-color: var(--section-bg-color);
}
</style>
<section class="section container">
<!-- контент -->
</section>


В контейнере создаётся сетка из трёх колонок. Центральная колонка предназначена для контента. Её максимальная ширина ограничена значением 1440px в функции minmax(). Эта колонка будет растягиваться и сжиматься в зависимости от ширины экрана.

Крайние колонки выполняют роль полей. Их минимальная ширина составляет 16px. Когда центральная колонка достигнет максимальной ширины, крайние колонки будут равномерно растягиваться на доступное пространство.

Правило с селектором .container > * нужно, чтобы любые дочерние элементы контейнера попадали в центральную колонку для контента. Потому что по умолчанию элементы последовательно распределяются по доступным колонкам.

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

#css
👍6🤔31
CSS Wrapped 2025

Как и в прошлом году, команда Chrome DevRel презентовала лэндинг CSS Wrapped 2025. На нём собрано 17 новых функций CSS и UI, которые внедрены в течении 2025 года. Функции разделены на три группы и снабжены примерами.

Кастомизируемые компоненты

Invoker Commands. Декларативный способ управления отображением <dialog> и popover без JS. Также можно создавать собственные команды и вызывать событие command.

Атрибут closedby. Управляет закрытием <dialog>: только через JS или по нажатию Esc, жесту «назад» и аналогичным действиям или всё перечисленное плюс нажатие вне элемента.

Эвфемерные поповеры. Значение popover="hint" создаёт новый тип поповеров. Их можно вкладывать внутрь других поповеров и их появление не будет закрывать ранее открытые поповеры.

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

CSS-карусели. Встроенный в CSS способ создавать карусели с кнопками «вперёд»/«назад» и маркерами. Есть ряд проблем, но они решаются.

Якорные контейнерные запросы. Стилизация элементов на основе положения связанного якоря из Anchor Positioning API.

Interest Invokers. Развитие Invoker Commands с вызовом команд по наведению указателя, фокусу и долгому тапу на тач-экранах.

Интерактивность нового поколения

Запросы состояния прокрутки. Контейнерные запросы для проверки наличия прокрутки, привязки элемента к точкам привязки или sticky-элемента.

Функции подсчёта элементов в дереве. Функция sibling-count() возвращает количество дочерних элементов. sibling-index() возвращает порядковый номер элемента внутри родителя.

scrollIntoView() для ближайшего контейнера. Дополнительный параметр container: 'nearest' для метода scrollIntoView() избавляет от некоторых багов с прокруткой.

Вложенные группы View Transition. Возможность вкладывать псевдо-элементы ::view-transition-group друг в друга для эмуляции 3D-переходов.

Перемещение элементов с сохранением состояния. Метод moveBefore() перемещает элемент по DOM-дереву с сохранением состояния. Полезно для <video> и <iframe>.

Улучшенная эргономика

Расширение attr(). Функция attr() расширена для работы с типами данных, чтения любых атрибутов и установки резервного значения.

ToggleEvent.source. Событию ToggleEvent добавлено свойство source с элементом, который стал источником вызова события.

Обрезка текста. Свойства text-box-trim, text-box-edge и сокращённое text-box позволяют обрезать лишнее пространство текстовых узлов для лучшего выравнивания.

Функция shape(). Описывает в похожем на SVG синтаксисе сложные фигуры, которые можно использовать как маски.

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

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

Расширенный синтаксис диапазонов. if() и style() только появились, а им уже добавили возможность проверки значений с помощью операторов <><= и >=.

Ключевое слово stretch. Новое значение свойств width и height. Оно позволяет элементу растягиваться на размеры содержащего блога, чего сейчас не добиться значениями auto, 100% и 100v*.

Свойство corner-shape. Предназначено для создания углов сложной формы, включая внутренние углы и вырезы в духе negative border radius.

Более подробно о каждой функции смотрите на лендинге: там есть демонстрации, примеры кода, видео, браузерная поддержка, статус Baseline и ссылки на дополнительные материалы. Ещё один крутой год для CSS и UI.

#html #css #ui
6👍3👾1
Custom Media Queries

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

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

:root {
--media-desktop: (width >= 750px);
--media-mobile: (width < 750px);
--media-reduced-motion: (prefers-reduced-motion: reduce);
}

/* Код ниже не сработает */
@media var(--media-desktop) {
/* Стили для десктопов */
}

@media var(--media-mobile) {
/* Стили для мобильных */
}

@media var(--media-reduced-motion) {
/* Стили для режима уменьшенного движения */
}


К сожалению, это не работает. В препроцессорах значение медиа-запроса можно поместить в переменную или сделать отдельный миксин для удобного использования. В спецификации Media Queries Level 5 подумали об этом и предложили директиву @custom-media.

С помощью этой директивы можно реализовать логику как в примере выше. Директива объявляет переменную, которая будет доступна в качестве значения медиа-запроса. Как это принято, имя переменной пишется в синтаксисе dashed-ident, с двумя дефисами вначале.

@custom-media --desktop (width >= 750px);
@custom-media --mobile (width < 750px);
@custom-media --reduced-motion: (prefers-reduced-motion: reduce);

@media (--desktop) {
/* Стили для десктопов */
}

@media (--mobile) {
/* Стили для мобильных */
}

@media (--reduced-motion) {
/* Стили для режима уменьшенного движения */
}


В @custom-media можно объявлять одиночные значения, перечислять несколько значений через запятую, использовать операторы and, or и not. Также доступен синтаксис диапазонов и такие запросы, как prefers-*, noscript, hover и другие.

Проблема в том, что ни один браузер не поддерживает этот синтаксис. Использовать его можно только с постпроцессорами. Для PostCSS есть плагин, а Lightning CSS транспилирует из коробки, если включить параметр drafts.customMedia: true.

На днях вышел Firefox 146. В конце релиз-ноутов есть раздел Experimental web features, где я увидел следующее:

Custom media queries: layout.css.custom-media.enabled

The @custom-media CSS at-rule defines aliases for long or complex media queries. Instead of repeating the same hardcoded <media-query-list> in multiple @media at-rules, it can be defined once in a @custom-media at-rule and referenced throughout the stylesheet whenever needed. (Firefox bug 1744292).


Это хорошая новость, потому что я жду эту фичу и наконец-то она сдвинулась с места. Конечно, есть шанс, что её удалят после экспериментов. Но хочется верить, что внедрение наоборот подтолкнёт остальные браузеры и фича попадёт в Interop 2026.

Под релиз этой экспериментальной функции на MDN написали статью с подробным описанием синтаксиса @custom-media и примерами кода. Рекомендую ознакомиться для лучшего понимания всех возможностей.

#css
13🔥3👾1
API пользовательских атрибутов

Пользовательские элементы обычно наследуются от интерфейса HTMLElement, который представляет общий HTML-элемент. Есть возможность наследования от других, более конкретных элементов, используя несколько многословный синтаксис и атрибут is.

<noscript>
class ToggleButton extends HTMLButtonElement {
connectedCallback() {
// Логика
}
}

customElements.define(
'toggle-button',
ToggleButton,
{
extends: 'button'
}
);
</noscript>

<button
type="button"
is="toggle-button"
data-target="content"
>
Подробнее
</button>
<div id="content" hidden>
<!-- Скрытый контент -->
</div>


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

Этот API называется «Customized built-in elements» и давно поддерживается во всех браузерах, кроме Safari. Браузерные инженеры из Apple принципиально отказались реализовывать этот API, сославшись на целый ряд причин.

Если углубиться в детали, то причины вполне адекватные. Из-за отказа Apple от внедрения, Customized built-in elements практически не используются. Есть несколько альтернативных идей, которые учитывают обратную связь от Apple.

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


Одна из таких идей, Custom Attributes, обсуждалась на недавнем TPAC 2025. Есть несколько разных предложений, но основная суть в том, чтобы добавить API для создания пользовательских атрибутов по аналогии с пользовательскими элементами.

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

<!--
Это гипотетический код нового API
Он не работает ни в одном из браузеров
-->
<noscript>
class ToggleElementAttr extends Attr {
// Встроенный тип данных
static dataType = AttributeType.IDREF;
// Опциональное значение по умолчанию
static defaultValue = null;

// Элемент, у которого установлен атрибут
ownerElement;
// Текущее значение атрибута
value;

// Атрибут подключен к элементу
connectedCallback() {
// Логика
}

// Атрибут удалён
disconnectedCallback() {}

// Атрибут изменился
changedCallback(oldValue, newValue) {}
}

HTMLButtonElement.attributeRegistry.define(
'toggle-element',
ToggleElementAttr
);
</noscript>

<button
type="button"
toggle-element="content"
>
Подробнее
</button>
<div id="content" hidden>
<!-- Скрытый контент -->
</div>


Это предложение также решает проблему HTML-first библиотек, таких как HTMX, Alpine.js, UIKit и других, которые используют нестандартные атрибуты для декларативного добавления логики и анализируют их с помощью регулярных выражений или XPath.

Custom Attributes API позволит убрать лишний код, который отвечает за поиск и разбор атрибутов и их значений. Работа с произвольными атрибутами должна стать удобнее и проще. Также пользовательские атрибуты будут считаться валидными.

<noscript>
// Где-то в коде htmx.js может быть так:

class HxPost extends Attr {
// ...
}

class HxSwap extends Attr {
// ...
}

const buttonAttrMap = {
'hx-post': HxPost,
'hx-swap': HxSwap,
// ...
}

for (const name in buttonAttrMap) {
HTMLButtonElement.attributeRegistry.define(
name,
buttonAttrMap[name]
);
}
</noscript>

<button hx-post="/clicked" hx-swap="outerHTML">
Нажми меня
</button>


#html #js #web_api
123🔥1👾1
Разделение шрифтов на подмножества

Подобно тому, как большие JS-файлы разбиваются на несколько мелких и загружаются по необходимости, файлы шрифтов так же могут быть разделены на более мелкие подмножества. Эта практика называется «font subsetting» или «разделение шрифта на подмножества»

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

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

Латинские символы практически не будут использоваться, не говоря уже про символы из группы CJK (Chinese, Japanese, Korean) и других языков. Проблема та же, что с неиспользуемым JS-кодом, которая решается разделением кода и three-shaking. Аналогично можно поступить со шрифтами.

Один большой шрифт с множеством символов можно разделить на несколько:

- Шрифт с базовой кириллицей — основные буквы кириллицы, цифры, основные печатаемые знаки препинания и символы, можно ещё добавить типографические кавычки, длинные тире, многоточие и так далее;
- Шрифт с расширенной кириллицей — специфические буквы и символы (диакритические знаки) для Украинского, Белорусского, Болгарского, Сербского и других языков;
- Шрифт базовой латиницы — основные буквы латинского алфавита;
- Шрифт расширенной латиница — специфические буквы и символы (диакритические знаки) для Немецкого, Польского, Литовского, Турецкого и других языков;
- Шрифты других подмножеств при необходимости.

Латиница на русскоязычном сайте пригодится для терминов, названий и переводчиков. Я посещаю англоязычные сайты и иногда использую функцию перевода страницы. Точно так же делают иностранцы. Не очень красиво, если вместо дизайнерского шрифта загрузится Times New Roman.

Существует несколько способов разделить шрифт. Самый простой — использовать Google Fonts. Не в том смысле, чтобы подключать через него шрифты, я так делать не рекомендую. А использовать его для получения готовых подмножеств:

1. находите шрифт на Google Fonts;
2. копируете сгенерированную ссылку из <link rel="stylesheet">;
3. открываете ссылку в новой вкладке, там будет набор директив @font-face;
4. копируете нужные подмножества (помечены комментариями)
5. скачиваете соответствующие файлы шрифтов из src;
6. добавляете @font-face в проект и подставляете ссылки к скачанным файлам.

Другой способ — использовать Font subsetting. Загружаете шрифт, указываете символы или Unicode-последовательности и получаете шрифт, состоящий только из указанных символов. Третий способ — воспользоваться CLI-инструментом glyphhanger, подробнее в статье Зака Лезермана.

Чтобы браузер понял, когда и какое подмножество шрифта использовать, ему нужно знать, какие символы есть в шрифте. Эта информация задаётся свойством unicode-range внутри директивы @font-face:

/* cyrillic */
@font-face {
/* ... */
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}


Cвойство unicode-range содержит список символов в формате Unicode. Можно указать конкретный символ (U+2116 — знак номера «№») или диапазон (U+0410-044F — от буквы «А» до буквы «я»). Список значений Unicode можно посмотреть в таблице.

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

#css #performance
🔥74👾1
Про шрифты

Дополню предыдущий пост про разделение шрифтов на подмножества записями двух докладов. Первый от Вадима Макеева с интригующим и загадочным названием «_ ___ ______?» Из этого доклада я узнал про «сабсеттинг» и свойство unicode-range.

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

Второй доклад «Давай поиграем со шрифтами» от Никиты Дубко прям свежий, с прошедшей в октябре конференции Frontendconf 2025. Я также выступал на этой конференции, поэтому на докладе присутствовал лично.

Никита рассказал про устройство и метрики шрифтов, отрисовку в браузере, новые CSS-единицы, привязанные к метрикам шрифта, стратегии загрузки, новые свойства text-box-* и другие интересные моменты, например, в шрифтах можно программировать.

#css
5🔥1👾1