<divelopers>
СтатистикаРандомные мысли про HTML, CSS, доступность, пользовательские интерфейсы, производительность, браузеры и веб-стандарты. Автор: @alexnozer
- Последний пост
- 14 авг.
- Последнее чтение
- 14:47
- Постов за неделю
- 3
- Всего постов
- 24
- Тип
- открытый
- Язык
- русский
- Категория
- Технологии (по похожим)
- В каталоге с
- 12 авг.
- 1/24сутки в ленте
- 312
- 1/48двое суток
- 357
- 1/72трое суток
- 385
Оценка по просмотрам недавних постов: пост набирает почти всё за первые сутки.
Посты
видео или голосовое, без подписи
видео или голосовое, без подписи
видео или голосовое, без подписи
видео или голосовое, без подписи
видео или голосовое, без подписи
видео или голосовое, без подписи
видео или голосовое, без подписи
видео или голосовое, без подписи
видео или голосовое, без подписи
видео или голосовое, без подписи
видео или голосовое, без подписи
Язык документа и его частей В посте, посвящённому всемирному дню осведомлённости о доступности, упоминалось, что на 13.5% сайтов не задан язык документа. Радует, что это не много по сравнению с другими проблемами. Не радует, что это всё ещё 13.5%. Исправление крайне простое: нужно задать у корневого элемента страницы <html> атрибут lang со значением, которое соответствует основному языку контента на странице в формате BCP 47. <!-- Русский --> <html lang="ru"></html> <!-- Русский, Россия --> <html lang="ru-RU"></html> <!-- Английский --> <html lang="en"></html> <!-- Английский, Британия --> <html lang="en-GB"></html> <!-- Английский, США --> <html lang="en-US"></html> На этом можно было бы и закончить пост. Просто добавьте атрибут и это исправит одну из топ 6 ошибок доступности, которую обнаруживает WAVE. Но я расскажу ещё некоторые особенности и фишки вокруг языка страницы. От языка страницы зависит отображение некоторых элементов, в частности <q>. Он предназначен для встроенных цитат и обрамляет текст в кавычки. Вид кавычек меняется в зависимости от языка (ёлочки или лапки). Если сайт мультиязычный и хочется что-то менять в CSS в зависимости от языка, есть селектор :lang(). Он более мощный, чем селектор по атрибуту html[lang="..."] из-за масок и работы с вычисленным языком, а не значением в атрибуте. ol:lang(en-US) > li::marker { /* Маркеры списка 1) 2) 3) для американскго английского */ content: counter(list-item) ') '; } ol:lang(ru, by, uk) > li::marker { /* Маркеры списка 1. 2. 3. для восточнославянских языков */ content: counter(list-item) '. '; } На странице могут быть элементы, контент которых не соответствует основному языку страницы: цитаты, термины, названия или переключатель языка. Такие элементы также нужно пометить соответствующим атрибутом lang. Вообще lang — глобальный атрибут и его можно указывать у любых элементов. Язык наследуется вглубь по дереву от ближайшего родителя с lang. Сам атрибут переопределяет унаследованный язык. <html lang="ru"> ... <body> ... <button type="button" popovertarget="langs" > Язык </button> <dialog id="langs" popover> <ul> <li> <a href="/ru" aria-current="true" > Русский </a> </li> <li> <a href="/en" lang="en" hreflang="en" > English </a> </li> <li> <a href="/es" lang="es" hreflang="es" > Español </a> </li> ... </ul> </dialog> </body> </html> Атрибут hreflang помогает роботам понять, что страница по ссылке на другом языке. Это важно при SEO-продвижении мультиязычного сайта. К этому ещё стоит добавить ссылки на альтернативные версии текущей страницы на других языках. <head> ... <link rel="alternate" href="/en/catalog" hreflang="en" lang="en" title="Catalog" > <link rel="alternate" href="/es/catalog" hreflang="es" lang="es" title="Catalogar" > ... </head> Среди контента могут быть общепринятые термины, названия брендов или фразы, которые написаны на другом языке, но не должны переводиться автоматическими переводчиками. На этот случай существует атрибут translate со значением no. <p><span lang="fr" translate="no">Vite</span> позволяет быстро собирать <span lang="en" translate="no">frontend</span>-проекты различной сложности.</p> #html #css #a11y
Эффект съезда с тротуара Читая материал о доступности, встретился с феноменом «эффект съезда с тротуара» (Curb cut effect). Его суть показалась мне интересной, чтобы написать и поделиться своими мыслями в виде поста. Представьте проезжую часть и тротуар с бордюром вдоль неё. В некоторых местах есть съезды: бордюр спущен на уровень проезжей части, а сам тротуар немного наклонён. Это сделано для устранения барьеров людям на инвалидных колясках. Но также это помогает людям с детской коляской, сумкой на колёсиках, тележкой, велосипедом, самокатом, скейтбордом и другими средствами личной мобильности или вещами с колёсами. Ещё это помогает пожилым людям. Эффект съезда с тротуара — это социально-экономический феномен, суть которого в том, что решения, изначально созданные для узкой группы людей, в конечном итоге оказываются полезными и эффективными для более широкой группы. Доступность сайтов и приложений часто воспринимается как специфическая работа ради малой группы пользователей, которой можно пренебречь. Тут как раз действует эффект съезда с тротуара. Улучшение доступности сайта даст больший эффект. Внедрение практик доступности окажет положительный эффект на всех. Причём эффект распространяется не только на людей, но и на машины. Отмечено влияние на поисковые роботы, агенты ИИ, разные парсеры и программы. Вот несколько примеров: - Хороший шрифт с достаточным размером и контрастными цветами улучшает читабельность сайта, пользователям удобнее читать; - Субтитры позволят пользователям смотреть видео без звука и читать субтитры в шумном помещении, если нет наушников; - Достаточно большие интерактивные элементы легче нажимать пальцами или стилусом на смартфоне или планшете с сенсорным экраном; - Подписи, подсказки и автодополнение у полей ввода помогут пользователям быстрее заполнять формы с меньшим количеством ошибок; - Хорошо размеченная с помощью семантического HTML страница более понятна поисковым работам, агентам, лучше разбирается в режиме чтения; - Текстовая расшифровка видео позволяет использовать поиск по тексту, переводить его на другой язык и копировать для различных целей; - Правильно заданный язык страницы и элементов помогает программе-переводчику определить язык и предложить перевод. Примеров можно ещё много придумать. Суть в том, что доступность можно и нужно рассматривать не как специальную задачу по адаптации сайта, а как общее улучшение качества, которое скажется на разных его аспектах и пользователях. #a11y
CSS Linked Parameters При использовании встроенных SVG-иконок в HTML удобно то, что к ним есть доступ в CSS. Можно, например, изменять цвета, скрывать отдельные части иконки, менять толщину линий, радиус окружности, применять анимации и так далее. <a href="..."> <svg width="24" height="24" viewBox="0 0 24 24" > <path d="..."/> </svg> Назад </a> <style> a { color: var(--color-link); transition: color .3s linear; svg path { fill: currentColor; transition: fill .3s linear; } } a:hover { color: var(--color-link-hover); } </style> Однако при использовании внешних SVG-иконок доступ в CSS теряется. Отчасти это можно обойти через SVG-спрайты, но у них есть свои нюансы. CSSWG выпустила черновик CSS Linked Parameters, который предлагает решение проблемы. Суть предложения в том, чтобы передавать через URL специальные параметры и затем получать к ним доступ через переменные окружения в атрибутах и CSS. Так можно будет передать цвет во внешний SVG-файл, подключаемый через <img> или url(). Дисклеймер: это обзор раннего предложения новых функций. Синтаксис может измениться в будущем или от функций могут отказаться <!-- #param() задаёт параметр --color со значением grern --> <img src="icon.svg#param(--color,green)"> <img src="icon.svg"> <style> img { /* Параметры можно задать в CSS с помощью link-parameters. Это добавит к src и ссылкам на все ресурсы #param(--color,green) */ link-parameters: param(--color, green) ; } .icon { /* Параметры можно задать в функции url() в CSS */ background-image: url( "icon.svg", param(--color, green) ) ; } </style> <!-- icon.svg --> <svg> <!-- env() считывает переданный в URL параметр --color, и устанавливает в атрибут fill, значение по умолчанию black --> <path fill="env(--color, black)" d="..." /> </svg> <!-- icon.svg --> <svg> <!-- можно задать в CSS --> <style> path { fill: env(--color, black); } </style> <path d="..."/> </svg> Можно передавать несколько параметров, перечислив их в URL через &, указав несколько param() через пробел в url() или через запятую в link-parameters. В качестве значения могут быть пользовательские свойства CSS. <!-- параметры в URL через & --> <img src="icon.svg#param(--color,green)¶m(--stroke,1)"> <img src="icon.svg"> <style> img { /* несколько param() в link-parameters, доступны пользовательские свойства */ link-parameters: param(--color, var(--color-primary)), param(--stroke, 1) ; } .icon { /* несколько param() в url(), доступны пользовательские свойства */ background-image: url( "icon.svg", param(--color, var(--color-primary)) param(--stroke, 1) ) ; } </style> Таким образом появляется возможность передавать данные во внешние файлы. Основное назначение — <svg>, но также упоминается возможность передачи для <iframe>. Синтаксис фрагментов используется для обратной совместимости. #html #svg #css
Отсутствующие подписи для полей Более половины сайтов (51%) по данным отчёта WebAIM Million 2026 содержат поля ввода без подписей. У полей нет имени и нельзя идентифицировать какие данные необходимо ввести. Это одна из 6 самых частых ошибок в WAVE. Исправить эту ошибку не сложно. Нужно указать подпись (имя) одним из способов. Порядок отражает приоритет применения: - aria-labelledby - aria-label - <label> - title - placeholder - aria-placeholder Лучше всего указывать видимые подписи в <label> и связывать их с полями через for и id. Или оборачивать в <label> с подписью внутри. Эти варианты не требуют ARIA, что соответствует первому правилу. Поэтому они предпочтительны. <label for="first_name"> Имя (обязательно) </label> <input type="text" name="first_name" id="first_name" autocomplete="given-name" autocapitalize="words" placeholder="Алексей" required > <label> Имя (обязательно) <input type="text" name="first_name" autocomplete="given-name" autocapitalize="words" placeholder="Алексей" required > </label> Атрибут title использовать не стоит. Он задаёт имя полю, но также создаёт системную всплывающую подсказку. Эта подсказка не соответствует требованиям доступности по критериям 1.4.4, 1.4.10, 1. 4.12, 1.4.13. Иногда дизайнеры рисуют подпись внутри поля и это выглядит как заполнитель. В таком случае разработчик может указать атрибут placeholder. Хотя он задаёт имя полю, он предназначен не для этого. Используйте атрибут по назначению. Когда подпись находится внутри поля, можно прибегнуть к технике плавающих подписей. Это когда при фокусе на поле, подпись подъезжает вверх и остаётся видимой. Техника не лишена проблем, но это лучше, чем подпись в placeholder. aria-label можно применить для быстрого исправления отсутствующей подписи, но она не будет видна визуально. Это нарушает один из критериев WCAG и ухудшает доступность. Поэтому лучше избегать aria-label для подписей. aria-labelledby работает как <label>, но наоборот: поле ссылается по id на элемент, который действует как подпись. Такой способ подойдёт для нестандартных полей ввода или если нет возможности добавить <label> в разметку. Что касается критериев, применимых к подписям, то их пять: - 1.3.1 Info and Relationship — подпись должна быть программно связана с полем, а её визуальная информация программно обозначена; - 2.4.6 Headings and Labels — подпись, если она есть, должна быть понятной и описательной; - 2.5.3 Label in Name — видимая подпись должна полностью или частично соответствовать программно заданному имени поля; - 3.3.2 Labels or Instructions — у полей должна быть видимая подпись или инструкция для обозначения требуемой информации; - 4.1.2 Name, Role, Value — у поля должно быть программно заданное имя. Вариант с <label> соответствует всем критериям, поэтому он предпочтительный. Всем критериям будет соответствовать вариант с aria-labelledby, ссылаясь на элемент с видимым описательным текстом. Это резервный вариант. <label for="first_name"> Имя (обязательно) </label> <input type="text" name="first_name" id="first_name" autocomplete="given-name" autocapitalize="words" placeholder="Алексей" required > <span id="first_name"> Имя (обязательно) </span> <input type="text" name="first_name" autocomplete="given-name" autocapitalize="words" placeholder="Алексей" required aria-labelledby="first_name" > #html #a11y
Prop for that Адам Аргайл выпустил небольшую JS-библиотеку prop-for-that, которая следит за различными параметрами страницы и записывает значения в пользовательские свойства CSS. Это позволяет писать стили на основе значений этих свойств. Что библиотека может отслеживать: - Положение курсора (x, y) в px и % от области просмотра; - Размер области просмотра; - Текущее время (часы, минуты, секунды, текущий timestamp); - Количество кадров в секунду; - Онлайн-статус; - Состояние видимости и фокусировки страницы; - Параметры сети (тип, downlink, rtt, режим экономии данных); - Состояние батареи (процент заряда, заряжается ли); - Нагрузку на CPU; - Прокрутку (направление и скорость); - Визуальную область просмотра (pinch-zoom, отображение виртуальной клавиатуры); - Виртуальную клавиатуру (открыта ли, размеры, координаты); - Ориентацию устройства; - Параметры акселерометра; - Геолокацию; - Плотность пикселей экрана; - Количество ядер процессора; - Доступный объём оперативной памяти; - Ширину полосы прокрутки; - Тип навигации; - Мета-данные (theme-color, og-image, color-scheme); - Размеры элемента; - Видимость элемента; - Значение <input type="range">; - Свойства текстовых полей (длина, пустое ли, валидное ли, лимит символов); - Состояние валидности полей ввода; - Состояния полей («чистое», «грязное», тронутое, нетронутое, изменённое, отправленное); - Количество полей (валидных, не валидных, заполненных); - Состояния <select> (индекс, количество опций, количество выбранных опций, текущее значение); - Цвет из <input type="color">; - Состояния <audio> и <video> (прогресс, текущее время, длительность, пауза, громкость); - Состояния <img> (физические размеры, загружено ли, ошибка загрузки); - Цвета <img> (доминантный, акцентный, тёмный, светлый, средний, температура); - Цвета <video> (доминантный, акцентный); - Состояние урезанного текста (line-clamp). Достаточно большой список. Плюс к этому есть JS API и система плагинов, поэтому библиотеку можно расширять. Некоторые свойства глобальные, как состояние сети, процессора или батареи. Такие свойства записываются в :root. Другие свойства работают на уровне элементов. Чтобы библиотека отслеживала свойства, нужно указать атрибут data-props-for у элемента и перечислить через запятую названия отслеживаемых свойств. По умолчанию ничего не отслеживается. <div class="box" data-props-for="size" style="resize: both" > </div> <style> .box::after { counter-reset: w calc(var(--live-w)) h calc(var(--live-h)); content: counter(w) ' × ' counter(h); } </style> В примере указано, что нужно отслеживать размеры (data-props-for="size"). В CSS будут доступны свойства --live-w и --live-h с актуальными значениями. Для примера они выводятся в элементе с помощью счётчиков. В документации много примеров, где и как это можно применить. Особенно хорошо это работает в сочетании с Container Style Queries и функцией if(), позволяя делать динамическую стилизацию на основе значений свойств. Счётчики и псевдо-элементы со свойством content можно использовать для вывода значений в интерфейсе. Также можно скрывать или показывать отдельные узлы по условию. В общем, фантазии хватает как это можно применить. #css #js
Вау-эффекты и проблемы гидратации Прочитал пост в канале Максима Морева о вау-эффектах на сайтах. Контекст: сайт документации drag-n-drop плагина для Vue создан на VitePress и на страницах есть мешающие чтению визуальные эффекты. Красиво, но не практично. Я решил зайти на сайт со своего телефона. Модель не флагманская, но относится к high-end категории. Я увидел… шапку с логотипом, иконкой поиска и бургером, переливающуюся полоску, пустоту и подвал с копирайтом. При нажатии на бургер, иконка меняется на крестик, но меню не открывается. При нажатии на поиск перенаправляет на страницу поиска, но поля ввода не видно. Нажатие на логотип возвращает на главную. Везде пустота. Шанс моего ухода с такого сайта 100%, я не вижу буквально ничего, сайт бесполезен. Причина, как выяснилось, в гидратации. Где-то SSR HTML не сошёлся с клиентским деревом компонентов и у контента не удалилось свойство opacity: 0. Досадная ошибка, которая сделала сайт бесполезным. Все мы допускаем ошибки и это нормально. Но не будь этот сайт создан на VitePress с гидратацией на клиенте и вау-эффектами, эта ошибка бы технически не могла возникнуть. Это сайт документации — текст, блоки кода (тоже текст), демо и изображения. Контент статический и требует минимального JS для меню, поиска, переключения вкладок и подобных виджетов. Цель сайта — дать информацию, а не показать эффекты. Нет ни единого повода добавлять на такой сайт гидратацию, клиентский роутинг, реактивность и компонентную модель Vue. Это всё обходится сайту в 262кб (1.5мб без cжатия) JS и сопутствующими затратами на его обработку без какой-то пользы. Набор предварительно сгенерированных лёгких HTML-страниц (SSG), точечный JS для виджетов, навигация ссылками <a>, View Transition, предварительная загрузка и Speculation Rules дадут тот же эффект «мгновенной навигации SPA» без JS. VitePress умеет работать в режиме MPA без JS, но экспериментально. Альтернатива — Astro или любой генератор статических сайтов без JS в рантайме по умолчанию. Это проще, надёжнее, производительнее и не будет никаких ошибок гидратации. #js #architecture
Пустые кнопки и ссылки По данным отчёта WebAIM Million 2026 на 46.3% сайтов встречаются пустые ссылки и на 30.6% сайтов — пустые кнопки. Обе эти проблемы входят в топ 6 самых частых нарушений доступности, обнаруживаемых с помощью WAVE. Пустые кнопки и ссылки — это те, у которых имя не задано ни одним из способов и браузер вычисляет его как пустую строку. Вспомогательные технологии используют имя для идентификации элементов, а люди полагаются на это при взаимодействии. - Программы чтения с экрана озвучивают тип элемента и его имя: «Каталог, ссылка» или «Добавить в корзину, кнопка». Пользователь слышит и понимает, что за элемент и что он делает; - Программы голосового управления ищут элементы по имени: «Нажми Каталог» или «Нажми Добавить в корзину» программа найдёт элемент на экране и нажмёт на него; - Программы чтения с экрана и специальные браузерные расширения выводят все ссылки или кнопки со страницы в виде списка, имя используется при формировании этого списка. При отсутствии имени элементы будут анонимными. Просто какая-то «Ссылка» или «Кнопка». Не понятно назначение, сложно идентифицировать, не удобно взаимодействовать. Всё это создаёт препятствия для пользователей. Чаще всего пустые ссылки и кнопки получаются, когда они отображаются в виде иконок: три горизонтальные линии, тележка, крестик, урна, стрелка, плюс и так далее. Визуально может быть понятно, но программно — нет. <button type="button"> <svg> <!-- иконка с тремя линиями --> </svg> </button> <a href="/cart" class="icon icon--cart" > <!-- иконка тележки в CSS --> </a> <button type="button"> <i class="fa-solid fa-trash"> <!-- иконка мусорки из шрифта --> </i> </button> <a href="/catalog?page=3"> <!-- внешняя иконка стрелки --> <img src="/assets/arrow-left.svg" alt="" > </a> Это примеры «пустых» кнопок и ссылок без имени. Базовое правило таково: у кнопок и ссылок должно быть короткое, лаконичное и понятное имя, заданное одним из способов: - атрибут aria-labelledby; - атрибут aria-label; - элемент <label> (только для <button>); - текстовый контент; - атрибут title. Порядок указывает приоритет применения. aria-label переопределяет имя, заданное через <label>, а aria-labelledby, в свою очередь, переопределяет имя, заданное в aria-label. Стоит выбрать какой-то один способ и не смешивать во избежание неожиданностей. Наилучшим из способов будет видимый текстовый контент. Он соответствует первому правилу ARIA: не полагаться на атрибуты ARIA и использовать встроенные способы. Также видимый текст можно прочесть в отличие от скрытого. То есть кнопка или ссылка с иконкой и текстом — лучшее из возможных решений. Глядя на текст становится понятно, что нужно сказать для активации элемента голосом. Также люди с когнитивными нарушениями могут не понимать метафору иконки. <button type="button"> <svg aria-hidden="true"> <!-- иконка с тремя линиями --> </svg> Меню </button> <a href="/cart" class="icon icon--cart" > <!-- иконка тележки в CSS --> Корзина </a> <label for="delete"> Удалить </label> <button type="button" id="delete"> <i class="fa-solid fa-trash" aria-hidden="true" > <!-- иконка мусорки из шрифта --> </i> </button> <a href="/catalog?page=3"> <!-- внешняя иконка стрелки --> <img src="/assets/arrow-left.svg" alt="" > Предыдущая страница </a> При наличии видимого текста стоит скрыть иконки, если они выполняют только функцию визуальной подсказки и текст уже описывает это. Подойдёт атрибут aria-hidden="true" или пустой alt="" у <img>, что скрывает элементы из дерева доступности. Видимый контент это хорошо, но не всегда возможно, потому что часто дизайнеры против лишнего визуального шума и загромождения интерфейса, особенно на мобильных. Поэтому распространены именно ссылки и кнопки только с иконками. В таком случае придётся прибегнуть к скрытым подписям, которые задают имя, но не видны в интерфейсе. Если следовать всё тому же правилу ARIA, то есть несколько способов задать скрытое имя без использования атрибутов ARIA: - Элемент <title> внутри <svg> задаёт альтернативный текст для изображения-иконки (работает как alt у <img>) и используется как текстовый контент для вычисления имени; - Специальный класс visually-hidden делает элемент визуально невидимым, но всё ещё доступным как текстовый контент, который задаёт имя; - Атрибут title задаёт имя элементу при отсутствии контента, но текст в атрибуте отображается в виде системной всплывающей подсказки только при наведении (и при фокусе в Edge); - Атрибут alt задаёт альтернативный текст для изображения и его значение также используется как текстовый контент и задаёт имя элементу. Элемент <title> и атрибут title создают системные всплывающие подсказки. C ними много проблем: появляются только при наведении мыши, не реагируют на настройки размера текста и так далее. Не рекомендуется их использовать. <button type="button"> <svg> <title> Меню </title> <!-- иконка с тремя линиями --> </svg> </button> <a href="/cart" class="icon icon--cart" > <!-- иконка тележки в CSS --> <span class="visually-hidden"> Корзина </span> </a> <button type="button" title="Удалить" > <i class="fa-solid fa-trash"> <!-- иконка мусорки из шрифта --> </i> </button> <a href="/catalog?page=3"> <!-- внешняя иконка стрелки --> <img src="/assets/arrow-left.svg" alt="Предыдущая страница" > </a> Если прибегнуть к ARIA, то имя кнопок и ссылок можно задать с помощью атрибутов aria-label или aria-labelledby. Можно указать как на самих элементах, так и на иконках в виде альтернативного текста, который будет использован как текстовый контент кнопки. <button type="button"> <svg role="img" aria-label="Меню" > <!-- иконка с тремя линиями --> </svg> </button> <a href="/cart" class="icon icon--cart" aria-labelledby="cart-label" > <!-- иконка тележки в CSS --> <span id="cart-label" hidden> Корзина </span> </a> <span id="del-icon" hidden> Удалить </span> <button type="button"> <i class="fa-solid fa-trash" role="img" aria-labelledby="del-icon" > <!-- иконка мусорки из шрифта --> </i> </button> <a href="/catalog?page=3" aria-label="Предыдущая страница" > <!-- внешняя иконка стрелки --> <img src="/assets/arrow-left.svg" alt="" > </a> - role="img" превращает <svg> в <img>, а aria-label в таком случае работает как alt. Альтернативный текст используется как имя элемента; - aria-labelledby ссылается на элемент по указанному id и текст этого элемента используется как имя, при этом сам элемент может быть скрыт; - aria-labelledby также работает как alt для элементов с role="img" и берёт альтернативный текст из указанного элемента; - самый простой и короткий способ — просто указать ссылке или кнопке атрибут aria-label, который задаёт имя элементу. Какой бы способ вы ни выбрали, всегда проверяйте значение Name у элемента на панели Accessibility в DevTools и проходитесь программой чтения с экрана. Не лишним будет прогнать страницу через Axe и подключить линтеры. #html #a11y
Пример приложения на чистом Node.js и Web API Я не так давно делился мнением по поводу высказывания Тимура Шемсединова о том, что фронтенд фреймворки не нужны и это инерция. Я неоднократно видел, что подход разработки с использованием встроенных Web API вполне возможен. Тимур поделился репозиторием с proof of concept своей мысли. Это приложение редактирования профиля: простой сервер с роутером на чистом Node.js и фронтенд в SPA-стиле на ES-модулях, веб-компонентах c Shadow DOM и шаблонами. Приложение позволяет смотреть, искать, создавать, изменять и удалять профили экспертов с валидацией данных на клиенте и сервере, вычисляемыми полями и общей выделенной доменной логикой для клиента и сервера, не простой CRUD. Особенность проекта в том, что он целиком построен на чистом Node.js и браузерных API и не использует рантайм-зависимости, фреймворки и сборщики. Это пример ровно того подхода, о котором говорил Тимур. Ограничения прописаны в репозитории: - Запросы данных напрямую из компонентов запрещены; - Только встроенные API; - Никаких рантайм-зависимостей из npm, dev-зависимости допустимы, но опциональны (проект использует ESLint и Prettier); - Чистый Node.js для серверной части; - Стандартные ES-модули на клиенте; - Веб-компоненты, Template API и Navigation API на клиенте; - <template> для фрагментов UI и веб-компонентов с Shadow DOM, где уместно. В репозитории проекта есть подробный README с описанием архитектуры сервера и клиента, выделенной доменной логики, валидации, роутинга, декларативного рендеринга, шаблонов, сборки страниц, API, хранилища, пользовательских сценариев. Я изучил код. Мне всё понятно, код аккуратный и лаконичный, есть чёткая структура, понятно, где что находится и как это расширять. И нет никаких монструозных абстракций, которых все боятся, когда речь о разработке на чистом Web API. Да, это не похоже на общепринятый «индустриальный стандарт». Тут нет декларативных шаблонов с реактивностью, сложного клиентского состояния, а само приложение ориентировано на сервер. Зато ноль зависимостей, не нужно думать о tree shaking, смотреть bundle analyzer, искать дубликаты, регулярно обновляться, следить за релизами и уязвимостями, слать issue и ждать релиз с исправлением бага, бороться с ограничениями, конфигурировать сборку. Весь код полностью доступен, его можно доработать под потребности, он решает только проектные задачи, лишнее можно удалить, чтобы не висело мёртвым грузом, недостающие абстракции можно дописать, с LLM это довольно быстро. Многие команды застревают в поддержке. Проект работает на старых версиях, уходят месяцы на обновления и оптимизацию бандлов, выделяются отдельные специалисты или инфраструктурные команды (кто-то называет их FrontOps). Я вижу тут компромисс и trade off: DX, некоторые удобства, синтаксический сахар и «индустриальный стандарт» меняется на простоту поддержки, устойчивость, долговечность и полное владение кодовой базой. Снижается паразитная сложность, система становится менее громоздкой, упрощается деплой и поддержка. Сложность становится контролируемой. Но это требует опыта, дисциплины, хорошего знания стандартов и Web API. В процессе возникает «фреймворк», хотя это набор специфичных для проекта решений, что лично я бы фреймворком не назвал. Пусть будет «свой велосипед». React тоже был «своим велосипедом» для решения проблем команды Facebook. Возможно, я предвзят, потому что сам предпочитаю работать со стандартными API и не люблю лишние зависимости. В любом случае, советую посмотреть проект и заложенные там идеи. Без фреймворков писать можно и это не так страшно, сколько непривычно. #js #web_api #architecture
Вредные компоненты без JS Мне нравится идея создания компонентов без JS. Видео Native CSS Components vs Modern UI Libraries показывает реализацию нескольких компонентов на HTML/CSS и сравнивает их с аналогами из Shadcn. Компоненты в Shadcn действительно переусложнены. Но у показанных в видео аналогов есть проблемы. Рассмотрим, что не так с каждой реализацией и как можно сделать лучше (сейчас или в будущем). Диалог Диалог реализован с помощью Popover API. popover не задаёт нужную семантику. <div popover> остаётся контейнером с role="group". У диалога должна быть соответствующая семантика, состояния, поведение и UX. Есть <dialog>, который обладает нужной семантикой и свойствами. Он работает с popover для немодальных диалогов, а с command для модальных и немодальных. Атрибут closedby="any" добавляет закрытие по нажатию вне диалога. <!-- немодальный диалог с popover --> <button popovertarget="dlg" popovertargetaction="show" > Open dialog </button> <dialog id="dlg" popover> ... <button popovertarget="dlg" popovertargetaction="hide" > Close </button> </dialog> <!-- немодальный диалог с command --> <button commandfor="dlg" command="show-popover" > Open dialog </button> <dialog id="dlg" popover> ... <button commandfor="dlg" command="hide-popover" > Close </button> </dialog> <!-- модальный диалог с command и closedby --> <button commandfor="dlg" command="show-modal" > Open dialog </button> <dialog id="dlg" closedby="any"> ... <button commandfor="dlg" command="close" > Close </button> </dialog> Выпадающее меню В видео выпадающее меню реализовано с помощью popover и Anchor Positioning. Как и с диалогом, меню не обладает нужной семантикой и поведением. Dropdown menu в Shadcn реализован как элемент с ролью menu. Виджет menu предназначен для команд, а не навигационных ссылок. Внутри допустимы только определённые роли. Требуется навигация стрелками и Home/End. Для этого нужен JS. Всё может измениться с приходом focusgroup, но пока это экспериментальный API. <button commandfor="menu" command="toggle-popover" > Open menu </button> <div id="menu" role="menu" popover focusgroup="menu" > <button role="menuitem"> Profile </button> <button role="menuitem"> Settings </button> <button role="menuitem"> Sign out </button> </div> Всплывающая подсказка В видео отображение подсказки реализовано через :hover/:focus и комбинатор ~. Роль подсказки — tooltip, а кнопка ссылается на неё через aria-describedby. Такая всплывающая подсказка нарушает критерий WCAG 1.4.13 Content on Hover or Focus. Реализация поведения для соответствия 1.4.13 требует JS. Можно использовать popover с его закрытием по Esc, но отображение потребует JS. Interest Invokers API и popover="hint" позволят в будущем реализовать это без JS. <button interestfor="copy" aria-describedby="copy" > Copy </button> <span id="copy" popover="hint" role="tooltip" > Hover or focus me </span> Вкладки Вкладки реализованы как набор радио-кнопок, которые в сочетании с псевдо-классом :checked и селектором :has() у родителя переключают дочерние панели. Вновь не хватает нужной семантики и поведения и в целом это хак. На канале есть разбор вкладок с использованием ссылок <a> и псевдо-класса :target и почему это не вкладки. То же с радио-кнопоками и :has(). Реализовать вкладки без JS сейчас нельзя, но, возможно, появятся новые элементы или примитивы. Аккордеон На последок виджет аккордеона. В видео он сделан из нескольких <details> и <summary>. Также демонстрируется новый псевдо-элемент ::details-content и свойство interpolate-size: allow-keywords для анимации раскрытия. Вот аккордеон можно использовать вместо решений на JS, это не хак. Тут ещё можно углубиться в различия аккордеона и раскрываемых панелей, но в другой раз. А примеры диалога, меню, подсказки и вкладок из видео использовать не стоит. #html #css #js #ui