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

Автор: @alexnozer
Download Telegram
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
Страница на Claude Code и Figma MCP

Недавно посмотрел страницу «Coming Soon» одного проекта, которая создана с помощью Claude Code в сочетании с Figma MCP по отрисованному макету. Её создал человек без опыта разработки за 30 минут. На доработку ему понадобилось 3 промпта.

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

Я бы не писал об этом, если бы вновь не пришлось узреть мощь современных технологий. Страница создана на Next и Tailwind. Дело не в самих технологиях, а в том, что их использование не оправдано для такой простой страницы.

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

Интересно то, что Pagespeed выдаёт 100 баллов по производительности и 97 по доступности. То есть всё в порядке, цифры зелёные не смотря на Next. Тут важна трактовка результатов. Было бы странно видеть на такой странице не 100.

Достаточно добавить разделы с формой, каруселью, раскрываемыми FAQ, шапку с выпадающим меню, эффекты и аналитику, и эти 100 баллов стремительно полетят вниз. Весь бюджет JS съеден обвязкой Next уже на самом старте.

На страницу загружается 8 JS-файлов общим размером 160кб (~540кб без сжатия) просто потому что так работает Next. Вся интерактивность на странице — отправка email с формы в сторонний сервис. Для этого не нужен Next.

При загрузке страницы виден прыжок шрифта. Подключается вариативный шрифт размером 1.9мб (4.9мб без сжатия) для отрисовки заголовка и блока текста с одним предложением. Обший размер всех ресурсов — 2.5мб (6мб без сжатия).

Для создания этой простейшей страницы развёрнута целая машинерия. Банальная замена текста — процесс с запросом к Claude, который ждёт сборку и загрузку на Vercel. А ведь можно было просто открыть файл и поправить текст.

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

Эта страница должна быть одним HTML-файлом со встроенными <style> и <script>. Без Next, Tailwind, node_modules, Turbopack, 8 чанков JS, вариативного шрифта и так далее. Для отправки формы простой скрипт на чистом JS.

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

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

#performance
🔥167🤡4👍3🌚1😎1
Оптимизация размера DOM бесполезна?

Я писал про уменьшение размера DOM. На днях смотрел подкаст «Организованное Программирование» про SEO, там гость заявил, что считает совет Lighthouse об оптимизации размера DOM вредным и бесполезным. Так ли это на самом деле?

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

Но есть парадокс под названием сжатие. 72% текстовых файлов передаются по сети в сжатом виде. Если вы входите в 28%, то настройте хотя-бы Gzip, а лучше Brotli или Zstd. Сервер сжимает файл при отправке, браузер разжимает при получении.

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

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

Помимо передачи по сети HTML служит основой для построения DOM. HTML-парсер проходится по разметке и создаёт для каждого HTML-элемента, атрибута, строки текста и комментария объект с множеством свойств и связей.

Эти объекты нужны для того, чтобы ими манипулировать через DOM API, что под капотом делают любые JS-фреймворки. Также нужно построить CSSOM — для каждого узла вычислить все CSS-свойства, чтобы знать размеры и положение.

CSSOM нужен для итоговых процессов отрисовки страницы: Render tree, Layout и Paint. Это чтобы первично отрисовать страницу. Далее она прокручивается, меню выпадает, слайды переключаются, кнопки меняют цвет при наведении и так далее.

Всё это приводит к повторным пересчётам. Многие взаимодействия со страницей вызывают пересчёт. Браузеры выполняют огромное количество операций в секунду. За годы существования все эти процессы отлично оптимизированы.

Чтобы страница ощущалась быстрой и плавной, все операции по пересчёту стилей и отрисовке должны укладываться в один кадр отрисовки, то есть ~16мс для экрана с частотой обновления 60 Гц. Желательно не впритык, а с некоторым запасом.

Чем больше DOM-узлов, тем больше всего браузеру нужно рассчитать и подготовить к отрисовке. Осложняют дело «тяжёлые» новинки, такие как селектор :has(), контейнерные и стилевые запросы, @scope, каскадные слои.

JS-отрисовка добавляет дополнительные накладные расходы. Размеры бандлов растут из-за шаблонов в коде, потребление памяти и процессора растёт, потому что код шаблона нужно выполнить, а с VDOM ещё держать в памяти деревья.

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

Столкнувшись с «фризами» пользователи уйдут к конкурентам, у которых такого нет. Поисковики эти сигналы считывают и запоминают, ухудшаются поведенческие факторы и сайт теряет позиции в выдаче поиска. Бизнес теряет пользователей.

Это может показаться притянутым за уши. Но проблема точно реальная и с ней борются. Глянуть сайт на предмет просадки кадров точно стоит (в DevTools меню с тремя точками → More tools → Rendering → флажок Frame Rendering States).

Во вкладке Performance можно обратить внимание, сколько времени занимает Parse HTML, Rendering и Painting. Во вкладке Memory можно сделать снэпшот и увидеть, сколько оперативной памяти выделено на разные объекты, в том числе узлы DOM.

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

Современный CSS с flex и grid, а также немного смекалки позволяют достигать результата с меньшим количеством разметки. Убрали лишние узлы, сжали картинки, взяли встроенное API вместо JS и уже немало сэкономили.

#html #performance
🔥11👍82🌚1🤝1
Стилизуемый combobox и фильтруемый select

Браузеры совместно с OpenUI продолжают развивать <select>, добавляя новые функции и возможности стилизации. В Сhrome уже можно стилизовать стандартный <select> с одиночным выбором, на подходе вариант с множественным выбором и combobox.

На конференции BlinkOn 21 контрибьютор движка Chromium поделился прогрессом в этом направлении. Я уже писал о том, как работают атрибуты size и multiple у стандартного <select> и планах по расширению их функциональности.

<select multiple size="1">
<button>
<selectedcontent>
<div>One</div>
<div>Two</div>
</selectedcontent>
<span>0 Selected</span>
</button>
<option selected>One</option>
<option selected>Two</option>
<!-- ... -->
</select>
<style>
selectedcontent:not(:empty) + span {
display: none;
}
</style>


Первое — обёртки <div> для проецируемых опций в <selectedcontent>. Этот элемент был внедрён как часть стилизуемого <select> для проецирования значения выбранной опции. Теперь проецируются несколько опций и они оборачиваются в <div> для стилизации.

Для состояния, когда не выбрана ни одна опция, а значит <selectedcontent> пуст, предлагается использовать резервный элемент <span> с текстом. Он скрывается через комбинацию псевдо-классов :not(), :empty и комбинатор +.

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

<button
commandfor="dialog"
command="showpopover"
>
<selectedcontent id="sc">
</selectedcontent>
</button>
<dialog popover id="dialog">
<input filter="select">
<select
size="4"
id="select"
selectedcontentelement="sc"
>
<!-- опции -->
</select>
<!-- можно добавить другие элементы -->
</dialog>


Виджет собран из нескольких встроенных API. Кнопка с атрибутами command/commandfor управляет отображением диалога, который реализован с помощью <dialog popover>. Внутри <select>, который связан с <selectedcontent> для проекции выбранной опции.

<input> связан с <select> через новый атрибут filter, который позволяет фильтровать опции. Предложенный вариант многословен, но хорош тем, что в <dialog> можно поместить другие элементы (текст, кнопки, иконки) помимо <input> и <select>.

Прорабатывается вариант для простых случаев, когда есть только опции и поле для их фильтрации, без дополнительных элементов. В таком случае предлагается вкладывать <input> внутрь <select> перед опциями. Для этого будет доработан парсер.

<select>
<input>
<!-- опции -->
</select>


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

Показана возможность стилизации уже существующей в браузерах связки <input> и <datalist>. Как и в случае со стилизуемым <select>, API базируется на идее базового внешнего вида и стандартных стилей, задаваемых свойством appearance: base;.

<input list="options">
<datalist id="options">
<!-- опции -->
</datalist>
<style>
input[list], datalist {
appearance: base;
}
</style>


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

#html #css #ui
👍61🌚1🤗1
You don't know HTML: <fieldset>

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

Подпись задаётся с помощью элемента <legend>. Если он есть, то должен быть первым прямым потоком <fieldset>. Иное размещение или оборачивание в контейнер не валидно и подпись не будет привязана к <fieldset>.

У элемента <fieldset> встроенная роль group. Наличие валидного <legend> задаёт имя для группы. Воспроизвести это можно с помощью ARIA, но зачем, если в платформе есть встроенный элемент с соответствующей семантикой:

<div 
role="group"
aria-labelledby="legend"
>
<div id="legend">Размер</div>
<!-- переключатели -->
</div>

<!-- встроенный аналог -->
<fieldset>
<legend>Размер</legend>
<!-- переключатели -->
</fieldset>


Помимо встроенной роли, семантики и привязки имени у <fieldset> есть API. Атрибут disabled отключает вложенные элементы формы, поэтому не нужно вручную каждому проставлять disabled. Полезно для отключения части формы.

Атрибут name задаёт идентификатор группы для доступа к ней через Form API. Это удобно использовать в сочетании со свойством elements, которое хранит все элементы формы или группы. Через него можно получить конкретный элемент.

<form name="variants">
<fieldset name="sizes">
<legend>Размер</legend>
<!-- переключатели -->
</fieldset>
<!-- ... -->
</form>
<script>
const variantsForm = document.forms['variants'];
// или document.forms.variants
// или document.forms.namedItem('variants')
const sizeGroup = variantsForm.elements['sizes'];
// или variantsForm.elements.sizes
  // или variantsForm.elements.namedItem('sizes')

// отключение группы
sizeGroup.disabled = true;
// элементы группы
const elements = sizeGroup.elements;
</script>


У <fieldset> можно вызывать методы Constraint Validation API и проверить на валидность только поля из группы, а не всей формы целиком. Также применимы псевдо-классы :valid/:invalid, :user-valid/:user-invalid.

Атрибут form позволяет привязать группу со всеми элементами к указанной форме. Это позволяет вынести группу за пределы формы, сохранив с ней связь. Сбор данных для отправки, валидация и методы работы с элементами будут доступны.

У <fieldset> есть некоторые особенности стилизации. По умолчанию у него блочный контекст форматирования, ширина подстраивается под контент, есть рамка и <legend> размещается поверх рамки. Но элемент полностью стилизуемый.

Таким образом <fieldset> в сочетании с <legend> полезен для группировки полей, передаёт вспомогательным технологиям нужную семантику и имя, обладает API для управления элементами (отключение, вынос из формы, получение, валидация).

#ydkhtml
👍11🌚1🤝1
sizes="auto" для изображений

Для адаптивных изображений у элемента <img> есть два атрибута. srcset задаёт набор изображений с физической шириной или плотностью пикселей. sizes задаёт доступное пространство, которое браузер заполняет подходящим изображением.

В Firefox 150 и Chrome 124 добавили новое значение sizes="auto". Однако не стоит думать, что теперь указывать размеры в sizes не нужно. sizes="auto" работает в паре с loading="lazy". Без него указывать auto нет смысла, работать не будет.

Зачем указывать sizes, разве браузер не знает размеры области для отрисовки изображения? Дело в том, что не знает до загрузки и обработки всех стилей. Сканер предварительной загрузки анализирует HTML и ищет ресурсы.

Обнаружив изображение, браузер поместит его в очередь загрузки. В моменте нужно решить, какое из изображений ставить в очередь. К этому времени стили могут быть ещё не загружены или не обработаны. Поэтому браузер смотрит значение sizes.

С лениво-загружаемыми изображениями ситуация другая. Их загрузка отложена до тех пор, пока изображение не появится в области просмотра. Чтобы это выяснить, браузеру нужно обработать стили и знать размеры и положение на странице.

К моменту загрузки ленивого изображения, даже если оно находится на первом экране, браузер уже обработал стили и знает все размеры. Именно поэтому sizes="auto" будет работать, браузер возьмёт известные ему размеры области.

Таким образом изображениям с ленивой загрузкой loading="lazy" можно задать sizes="auto". Остальным всё ещё нужно указывать размеры. Также остаются браузеры, которые не понимают значение auto, для них тоже нужны размеры.

Синтаксисом предусмотрено совмещение значения auto с размерами. Если браузер не понимает auto или не может получить размеры из стилей, он откатится к размеру, который указан после auto через запятую: sizes="auto, (min-width: 600px) 50vw, 90vw".

Мэт Маркиз, бывший председатель группы адаптивных изображений (Responsive Image Community Group) написал статью «The end of responsive images», в которой рассказал более чем 10-летнюю историю появления sizes="auto".

#html #css
👍4🔥3🌚1🤝1
ARIA APG: доверяй, но проверяй

Есть ресурс под названием ARIA Authoring Practices Guide, сокращённо APG. На него ссылаются как на источник доступных шаблонов элементов интерфейса. Это неплохой ресурс, но при обращении к нему стоит помнить о некоторых нюансах.

Ресурс выглядит официально, содержит логотип W3C и WAI, размещён на домене w3.org, упоминается на сайте инициативы по веб доступности. Внушает доверие и вызвает ложное ощущение официального стандарта от W3C.

На деле APG — не стандарт W3C, в отличие от ARIA, WCAG, ARIA in HTML и других. APG создан специальной группой APG Task Force из сообщества WAI, которое, в свою очередь, одно из многих сообществ в составе консорциума W3C.

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

Если взглянуть на шаблон кнопки, то APG предлагает использовать <div>, <a> и <span> с ролью button, tabindex="0" для фокуса и JS для обработки нажатия. APG свою задачу выполнил: показал, как использовать роль button.

Тем временем первое правило ARIA гласит, что не стоит использовать ARIA, если есть встроенный элемент. В HTML кнопка есть — <button>. На практике <div> с ролью кнопки, tabindex и обработчиком нажатий считается антипаттерном.

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

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

Всё сказанное выше не значит, что на APG не стоит смотреть. Это неплохой источник информации о проектировании доступных интерфейсов. Важно не воспринимать его как официальный источник истины со 100% правильными шаблонами.

Эрик Бэйли считает хорошими частями в APG:

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

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

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

#ui #a11y
1👍54🔥31🌚1😎1
Переусложнённые радио-кнопки в Shadcn

Пол Геберт в блоге написал о чрезмерно переусложнённых радио-кнопках в Shadcn. Это библиотека готовых компонентов для React, которая построена на базе Radix UI, использует Tailwind и работает по принципу «copy-paste» кода в проект.

Предположим, что в проекте нужны радио-кнопки. В HTML это решается добавлением <input type="radio">. В Shadcn для этого нужно 3 импорта, 45 строк кода, сторонняя библиотека иконок для кружка, 30 классов Tailwind.

Если залезать под капот импортируемого RadioGroupPrimitive, там ещё 215 строк кода и 7 импортов. Неужели для радио-кнопок нужно столько кода? Но это ладно. Какой результат получается в браузере при отрисовке этого всего?

<button
id="debit"
class="классы Tailwind"
type="button"
role="radio"
aria-checked="true"
data-state="checked"
value="debit"
tabindex="0"
data-radix-collection-item
>
<span
class="классы Tailwind"
data-state="checked"
>
<svg
class="классы Tailwind"
xmlns="..."
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<circle cx="12" cy="12" r="10">
</circle>
</svg>
</span>
</button>
<input
aria-hidden="true"
style="..."
tabindex="-1"
type="radio"
value="debit"
checked
name="credit-or-debit"
>


Радио-кнопка в Shadcn — это кнопка с ARIA-атрибутами для имитации радио-кнопки, SVG-круг, скрытый <input type="radio">, который нужен для работы форм и классы Tailwind. Всё может быть гораздо проще, если использовать стандартный HTML:

<input
type="radio"
name="credit-or-debit"
value="debit"
checked
>


Применение appearance: none к флажкам и радио-кнопкам сбрасывает их стандартный внешний вид. После этого можно задавать размеры, фон, рамку, тень, закругление и так далее. Маркеры можно сделать при помощи псевдо-элементов.

<input
type="radio"
name="credit-or-debit"
value="debit"
checked
>
<style>
input[type="radio"] {
--color: currentColor;
appearance: none;
display: inline-grid;
place-content: center;
width: 24px;
height: 24px;
border: 2px solid var(--color);
border-radius: 50%;
margin: 0;

&::before {
content: '';
width: 12px;
height: 12px;
border-radius: 50%;
background-color: transparent;
transition: background-color .3s;
}

&:checked::before {
background-color: var(--color);
}

@media (forced-colors: active) {
--color: ButtonText;
}
}
</style>


Задумка Shadcn и Radix, по всей видимости, в надёжности стилизации. С <input> дела обстоят хуже, чем с <button>. Но всё же решение с appearance работает сегодня во всех актуальных браузерах. Тогда к чему весь этот оверинжиниринг?

#html #css #ui
🔥11👍53🌚3👨‍💻1
Маркетинговый сайт, веб-компоненты и nanotags

Павел Гринченко в блоге Злых Марсиан описал способ разработки маркетинговых сайтов с использованием веб-компонентов и разработанного для них инструмента под названием nanotags, в основе которого другое решение от Марсиан — nanostores.

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

Остаётся вопрос с тем, как добавлять ту самую интерактивность. Павел считает API веб-компонентов, встроенные в браузеры, полезными для этой задачи. Но в сыром виде API низкоуровневые и многословные. Поэтому была создана библиотека nanotags.

Это лёгкая библиотека, которая добавляет декларативный способ создания пользовательских элементов, поиска узлов, объявления реактивных свойств на основе nanostores и обработки событий. Цель — скрыть шаблонный код под капот.

nanotags рассчитан на работу с готовой разметкой, генерируемой на сервере или при сборке. Шаблонизатор отсутствует, Shadow DOM не используется. Это отличает nanotags от популярной библиотеки для веб-компонентов — Lit.

<my-counter count="0">
<span data-ref="display">0</span>
<button data-ref="button">+1</button>
</my-counter>
<script>
import { define } from "nanotags"

define("my-counter")
.withProps(p => ({
count: p.number(0)
}))
.withRefs(r => ({
display: r.one("span"),
button: r.one("button")
}))
.setup(ctx => {
ctx.on(ctx.refs.button, "click", () => {
ctx.props.$count.set(ctx.props.$count.get() + 1)
})
ctx.effect(ctx.props.$count, val => {
ctx.refs.display.textContent = String(val)
})
})
</script>


В разметке пользовательский элемент <my-counter>, внутри которого <span> и <button>. Разметка генерируется Astro на этапе сборки. Вместо Astro может быть любой генератор статики или серверное решение, которое отдаёт готовый HTML.

Затем подключается nanotags. Функция define() определяет пользовательский элемент. В withProps() указываются реактивные свойства, их тип и начальное значение. В withRefs() задаются ссылки на узлы, которые должны быть в разметке.

В setup() идёт установка обработчиков событий и эффектов, которые меняют значения реактивных свойств. Доступен объект контекста ctx, в котором хранятся свойства, ссылки на узлы и прочая информация о компоненте.

Библиотека хорошо интегрирована с TypeScript, выводит типы, поддерживает Standard Schema валидаторы (Valibot, Zod, ArkType) для свойств, работает с данными в формате JSON, делает проверки в рантайме на основе схем.

Больше возможностей nanotags описано в документации. Это интересный взгляд на то, как работать с веб-компонентами. Миграция сэкономила 100кб JS, что хорошо и позволяет лучше вписываться в бюджеты производительности.

Также я обратил внимание, что библиотека разработана с уважением к стандарту HTML. Ссылки на элементы отмечаются атрибутом data-ref, JSON-данные хранятся в блоках данных, пользовательские элементы названы в соответствии с правилами.

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

#js #tools
🔥3🤔311
Разработка сайтов, дружелюбных для агентов

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

Агенты взаимодействуют с сайтами одним из трёх способов:

- Анализ скриншота страницы с помощью машинного зрения;

- Анализ HTML-кода страницы и построенного на его основе DOM;

- Анализ дерева доступности, построенного из DOM и CSSOM.

Возможны комбинации этих способов, чтобы разрешить неоднозначные моменты: в разметке может быть кнопка «Добавить в корзину» как <div>, но на скриншоте она будет выглядеть как кнопка, а JS добавит необходимое поведение (не делайте так).

В статье даются советы, которые помогут агентам лучше воспринимать сайт:

- Все действия чётко обозначены в интерфейсе;

- Структура сайта стабильная и консистентная;

- Нет прозрачных наложений поверх интерфейса;

- Структура контента передана с помощью семантического HTML или ARIA, если нет возможности использовать семантический HTML;

- Интерактивные элементы обозначены в CSS с помощью cursor: pointer;

- Поля формы и подписи к ним связаны через <label for="...">;

- Размер интерактивных элементов как минимум 8×8px.

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

Чётко идентифицируемые элементы, стабильная структура, семантический HTML, ARIA для нестандартных виджетов, связь полей с метками, размер элементов, курсор с пальцем и отсутствие наложений — это база при разработке сайтов.

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

Тем, кто всегда стремился к качественным, семантичным, доступным и быстрым сайтам, выбирал готовый HTML и статику вместо SPA и сложных решений — повезло, их сайты уже со старта гораздо лучше адаптированы для агентов.

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

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

#html #css #ui
7👍4😁1🤔1
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🔥41🥰1🎉1