Эволюция стилей в темах Shopify
Адам Ватан на днях поделился новостью, что Shopify выкупил Tailwind. На DotDev 2026, конференции, организованной Shopify, был анонс внедрение Tailwind в движок Shopify Storefront Renderer. В связи с этим обзор подходов к стилям в Shopify.
Вопрос организации стилей в темах Shopify лежит на плечах авторов тем. Тут каждый сам решает, как к этому подойти. Поэтому можно найти разные решения. В рамках этого поста я сосредоточусь на решениях в официальных темах от Shopify.
Тема Debut, 2016-2021 годы. Все стили находятся в файле
Файл представлен как
Сами стили написаны в синтаксисе SCSS, что отражено в виде расширения
У подхода две проблемы:
- Время компиляции откладывает загрузку страницы и TTFB, особенно при больших размерах файла и при большом количестве SCSS-фич;
- Большой файл стилей откладывает отрисовку до тех пор, пока не будет полностью загружен и обработан.
Тема Dawn, 2021-2025 годы. От компиляции Liquid и SCSS отказались. Стили находятся в файлах
Применяется сразу несколько техник загрузки стилей. Первая —
Вторая техника — отложенная загрузка. С атрибутом
Чтобы не ломать страницу, можно добавить резервный вариант внутри
Однако такая организация стилей требует грамотного разделения стилей, знания используемых техник и дополнительного менеджмента со стороны разработчика. В разметке много элементов
Тема Horizon, 2025-2026 годы. Новая флагманская тема Horizon пришла на смену Dawn с архитектурой блоков. Это независимые компоненты с настройками в админке, которые могут вставляться в секции на любой странице и друг в друга.
Это потребовало изменений в организации стилей. При старом подходе это означало бы десятки элементов
Стили размещаются в Liquid-тэге
Стили извлекаются из тэгов
Позже тэг
Другая причина внедрения
В Horizon сохраняется файл с базовыми стилями
В стилях отсутствует системный подход, много сложных селекторов с комбинаторами и высокой специфичностью, связанные стили могут находится в разных файлах, много переопределений. Всё это усложняет кастомизацию и поддержку.
Также агентам сложно работать с такой кодовой базой не смотря на набор скиллов от разработчиков темы. В ответ на все эти вызовы Shopify объявил о внедрении поддержки Tailwind, а теперь и новость о приобретении Tailwind.
То есть вместо создания CSS-файлов, использования тэгов
При всей моей нелюбви к Tailwind, выбор Shopify в пользу Tailwind понятен. Бандл будет минимальным, агентам будут проще работать, разработчикам не нужно будет думать про организацию стилей, отдельные файлы, именование и специальные тэги.
Пока это планы, реализации нет. Но всё равно интересно проследить эволюцию и увидеть, как в итоге всё больше приходят к Tailwind. Я со своими предпочтениями к ванильному CSS немного грущу, но что уж тут поделать.
#css
Адам Ватан на днях поделился новостью, что Shopify выкупил Tailwind. На DotDev 2026, конференции, организованной Shopify, был анонс внедрение Tailwind в движок Shopify Storefront Renderer. В связи с этим обзор подходов к стилям в Shopify.
Вопрос организации стилей в темах Shopify лежит на плечах авторов тем. Тут каждый сам решает, как к этому подойти. Поэтому можно найти разные решения. В рамках этого поста я сосредоточусь на решениях в официальных темах от Shopify.
Тема Debut, 2016-2021 годы. Все стили находятся в файле
theme.scss.liquid, где примерно 5700 строк кода. Файл содержит стили normalize.css, карусели slick, системы сетки, разных компонентов и шаблонов страниц самой темы.Файл представлен как
.liquid, то есть там доступны ограниченные возможности шаблонизатора Liquid. Это используется для получения различных настроек из админкм и подстановки в переменные для дальнейшего использования в стилях.Сами стили написаны в синтаксисе SCSS, что отражено в виде расширения
.scss. Бэкенд Shopify компилирует Liquid в SCSS, а затем SCSS в CSS по запросу. В коде много SCSS-переменных, миксинов, функций и кое где используется вложенность.У подхода две проблемы:
- Время компиляции откладывает загрузку страницы и TTFB, особенно при больших размерах файла и при большом количестве SCSS-фич;
- Большой файл стилей откладывает отрисовку до тех пор, пока не будет полностью загружен и обработан.
Тема Dawn, 2021-2025 годы. От компиляции Liquid и SCSS отказались. Стили находятся в файлах
.css или встроены в <style>, где требуется кастомизация. На страницу подключается набор базовых стилей и стили используемых секций.<head>
<!-- Базовые стили подключаются на всех страницах -->
<link
rel="stylesheet"
href="//shop.myshopify.com/cdn/.../base.css"
>
<!-- ... -->
</head>
<body>
<header class="section-header">
<style>
/*
Некоторые критические стили
шапки для быстрой отрисовки
*/
</style>
<!--
Менее критические стили, которые
не нужны сразу, загружаются лениво
-->
<link
rel="stylesheet"
href="//shop.myshopify.com/cdn/.../component-search.css"
media="print"
onload="this.media='all'"
>
</header>
<section
id="...__imge-banner"
class="shopify-section section"
>
<!--
Стили секции подключаются
перед контентом самой секций
-->
<link
rel="stylesheet"
href="//shop.myshopify.com/cdn/.../section-image-banner.css"
media="all"
>
<!-- Контент секции -->
</section>
<!-- Аналогично в других секциях -->
</body>
Применяется сразу несколько техник загрузки стилей. Первая —
<link> в <body>. Хотя <link> обычно находится в <head>, размещение в <body> даёт эффект последовательной отрисовки и улучшает показатель FCP за счёт уменьшения блокировок.Вторая техника — отложенная загрузка. С атрибутом
media="print" стили загружаются с пониженным приоритетом и не блокируют отрисовку. Когда они загрузятся, срабатывает обработчик onload и меняет print на all. После этого стили применяются.Чтобы не ломать страницу, можно добавить резервный вариант внутри
<noscript> без media="print" и обработчика onload. Если JS по каким-то причинам будет недоступен, стили загрузятся обычным способом.Однако такая организация стилей требует грамотного разделения стилей, знания используемых техник и дополнительного менеджмента со стороны разработчика. В разметке много элементов
<link>, которые провоцируют множество запросов.Тема Horizon, 2025-2026 годы. Новая флагманская тема Horizon пришла на смену Dawn с архитектурой блоков. Это независимые компоненты с настройками в админке, которые могут вставляться в секции на любой странице и друг в друга.
Это потребовало изменений в организации стилей. При старом подходе это означало бы десятки элементов
<link> в <body> и много запросов за небольшими файлами стилей. Это работоспособно, но не очень удобно и засоряет разметку.Стили размещаются в Liquid-тэге
stylesheet внутри файла секций и блоков. Это похоже на организацию Single File Component во Vue, когда стили колоцированы в одном файле вместе с шаблоном и логикой.<div class="block-heading">
<!-- Шаблон блока -->
</div>
{% stylesheet %}
/* Стили блока */
{% endstylesheet %}
{% schema %}
// Настройки блока
{% endschema %}
Стили извлекаются из тэгов
stylesheet у всех секций и блоков и объединяются в один бандл compiled_assets/styles.css. Это шаг назад к одному большому CSS-файлу времён Debut, но теперь он генерируется автоматически.Позже тэг
stylesheet был доработан так, чтобы для каждой страницы извлекались стили только используемых на ней секций и блоков. В теории это должно было давать бандл под каждую страницу, чего на практике не происходит.Другая причина внедрения
stylesheet — развитие агентской разработки. Агентам проще работать и сохранять контекст в рамках одного файла. Колокация шаблона и стилей этому способствует, а также уменьшает потребление токенов.В Horizon сохраняется файл с базовыми стилями
base.css и некоторое количество встроенных в <style> стилей. Что в Dawn, что в Horizon стили написаны достаточно сложно и запутано (во многом благодаря применению ИИ при разработке).В стилях отсутствует системный подход, много сложных селекторов с комбинаторами и высокой специфичностью, связанные стили могут находится в разных файлах, много переопределений. Всё это усложняет кастомизацию и поддержку.
Также агентам сложно работать с такой кодовой базой не смотря на набор скиллов от разработчиков темы. В ответ на все эти вызовы Shopify объявил о внедрении поддержки Tailwind, а теперь и новость о приобретении Tailwind.
То есть вместо создания CSS-файлов, использования тэгов
stylesheet и выстраивания системы, в шаблонах Liquid будут доступны классы Tailwind. Бэкенд Shopify будет анализировать шаблон и генерировать итоговый CSS-файл из Tailwind-классов.При всей моей нелюбви к Tailwind, выбор Shopify в пользу Tailwind понятен. Бандл будет минимальным, агентам будут проще работать, разработчикам не нужно будет думать про организацию стилей, отдельные файлы, именование и специальные тэги.
Пока это планы, реализации нет. Но всё равно интересно проследить эволюцию и увидеть, как в итоге всё больше приходят к Tailwind. Я со своими предпочтениями к ванильному CSS немного грущу, но что уж тут поделать.
#css
❤8🤝2
Подклассы Event вместо CustomEvent
Многие библиотеки предоставляют систему событий в качестве публичного API. Потребители могут подписываться на них и изменять поведение. Часто основой для событий служит класс
Нет ничего плохого в использовании
При работе с
Если событие не предполагает полезной нагрузки и служит только для оповещения, то достаточно обычного объекта
У
Если же событие подразумевает полезную нагрузку, то вместо
Также для собственных подклассов
Больше деталей про использование
- Stop Using CustomEvent;
- Creating Truly Custom Events for Web Components.
#js #web_api
Многие библиотеки предоставляют систему событий в качестве публичного API. Потребители могут подписываться на них и изменять поведение. Часто основой для событий служит класс
CustomEvent, у которого в detail могут быть данные.// Где-то в коде компонента
component.dispatchEvent(
new CustomEvent(
'quantity-change',
{
detail: {
newValue,
oldValue
}
}
)
);
Нет ничего плохого в использовании
CustomEvent. Исторически он существует, потому что для авторов библиотек нужен был механизм пользовательских событий, а наследование от встроенных классов до появления ES6 не поддерживалось.При работе с
CustomEvent нужно помнить, что полезная нагрузка события находится в свойстве detail. Нужен дополнительный шаг в цепочке доступа к свойству объекта или деструктуризация, что не очень удобно с точки зрения DX.component.addEventListener(
'quantity-change',
(e) => {
// чтение из detail
const newValue = e.detail.newValue;
const oldValue = e.detail.oldValue;
// или деструктуризация
const {
newValue,
oldValue
} = e.detail;
}
);
Если событие не предполагает полезной нагрузки и служит только для оповещения, то достаточно обычного объекта
Event. Его конструктор принимает тип (имя) и объект с параметрами события (bubbles, cancelable, composed).У
Event есть встроенные подкласы. Среди них, например, ErrorEvent, ToggleEvent или PointerEvent. Если семантика какого-то встроенного события подходит цели, то можно использовать соответствующий встроенный подкласс Event.Если же событие подразумевает полезную нагрузку, то вместо
CustomEvent можно использовать собственный подкласс Event. Так как это обычный класс, можно объявить произвольные поля экземпляра, без промежуточного detail.class QuantityChangeEvent extends Event {
static eventName = 'quantity-change';
newValue;
oldValue;
constructor(newValue, oldValue) {
super(QuantityChangeEvent.eventName);
this.newValue = newValue;
this.oldValue = oldValue;
}
}
// Где-то в коде компонента
component.dispatchEvent(
new QuantityChangeEvent(
newValue,
oldValue
)
);Также для собственных подклассов
Event с произвольными данными проще описать типы, чем для CustomEvent. Особенности типизации CustomEvent подробно описал Бартон Смит в статье Creating Strongly Typed Events for Web Components.Больше деталей про использование
Event можно найти в статьях от Джастина Фагнани и Бартона Смита:- Stop Using CustomEvent;
- Creating Truly Custom Events for Web Components.
#js #web_api
❤9👎2🤝1
Реализация пользовательских атрибутов
Под конец прошлого года я рассказывал об API пользовательских атрибутов, который предложен на замену отклонённых Apple настраиваемых встроенных элементов (Customized Built-In Elements). Это часть API веб-компонентов.
В Chrome и Firefox можно описать такой пользовательский элемент, который будет наследоваться от встроенного элемента, например кнопки, заголовка или абзаца. С помощью атрибута
Это позволяет сохранять поведение и семантику встроенных элементов HTML, но расширять их возможности и добавлять новое поведение. Apple категорически от этого отказались и Safari никогда не будет поддерживать эту возможность.
Пользовательских атрибуты предложены как альтернатива. На днях Кит Сиркель представил полифил пользовательских атрибутов, дополнив тем, что хотел бы видеть это в браузерах. А это значит, что скоро мы увидим прототип в Firefox.
Пользовательские атрибуты похожи на пользовательские элементы способом описания, регистрацией и жизненным циклом. Но с атрибутами можно расширять существующие элементы не вводя новые. Идея похожа на атрибут
В примере к полифилу приводится атрибут
Использование в HTML:
Такой API выглядит интереснее, чем
Вторая: один атрибут можно применить к разным элементам из-за наследования от
Пользовательские атрибуты дадут авторам HTML-first библиотек более удобный способ связывания атрибутов с элементами и возможность реагирования на события жизненного цикла. Кроме того, это способ расширять стандартные элементы.
Вместе с полифилом есть ссылка на эксплейнер, в котором подробнее описан предлагаемый API. Также есть страницы с описанием мотивации и дизайна API. Я заинтересован и буду ждать появления прототипа в Firefox.
#html #js #web_api
Под конец прошлого года я рассказывал об API пользовательских атрибутов, который предложен на замену отклонённых Apple настраиваемых встроенных элементов (Customized Built-In Elements). Это часть API веб-компонентов.
В Chrome и Firefox можно описать такой пользовательский элемент, который будет наследоваться от встроенного элемента, например кнопки, заголовка или абзаца. С помощью атрибута
is пользовательский элемент связывается со встроенным.Это позволяет сохранять поведение и семантику встроенных элементов HTML, но расширять их возможности и добавлять новое поведение. Apple категорически от этого отказались и Safari никогда не будет поддерживать эту возможность.
Пользовательских атрибуты предложены как альтернатива. На днях Кит Сиркель представил полифил пользовательских атрибутов, дополнив тем, что хотел бы видеть это в браузерах. А это значит, что скоро мы увидим прототип в Firefox.
Пользовательские атрибуты похожи на пользовательские элементы способом описания, регистрацией и жизненным циклом. Но с атрибутами можно расширять существующие элементы не вводя новые. Идея похожа на атрибут
is.Дисклеймер: это обзор раннего предложения новых функций. Синтаксис может измениться в будущем или от функций могут отказаться
В примере к полифилу приводится атрибут
persist-value, который вешает слушатель события input и записывает значение элемента в LocalStorage. В роли ключа используется значение атрибута. Такой атрибут применим к разным полям ввода.class PersistValue extends Attr {
connectedCallback() {
const stored = localStorage.getItem(
this.value
);
if (stored !== null) {
this.ownerElement.value = stored;
}
this
.ownerElement
.addEventListener(
"input",
this
);
}
disconnectedCallback() {
this
.ownerElement
.removeEventListener(
"input",
this
);
}
attributeChangedCallback(oldValue, newValue) {
if (oldValue !== null) {
localStorage.removeItem(oldValue);
}
}
handleEvent() {
localStorage.setItem(
this.value,
this.ownerElement.value
);
}
}
customAttributes.define(
"persist-value",
PersistValue
);Использование в HTML:
<input
name="email"
persist-value="email-draft"
>
<textarea
name="bio"
persist-value="bio-draft"
>
</textarea>
Такой API выглядит интереснее, чем
is как минимум по двум причинам. Первая: у одного элемента можно указать сразу несколько разных атрибутов. В is можно указать только имя одного конкретного пользовательского элемента.Вторая: один атрибут можно применить к разным элементам из-за наследования от
Attr. В is можно указать имя только того пользовательского элемента, который наследуется от соответствующего типа встроенного элемента.Пользовательские атрибуты дадут авторам HTML-first библиотек более удобный способ связывания атрибутов с элементами и возможность реагирования на события жизненного цикла. Кроме того, это способ расширять стандартные элементы.
Вместе с полифилом есть ссылка на эксплейнер, в котором подробнее описан предлагаемый API. Также есть страницы с описанием мотивации и дизайна API. Я заинтересован и буду ждать появления прототипа в Firefox.
#html #js #web_api
👍4🤝1
Images Preview
На некоторых сайтах, где надо показывать изображения в высоком качестве, для UX применяются изображения-заполнители низкого качества. Они дают представление о некоторых деталях и цветах оригинала, пока тот загружается.
Эта техника также известна как LQIP (Low Quality Image Placeholder) и есть разные способы реализации, даже с помощью CSS. Но чаще это специально подготовленное изображение, встроенное как фон в виде data URL в Base64.
Пользователь сразу видит размытое изображение, которое даёт представление об оригинальном изображении, которое тем временем загружается в фоне. Когда оно будет загружено, содержимое
Патрик Броссет предлагает реализовать этот шаблон на уровне веб-платформы. Он подготовил эксплейнер и описал в статье как это решается сегодня и предлагаемое им решение. Обратную связь можно оставить в репозитории предложения.
К
Алгоритм у браузера следующий:
- Загрузить оригинал и заполнитель, отдавая более высокий приоритет оригиналу;
- Отобразить заполнитель;
- Заменить заполнитель на оригинал, когда тот загрузится и будет готов.
Если оригинал по каким-то причинам будет загружен раньше заполнителя, то он будет показан. Заполнитель можно встроить как Base64. Если браузер не поддерживает атрибут, то всё будет работать как обычно, а фон можно использовать как фоллбэк.
Атрибут
Кроме того, так как это атрибут в HTML, его можно тесно интегрировать с парсером и сканером предварительной загрузки. Тогда сетевой процесс браузера сможет менять приоритеты в зависимости от ситуации и применять прочие оптимизации загрузки.
Интересно, что в Netscape, IE4 и ранних Firefox был нестандартный атрибут
#html
На некоторых сайтах, где надо показывать изображения в высоком качестве, для UX применяются изображения-заполнители низкого качества. Они дают представление о некоторых деталях и цветах оригинала, пока тот загружается.
Эта техника также известна как LQIP (Low Quality Image Placeholder) и есть разные способы реализации, даже с помощью CSS. Но чаще это специально подготовленное изображение, встроенное как фон в виде data URL в Base64.
<img
src="high-quality.webp"
style="background-image: url(
data:image/png;base64,...
)"
width="..."
height="..."
alt="..."
>
<img> — замещаемый элемент, во время загрузки изображения из src это обычный контейнер, которому можно задать фон в CSS. Небольшое размытое изображение встроено как Base64 в HTML, чтобы не тратить время на загрузку.Пользователь сразу видит размытое изображение, которое даёт представление об оригинальном изображении, которое тем временем загружается в фоне. Когда оно будет загружено, содержимое
<img> замещается и фон перестаёт работать.Патрик Броссет предлагает реализовать этот шаблон на уровне веб-платформы. Он подготовил эксплейнер и описал в статье как это решается сегодня и предлагаемое им решение. Обратную связь можно оставить в репозитории предложения.
Дисклеймер: это обзор раннего предложения новых функций. Синтаксис может измениться в будущем или от функций могут отказаться
К
<img> предлагается добавить атрибут previewsrc для изображения-заполнителя, которое будет отображаться до загрузки основного изображения из src. Атрибут никак не меняет работу src, srcset, sizes и loading.<img
previewsrc="low-quality.png"
src="high-quality.webp"
width="..."
height="..."
alt="..."
>
Алгоритм у браузера следующий:
- Загрузить оригинал и заполнитель, отдавая более высокий приоритет оригиналу;
- Отобразить заполнитель;
- Заменить заполнитель на оригинал, когда тот загрузится и будет готов.
Если оригинал по каким-то причинам будет загружен раньше заполнителя, то он будет показан. Заполнитель можно встроить как Base64. Если браузер не поддерживает атрибут, то всё будет работать как обычно, а фон можно использовать как фоллбэк.
Атрибут
previewsrc лишь удобный способ указать заполнитель. Браузер не делает никаких трансформаций и не создаёт сам заполнитель, это отдельная задача, всё ещё лежащая на плечах разработчиков. С атрибутом будет меньше хаков.Кроме того, так как это атрибут в HTML, его можно тесно интегрировать с парсером и сканером предварительной загрузки. Тогда сетевой процесс браузера сможет менять приоритеты в зависимости от ситуации и применять прочие оптимизации загрузки.
Интересно, что в Netscape, IE4 и ранних Firefox был нестандартный атрибут
lowsrc, который делал то же самое. Это устаревший атрибут, ни один современный браузер его не поддерживает. Однако в DOM API все ещё можно читать и записывать свойство lowsrc, хотя это ни к чему не приводит.#html
👍15👀2🤝2🥴1
You don’t know HTML: SGML, HTML, XML, XHTML
За свою историю язык разметки гипертекста HTML был как производным продуктом на базе другого языка, вобрав в себя его идеи и синтаксис, так и прародителем новых языков. В итоге HTML стал независимым и самостоятельным языком.
HTML изначально создан как приложение SGML. Тим Бернерс-Ли при проектировании нового языка для будущего Интернета не стал изобретать велосипед, а взял за основу уже существующий и стандартизированный язык SGML и его диалект CERN SGML.
Сам SGML — это метаязык для создания других языков разметки компьютерного текста, независимого от устройства. SGML определяет синтаксис и правила, а по ним описывается конкретный набор элементов, атрибутов и правил их применения.
Суть SGML в том, чтобы описывать типы документов (DTD), в которых определён список доступных элементов, атрибутов и специальных сущностей, обязательность или необязательность открывающих и закрывающих тэгов, правила вложенности.
Первые версии HTML были конкретным типом документа SGML. Это отражалось в длинной конструкции
Такая конструкция была на страницах до HTML5 и простого
SGML, с одной стороны, был достаточно простым языком в плане синтаксиса. С другой стороны, он был крайне сложным по части парсинга. Из-за этого браузерам того времени было сложно обрабатывать документы на SGML-языках.
В ответ на сложность SGML появился XML. Цель та же: универсальный метаязык для описания языков разметки. Разница в более строгом синтаксисе, упрощённых правилах парсинга и удалении сложных и редко используемых возможностей SGML.
XML стал новым стандартом, на его основе были созданы различные языки разметки. Возникла идея «переписать» HTML с SGML на XML. Так появился стандарт XHTML, первая версия которого — перенос HTML 4 с SGML на строгий синтаксис XML.
XHTML 1.0 не добавлял ничего нового, а делал HTML 4 строже. Пропущенный тэг, недопустимое значение атрибута, отсутсвие кавычек или неверная вложенность делают весь документ невалидным, прекращая парсинг и отображая ошибку.
За XHTML 1.0 последовала версия 1.1 с идеей разбивки на модули. Затем вышла версия 2.0 с полным переосмыслением идей HTML. XHTML 2.0 был новым языком для веба, который должен был заменить существовавший на то время HTML 4.
XHTML 2.0 не был совместим ни с HTML 4, ни с XHTML 1.0/1.1, которые поддерживали браузеры. Такое радикальное изменение ставило под угрозу совместимость веба, а потенциальная польза и затраты усилий на внедрение не стоили того.
От XHTML 2.0 отказались и было принято решение развивать уже существующий HTML 4 менее радикально. От строго XML синтаксиса тоже отказались в пользу более толерантного к ошибкам, похожего на SGML. Итогом стал HTML 5.
Для HTML 5 был разработан собственный парсер, который проще, чем у SGML, но не такой строгий, как у XML. С тех пор HTML стал отдельным самостоятельным языком, который не совместим ни с SGML, ни с XML, но впитал части от обоих.
Сегодня HTML развивается как живой стандарт без номера версий (больше нет никакого HTML 5) и сохраняет толерантный к ошибкам парсер. Даже самая невалидная разметка будет как-то разобрана, ляжет в основу DOM и отобразится в браузере.
#ydkhtml
За свою историю язык разметки гипертекста HTML был как производным продуктом на базе другого языка, вобрав в себя его идеи и синтаксис, так и прародителем новых языков. В итоге HTML стал независимым и самостоятельным языком.
HTML изначально создан как приложение SGML. Тим Бернерс-Ли при проектировании нового языка для будущего Интернета не стал изобретать велосипед, а взял за основу уже существующий и стандартизированный язык SGML и его диалект CERN SGML.
Сам SGML — это метаязык для создания других языков разметки компьютерного текста, независимого от устройства. SGML определяет синтаксис и правила, а по ним описывается конкретный набор элементов, атрибутов и правил их применения.
Суть SGML в том, чтобы описывать типы документов (DTD), в которых определён список доступных элементов, атрибутов и специальных сущностей, обязательность или необязательность открывающих и закрывающих тэгов, правила вложенности.
Первые версии HTML были конкретным типом документа SGML. Это отражалось в длинной конструкции
DOCTYPE с ссылкой на DTD, где описываются все доступные HTML-элементы, правила вложенности и атрибуты.<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
Такая конструкция была на страницах до HTML5 и простого
<!DOCTYPE html>. Она означала «это размеченный с помощью SGML документ типа HTML, определение которого находится по такому-то адресу».SGML, с одной стороны, был достаточно простым языком в плане синтаксиса. С другой стороны, он был крайне сложным по части парсинга. Из-за этого браузерам того времени было сложно обрабатывать документы на SGML-языках.
В ответ на сложность SGML появился XML. Цель та же: универсальный метаязык для описания языков разметки. Разница в более строгом синтаксисе, упрощённых правилах парсинга и удалении сложных и редко используемых возможностей SGML.
XML стал новым стандартом, на его основе были созданы различные языки разметки. Возникла идея «переписать» HTML с SGML на XML. Так появился стандарт XHTML, первая версия которого — перенос HTML 4 с SGML на строгий синтаксис XML.
XHTML 1.0 не добавлял ничего нового, а делал HTML 4 строже. Пропущенный тэг, недопустимое значение атрибута, отсутсвие кавычек или неверная вложенность делают весь документ невалидным, прекращая парсинг и отображая ошибку.
За XHTML 1.0 последовала версия 1.1 с идеей разбивки на модули. Затем вышла версия 2.0 с полным переосмыслением идей HTML. XHTML 2.0 был новым языком для веба, который должен был заменить существовавший на то время HTML 4.
XHTML 2.0 не был совместим ни с HTML 4, ни с XHTML 1.0/1.1, которые поддерживали браузеры. Такое радикальное изменение ставило под угрозу совместимость веба, а потенциальная польза и затраты усилий на внедрение не стоили того.
От XHTML 2.0 отказались и было принято решение развивать уже существующий HTML 4 менее радикально. От строго XML синтаксиса тоже отказались в пользу более толерантного к ошибкам, похожего на SGML. Итогом стал HTML 5.
Для HTML 5 был разработан собственный парсер, который проще, чем у SGML, но не такой строгий, как у XML. С тех пор HTML стал отдельным самостоятельным языком, который не совместим ни с SGML, ни с XML, но впитал части от обоих.
Сегодня HTML развивается как живой стандарт без номера версий (больше нет никакого HTML 5) и сохраняет толерантный к ошибкам парсер. Даже самая невалидная разметка будет как-то разобрана, ляжет в основу DOM и отобразится в браузере.
#ydkhtml
❤9👍5🤝1
Управление маркерами CSS-каруселей
После релиза CSS-каруселей в Chrome 135 перед Google I/O в прошлом году и критики доступности, новостей о развитии этого API как-то не было. Но в Chrome 140 и Chrome 154 (и соответствующих версиях Edge) CSS-карусели были немного улучшены.
Я делал пост о CSS-каруселях. Напомню, что это возможность добавить контейнеру с прокруткой кнопки «вперёд» и «назад», а также группу с маркерами (пагинацию) с помощью CSS. В сочетании со Scroll Snap получаются карусели без JS.
Одна из претензий к CSS-каруселям — создание интерактивных элементов в CSS. Кнопки, маркеры и их обёртка создаются в виде интерактивных псевдо-элементов что не даёт полного контроля и принуждает использовать
При просмотре в DevTools у карусели из примера выше получается такая структура:
Внутри списка создаются псевдо-элементы
Эту структуру с псевдо-элементами можно выразить в виде HTML-эквивалента для понимания итоговой семантики и связей элементов. Кнопки выносятся перед списком слайдов, а контейнер с маркерами в виде
По умолчанию контейнер с маркерами получает роль
Значения
-
-
Контейнер меняет роль с
Второй способ управления группой маркеров — возможность выноса в HTML. У элемента внутри карусели можно указать свойство
Чтобы это работало аналогично группе в CSS, внутри контейнера должны быть якорные ссылки, которые станут аналогом псевдо-элементов
По большей части это предназначено для интерактивного содержания, а не карусели. Маркеры по шаблону карусели должны быть сочетанием
В теории это можно решить с помощью
Теперь есть как минимум два способа управления клавиатурной навигацией и семантикой контейнера с маркерами. Это хорошо, но всё ещё к интерактивным псевдо-элементам в CSS остаются вопросы. Идея использовать реальные кнопки звучит лучше.
#css #ui
После релиза CSS-каруселей в Chrome 135 перед Google I/O в прошлом году и критики доступности, новостей о развитии этого API как-то не было. Но в Chrome 140 и Chrome 154 (и соответствующих версиях Edge) CSS-карусели были немного улучшены.
Я делал пост о CSS-каруселях. Напомню, что это возможность добавить контейнеру с прокруткой кнопки «вперёд» и «назад», а также группу с маркерами (пагинацию) с помощью CSS. В сочетании со Scroll Snap получаются карусели без JS.
<ul class="carousel">
<li data-slide="1">
<!-- Слайд 1 -->
</li>
<li data-slide="2">
<!-- Слайд 2 -->
</li>
<li data-slide="3">
<!-- Слайд 3 -->
</li>
</ul>
<style>
.carousel {
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-marker-group: after;
scrollbar-width: none;
[data-slide] {
scroll-snap-align: center;
}
[data-slide]::scroll-marker {
/* Резервное значение */
content: attr(data-slide);
content: sibling-index();
}
[data-slide]::scroll-marker:target-current {
background: var(--accent);
}
&::scroll-button(left) {
content: "←" / "Previous";
}
&::scroll-button(right) {
content: "→" / "Next";
}
&::scroll-button(*):focus-visible {
outline-offset: 5px;
}
}
</style>
Одна из претензий к CSS-каруселям — создание интерактивных элементов в CSS. Кнопки, маркеры и их обёртка создаются в виде интерактивных псевдо-элементов что не даёт полного контроля и принуждает использовать
content в CSS.При просмотре в DevTools у карусели из примера выше получается такая структура:
<ul class="carousel">
::scroll-button(left)
::scroll-button(right)
::scroll-marker-group
<li data-slide="1">
::scroll-marker
<!-- Слайд 1 -->
</li>
<li data-slide="2">
::scroll-marker
<!-- Слайд 2 -->
</li>
<li data-slide="3">
::scroll-marker
<!-- Слайд 3 -->
</li>
</ul>
Внутри списка создаются псевдо-элементы
::scroll-button(*) — кнопки «вперёд» и «назад», ::scroll-marker-group — контейнер для маркеров с ролью navigation и ::scroll-marker — сами маркеры с ролью link.Эту структуру с псевдо-элементами можно выразить в виде HTML-эквивалента для понимания итоговой семантики и связей элементов. Кнопки выносятся перед списком слайдов, а контейнер с маркерами в виде
<nav> и <a> после списка.<button
aria-label="Previous"
aria-controls="[ссылается на <ul>]"
>
←
</button>
<button
aria-label="Next"
aria-controls="[ссылается на <ul>]"
>
→
</button>
<ul class="carousel">
<li data-slide="1">
<!-- Слайд 1 -->
</li>
<li data-slide="2">
<!-- Слайд 2 -->
</li>
<li data-slide="3">
<!-- Слайд 3 -->
</li>
</ul>
<nav aria-controls="[ссылается на <ul>]">
<a
href="[ссылается на <li data-slide='1'>]"
aria-details="[ссылается на <li data-slide='1'>]"
>
1
</a>
<a
href="[ссылается на <li data-slide='2'>]"
aria-details="[ссылается на <li data-slide='2'>]"
>
2
</a>
<a
href="[ссылается на <li data-slide='3'>]"
aria-details="[ссылается на <li data-slide='3'>]"
>
3
</a>
</nav>
По умолчанию контейнер с маркерами получает роль
navigation, а сами маркеры — роль link. Работает это, соответственно, как <nav> с набором <a> внутри. Изменить это поведение можно с помощью новых значений scroll-marker-group.Значения
links и tabs идут в дополнение к уже существующим значениям before и after, которые определяют визуальное положение группы маркеров. links и tabs определяют тип группы:-
links: у контейнера маркеров будет роль navigation, у маркеров будет роль link и они будут вести себя как якорные ссылки, табуляция по ссылкам будет последовательная;-
tabs: у контейнера маркеров будет роль tablist, у маркеров будет роль tab и они будут вести себя как вкладки, табуляция попадает только на активную вкладку, а навигация между вкладками с помощью стрелок.<ul class="carousel">
<li data-slide="1">
<!-- Слайд 1 -->
</li>
<li data-slide="2">
<!-- Слайд 2 -->
</li>
<li data-slide="3">
<!-- Слайд 3 -->
</li>
</ul>
<style>
.carousel {
/* ... */
scroll-marker-group: after tabs;
}
</style>
<!-- Итоговая структура в HTML-эквиваленте -->
<ul class="carousel">
<li data-slide="1">
<!-- Слайд 1 -->
</li>
<li data-slide="2">
<!-- Слайд 2 -->
</li>
<li data-slide="3">
<!-- Слайд 3 -->
</li>
</ul>
<div
role="tablist"
aria-multiselectable="false"
aria-orientation="horizontal"
aria-controls="[ссылается на <ul>]"
>
<button
role="tab"
aria-selected="true"
aria-details="[ссылается на <li data-slide='1'>]"
>
1
</button>
<button
role="tab"
aria-selected="false"
aria-details="[ссылается на <li data-slide='2'>]"
>
2
</button>
<button
role="tab"
aria-selected="false"
aria-details="[ссылается на <li data-slide='3'>]"
>
3
</button>
</div>
Контейнер меняет роль с
navigation на tablist, а маркеры с link на tab. Так как tablist — это виджет, который требует клавиатурной навигации, браузер автоматически добавляет навигацию стрелками между маркерами.Второй способ управления группой маркеров — возможность выноса в HTML. У элемента внутри карусели можно указать свойство
scroll-target-group: auto и тогда он будет использоваться в качестве контейнера для маркеров.Чтобы это работало аналогично группе в CSS, внутри контейнера должны быть якорные ссылки, которые станут аналогом псевдо-элементов
::scroll-marker. Для них браузер автоматически вычислит состояние :target-current.<div class="carousel">
<div id="slide-1">
<!-- Слайд 1 -->
</div>
<div id="slide-2">
<!-- Слайд 2 -->
</div>
<div id="slide-3">
<!-- Слайд 3 -->
</div>
<nav class="markers">
<a href="#slide-1">1</a>
<a href="#slide-2">2</a>
<a href="#slide-3">3</a>
</nav>
</div>
<style>
.carousel {
/* ... */
.markers {
scroll-target-group: auto;
a:target-current {
background-color: var(--accent);
}
}
}
</style>
По большей части это предназначено для интерактивного содержания, а не карусели. Маркеры по шаблону карусели должны быть сочетанием
tablist и tab. Если вместо ссылок будут другие элементы, то автоматика браузера не сработает.В теории это можно решить с помощью
focusgroup и ARIA, добавив клавиатурное поведение и семантику. Но от встроенного решения я жду отсутствия необходимости указывать какие-либо атрибуты ARIA для доступности.<div class="carousel" id="carousel">
<!-- Слайды -->
<nav
class="markers"
focusgroup="tablist"
aria-controls="carousel"
>
<a
href="#slide-1"
role="tab"
aria-controls="slide-1"
aria-details="slide-1"
>
1
</a>
<!-- остальные маркеры -->
</nav>
</div>
Теперь есть как минимум два способа управления клавиатурной навигацией и семантикой контейнера с маркерами. Это хорошо, но всё ещё к интерактивным псевдо-элементам в CSS остаются вопросы. Идея использовать реальные кнопки звучит лучше.
#css #ui
👍5❤2🤝1
19½ вещей, которые вы не знали о доступности в HTML и CSS
24 сентября прошло мероприятие Inclusive Design 24, на котором транслировались доклады от экспертов доступности. Хочу поделиться докладом «19½ things you didn’t know about accessibility in HTML and CSS» от Мануэля Матузовича.
- Запись доклада на YouTube
- Текстовая расшифровка слайдов
- Плейлист со всеми докладами
Мануэль рассказывает о проблеме
#a11y #html #css
24 сентября прошло мероприятие Inclusive Design 24, на котором транслировались доклады от экспертов доступности. Хочу поделиться докладом «19½ things you didn’t know about accessibility in HTML and CSS» от Мануэля Матузовича.
- Запись доклада на YouTube
- Текстовая расшифровка слайдов
- Плейлист со всеми докладами
Мануэль рассказывает о проблеме
fixed элементов при зуме страницы, семантике и клавиатурной навигации поповеров, имени SVG-иконок, разнице aria-hidden="true" и role="presentation", вложенности и сбросе семантики, <geolocation>, focusgroup, reading-flow и headingoffset.#a11y #html #css
YouTube
Manuel Matuzović / 19½ Things You Didn’t Know about Accessibility in HTML and CSS #id24 2026
Manuel skips the "Alt Text" lectures and dives straight into the deep end, exploring the hidden mechanics of the Popover API, keyboard navigation, and ARIA.
About the speaker
Manuel is a freelance frontend developer, accessibility auditor, teacher, author…
About the speaker
Manuel is a freelance frontend developer, accessibility auditor, teacher, author…
❤8🤗2
Стилизация тултипов
Два года назад я писал о проблеме с
Общая практика такова, что стоит избегать использования атрибута
Свойство
Это решает многие проблемы с тултипом
- Появляется при наведении и фокусе;
- Стилизуется как обычный псевдо-элемент;
- Задержка появления контролируется свойством
- Реагирует на изменение размера шрифта;
- Закрывается при нажатии
- Можно навести курсор на контент.
Но некоторые проблемы доступности не решаются. Вспомогательные технологии работают с
В качестве альтернативы предлагается новый атрибут
С точки зрения доступности рекомендации для стилизуемых тултипов выглядят так:
- Не переопределяют имя элемента;
- Встроенная роль
- Элемент ссылается на тултип через
Но есть и другой способ создания тултипов — сочетание уже существующих Popover API и Interest Invokers. Вместо атрибута можно использовать элемент, связать его с триггером через Anchor Positioning и добавить поведение через атрибуты:
В свежем 547 выпуске Веб-стандартов обсуждалась статья Патрика. Ведущие задались вопросом: зачем нам два разных способа создания тултипов, когда Popover и Interest Invokers решают задачу? Я вижу это как решения разных задач.
Простые задачи должны решаться просто. Атрибут
В более сложных сценариях, когда нужны тултипы с HTML или тонкий контроль, в дело вступают Interest Invokers. Но с ними нужно больше повозиться, чтобы сделать хорошо. Часть магии браузера не будет автоматически применяться.
Поэтому нужно вручную:
- Добавить отдельный элемент с
- Позиционировать тултип относительно триггера через Anchor Positioning;
- Добавить триггеру
- Стилизовать тултип.
Это немного больше работы, чем просто указать текст в атрибуте и стилизовать в CSS через псевдо-элемент. Поэтому оба подхода уместны на мой взгляд. Патрик пишет, что даже стили остаются, меняется только селектор.
Можно оставить обратную связь по предложению Edge.
#html #css #ui
Два года назад я писал о проблеме с
title и о принятом рабочей группой CSS предложении добавить псевдо-элемент ::tooltip от Лии Веру. Похоже, команда Edge займётся тултипами, о чём можно судить по статье Патрика Броссета.Дисклеймер: это обзор раннего предложения новых функций. Синтаксис может измениться в будущем или от функций могут отказаться
Общая практика такова, что стоит избегать использования атрибута
title из-за ряда проблем. Патрик описывает эти проблемы в статье. Текущее решение команды Edge — добавить опциональный «сброс», как это сделано в стилизуемом <select>:<style>
/*
Новый псевдо-элемент,
выбирает все тултипы
*/
::tooltip {
/* Включение стилизации */
appearance: base;
/* Стили тултипа */
}
</style>
<button title="Это тултип">
Наведи на меня
</button>
Свойство
appearance: base включает стилизацию. Системный тултип заменяется на псевдо-элемент с базовыми стилями. Он привязан к элементу с помощью Anchor Positioning и работает под капотом на основе Popover API и Interest Invokers.Это решает многие проблемы с тултипом
title:- Появляется при наведении и фокусе;
- Стилизуется как обычный псевдо-элемент;
- Задержка появления контролируется свойством
interest-delay;- Реагирует на изменение размера шрифта;
- Закрывается при нажатии
Esc;- Можно навести курсор на контент.
Но некоторые проблемы доступности не решаются. Вспомогательные технологии работают с
title непоследовательно. Кроме того, в сообществе есть стойкое убеждение, что title это плохо и его не стоит использовать.В качестве альтернативы предлагается новый атрибут
tooltip. Он работает как title, но без отображения системного тултипа, с определённой семантикой и связями и ограничен <button>, <a>, <input>, <textarea>, <select>:<style>
::tooltip {
/* Стили тултипа */
}
</style>
<button tooltip="Это тултип">
Наведи на меня
</button>
С точки зрения доступности рекомендации для стилизуемых тултипов выглядят так:
- Не переопределяют имя элемента;
- Встроенная роль
tooltip;- Элемент ссылается на тултип через
aria-describedby.Но есть и другой способ создания тултипов — сочетание уже существующих Popover API и Interest Invokers. Вместо атрибута можно использовать элемент, связать его с триггером через Anchor Positioning и добавить поведение через атрибуты:
<!--
interestfor связывает кнопку
с тултипом и отображает
при наведении и фокусе
-->
<button
interestfor="my-tooltip"
aria-describedby="my-tooltip"
>
Наведи на меня, у меня есть тултип с HTML
</button>
<!--
popover делает элемент
всплывающим и позволяет
добавить любой контент
-->
<div
id="my-tooltip"
popover
role="tooltip"
>
Это тултип с <span>HTML</span>
</div>
В свежем 547 выпуске Веб-стандартов обсуждалась статья Патрика. Ведущие задались вопросом: зачем нам два разных способа создания тултипов, когда Popover и Interest Invokers решают задачу? Я вижу это как решения разных задач.
Простые задачи должны решаться просто. Атрибут
title/tooltip и псевдо-элемент ::tooltip как раз для простых задач, где нужно вывести короткий текст или подпись к элементу. Браузер сам сделает магию связывания и доступности.В более сложных сценариях, когда нужны тултипы с HTML или тонкий контроль, в дело вступают Interest Invokers. Но с ними нужно больше повозиться, чтобы сделать хорошо. Часть магии браузера не будет автоматически применяться.
Поэтому нужно вручную:
- Добавить отдельный элемент с
popover, id и role;- Позиционировать тултип относительно триггера через Anchor Positioning;
- Добавить триггеру
interestfor и aria-describedby;- Стилизовать тултип.
Это немного больше работы, чем просто указать текст в атрибуте и стилизовать в CSS через псевдо-элемент. Поэтому оба подхода уместны на мой взгляд. Патрик пишет, что даже стили остаются, меняется только селектор.
Можно оставить обратную связь по предложению Edge.
#html #css #ui
Patrick Brosset
Patrick - Opening the can of tooltips
Tooltips seem simple, until you look closer. The title attribute can't be styled and isn't accessible to everyone, while custom tooltips come with plenty of complexity. What would it take to build a native tooltip that's customizable, accessible, and easy…
👍9❤2🤝1
Замена циклов и рекурсии на Declarative Partial Updates
Дэйв Руперт написал в своём блоге статью с интригующим названием «Recursion is DEAD and HTML killed it». В ней рассмотрен интересный способ использования новой функции Declarative Partial Updates. Что это такое — я описывал в отдельном посте.
Declarative Partial Updates предназначены для потоковой передачи фрагментов HTML, которые браузер перемещает из любого произвольного места в DOM в конкретные, именованные области, которые отмечены специальными маркерами.
Дейв искал другие варианты применения и вспомнил про циклы и рекурсию. Иногда встречается иерархический контент в виде дерева, когда элементы вложены друг в друга. Глубина вложенности может быть произвольной и меняться со временем.
- Меню с вложенными подменю;
- Комментарии к посту с ответами;
- Ветка сообщений, как в Reddit или Bluesky;
- Структура директорий и файлов;
- Категории с подкатегориями в интернет-магазине;
- Компонент древовидного списка (tree) фильтров;
- И так далее…
Наивный и не очень оптимальный способ отрисовки такого контента — вложенные циклы. Это работает, когда количество уровней вложенности известно заранее и оно небольшое. Однако циклы прописаны в коде, что лишает решение гибкости.
Изменение уровня вложенности потребует изменения кода. Более гибкое решение — рекурсивная функция, которая проверяет наличие следующего уровня и вызывает сама себя. Главное правильное условие выхода из рекурсии.
Для цикла и рекурсии нужны данные в виде дерева. Исходные данные обычно представлены в виде плоского списка, где сущности связаны через идентификаторы. Такую структуру нужно преобразовать в дерево, что может быть затратно.
Благодаря Declarative Partial Updates можно вместо подготовки данных и вывода HTML с помощью рекурсии, сохранить плоский список и по нему вывести HTML, а через маркеры с идентификаторами указать, куда нужно переместить фрагменты.
В результате будет сгенерирован HTML с плоским списком комментариев после
Разметка, отданная браузеру:
В итоге эта разметка будет автоматически преобразована в древовидную структуру:
#html #web_api
Дэйв Руперт написал в своём блоге статью с интригующим названием «Recursion is DEAD and HTML killed it». В ней рассмотрен интересный способ использования новой функции Declarative Partial Updates. Что это такое — я описывал в отдельном посте.
Declarative Partial Updates предназначены для потоковой передачи фрагментов HTML, которые браузер перемещает из любого произвольного места в DOM в конкретные, именованные области, которые отмечены специальными маркерами.
Дейв искал другие варианты применения и вспомнил про циклы и рекурсию. Иногда встречается иерархический контент в виде дерева, когда элементы вложены друг в друга. Глубина вложенности может быть произвольной и меняться со временем.
- Меню с вложенными подменю;
- Комментарии к посту с ответами;
- Ветка сообщений, как в Reddit или Bluesky;
- Структура директорий и файлов;
- Категории с подкатегориями в интернет-магазине;
- Компонент древовидного списка (tree) фильтров;
- И так далее…
Наивный и не очень оптимальный способ отрисовки такого контента — вложенные циклы. Это работает, когда количество уровней вложенности известно заранее и оно небольшое. Однако циклы прописаны в коде, что лишает решение гибкости.
Изменение уровня вложенности потребует изменения кода. Более гибкое решение — рекурсивная функция, которая проверяет наличие следующего уровня и вызывает сама себя. Главное правильное условие выхода из рекурсии.
Для цикла и рекурсии нужны данные в виде дерева. Исходные данные обычно представлены в виде плоского списка, где сущности связаны через идентификаторы. Такую структуру нужно преобразовать в дерево, что может быть затратно.
Благодаря Declarative Partial Updates можно вместо подготовки данных и вывода HTML с помощью рекурсии, сохранить плоский список и по нему вывести HTML, а через маркеры с идентификаторами указать, куда нужно переместить фрагменты.
/* Получение всех комментариев */
const comments = db.comments.find(
c => c.post_id === post.id
);
const Comment = (c) => {
const parent_id = c.parent_id ?? c.post_id
return html`
<template for="comment-${parent_id}">
<div id="comment-${c.id}">
<div>${c.username}</div>
<div>${c.body}</div>
<?marker name="comment-${c.id}"?>
</div>
<?marker name="comment-${parent_id}">
</template>
`;
}
const Main = (p) => {
return html`
<main class="post">
${p.content}
</main>
<aside class="comments">
<?marker name="comment-${p.id}"?>
</aside>
${comments.forEach(c =>
Comment(c)
)}
`;
}
Main(post);
В результате будет сгенерирован HTML с плоским списком комментариев после
<aside>. Браузер переместит элементы <template for="..."> на места маркеров с соответствующим значением атрибута name и сформирует нужную иерархию.Разметка, отданная браузеру:
<main class="post">
<!-- ... -->
</main>
<aside class="comments">
<?marker name="comment-123"?>
</aside>
<template for="comment-123">
<div id="comment-123-1">
<!-- ... -->
<?marker name="comment-123—1"?>
</div>
<?marker name="comment-123"?>
</template>
<template for="comment-123">
<div id="comment-123-2">
<!-- ... -->
<?marker name="comment-123—2"?>
</div>
<?marker name="comment-123"?>
</template>
<template for="comment-123-1">
<div id="comment-123-1-1">
<!-- ... -->
<?marker name="comment-123—1-1"?>
</div>
<?marker name="comment-123-1"?>
</template>
<template for="comment-123-1-1">
<div id="comment-123-1-1-1">
<!-- ... -->
<?marker name="comment-123—1-1-1"?>
</div>
<?marker name="comment-123-1-1"?>
</template>
<template for="comment-123-2">
<div id="comment-123-2-1">
<!-- ... -->
<?marker name="comment-123—2-1"?>
</div>
<?marker name="comment-123-2"?>
</template>
В итоге эта разметка будет автоматически преобразована в древовидную структуру:
<main class="post">
<!-- ... -->
</main>
<aside class="comments">
<div id="comment-123-1">
<!-- ... -->
<div id="comment-123-1-1">
<!-- ... -->
<div id="comment-123-1-1-1">
<!-- ... -->
<?marker name="comment-123—1-1-1"?>
</div>
<?marker name="comment-123-1-1"?>
</div>
<?marker name="comment-123-1"?>
</div>
<div id="comment-123-2">
<!-- ... -->
<div id="comment-123-2-1">
<!-- ... -->
<?marker name="comment-123—2-1"?>
</div>
<?marker name="comment-123-2"?>
</div>
<?marker name="comment-123"?>
</aside>
#html #web_api
Dave
Recursion is DEAD and HTML killed it
There’s a new web platform feature flighting in Chromium called Declarative Partial Updates. This proposal adds a new <?marker> element which enables out-of-order streaming of content. Rather than the traditional way of building the HTML you need on the server…
🤯5🔥4❤1🤝1
Продвинутые постеры для видео
У элемента
Атрибут достаточно полезный:
- Маскирует пустой плеер, пока первый кадр видео не загружен, что уменьшает вспышки контента;
- Задаёт нужный кадр или специально нарисованную обложку вместо первого кадра видео;
- Уменьшает LCP в сочетании с
При всей пользе возможности настройки постера ограничены только ссылкой на изображение. Тем временем у
-
-
-
-
-
-
- и другие атрибуты, включая те, что могут появиться в будущем, например для заполнителя.
А если
У видео только одинокий
Для этого предлагается новый элемент
Это позволит использовать весь арсенал возможностей
Скот с командой из Squarespace в рамках хакатона подготовили предложение, представили его на заседаниях WHATWG, открыли запрос в стандарт HTML, получили отзывы и сейчас работают над тестами веб-платформы для дальнейшего внедрения.
В Mozilla уже высказались позитивно и готовы внедрять. С учётом предыдущего успешного опыта с
#html
У элемента
<video> есть возможность указать атрибут poster с ссылкой на изображение. Оно будет отображаться на месте <video>, пока видео не начнёт проигрываться или пока не загрузится первый кадр.Атрибут достаточно полезный:
- Маскирует пустой плеер, пока первый кадр видео не загружен, что уменьшает вспышки контента;
- Задаёт нужный кадр или специально нарисованную обложку вместо первого кадра видео;
- Уменьшает LCP в сочетании с
<link rel="preload" as="image"> для видео в начальной области просмотра.При всей пользе возможности настройки постера ограничены только ссылкой на изображение. Тем временем у
<img> есть:-
srcset и sizes для выбора из списка подходящего под текущий экрана изображения;-
loading="lazy" для отложенной загрузки при появлении в области просмотра;-
fetchpriority для управления приоритетом загрузки;-
decoding для переключения между синхронным и асинхронным декодированием;-
width и height для ранней подсказки соотношения сторон изображения, чтобы уменьшать CLS;-
alt для указания текстового описания изображения;- и другие атрибуты, включая те, что могут появиться в будущем, например для заполнителя.
А если
<img> совместить с <picture> и <source>, то можно использовать медиа-запросы для отображения разных версий изображения в зависимости от условий и подключать разные форматы файлов в зависимости от браузерной поддержки.У видео только одинокий
poster. Скот Йель со своей командой намерены это исправить. После успешного внедрения атрибута loading для ленивой загрузки <video> и <audio> в стандарт, а затем в браузеры, они хотят улучшить постеры.Дисклеймер: это обзор раннего предложения новых функций. Синтаксис может измениться в будущем или от функций могут отказаться
Для этого предлагается новый элемент
<poster>, который будет разрешён внутри <video>. В сам <poster> вкладывается элемент <img> с опциональной обёрткой <picture>. Изображение из <img> внутри <poster> станет постером видео.<video
src="skate-clip.mp4"
loading="lazy"
>
<poster>
<picture>
<source
srcset="skater-9x16.avif"
type="image/avif"
>
<img
src="skater-9x16.jpg"
alt="A skateboarder slides down a handrail above a concrete staircase."
>
</picture>
</poster>
</video>
Это позволит использовать весь арсенал возможностей
<img> и <picture> для тонкой настройки постеров, включая возможность указать альтернативный текст с описанием для вспомогательных технологий.Скот с командой из Squarespace в рамках хакатона подготовили предложение, представили его на заседаниях WHATWG, открыли запрос в стандарт HTML, получили отзывы и сейчас работают над тестами веб-платформы для дальнейшего внедрения.
В Mozilla уже высказались позитивно и готовы внедрять. С учётом предыдущего успешного опыта с
loading для <video> и <audio>, есть все шансы, что скоро это появится в тестовых реализациях за флагами, а там и в стандарт войдёт.#html
Scottjehl
Standards In Progress: Making Video Posters Accessible, Responsive, and an Element | Scott Jehl, Web Designer/Developer
Hot on the heels of recent video standards work, there's a new HTML proposal to make poster images responsive and accessible
👍8🤝1
Выбор предыдущего элемента в CSS
В CSS можно выбрать элемент, который находится сразу же за другим элементом с помощью комбинатора
В некоторых ситуациях эти комбинаторы полезны. Через
А
Иногда хотелось бы инвертированного поведения, то есть выбрать предыдущего прямого соседа или соседа, который на том же уровне вложенности расположен где-то перед элементом. Раньше это было невозможно, но появился
Таким образом теперь можно выбирать как следующие элементы, так и предыдущие. Может показаться, что это не понадобится, но изредка бывает нужно. Например, если нужно подсветить предыдущие элементы перед тем, на который навели указатель:
Или в компоненте формы с несколькими шагами или таймлайна закрасить все шаги, которые идут перед активным на данный момент. Это можно сделать, проставив всем предыдущим шагам классы. А можно сделать без дополнительных классов:
#css
В CSS можно выбрать элемент, который находится сразу же за другим элементом с помощью комбинатора
+. Также можно выбрать элемент, котоый находится где-то после другого элемента на том же уровне вложенности с помощью комбинатора ~.<div class="foo"></div>
<div class="bar"></div>
<div class="baz"></div>
<style>
/* bar сразу после foo */
.foo + .bar {
}
/* baz где-то после foo */
.foo ~ .baz {
}
</style>
В некоторых ситуациях эти комбинаторы полезны. Через
+ можно задать элементу верхний отступ, когда он следует за таким же элементом. Таким образом у первого элемента не будет отступа, так как он ни за кем не следует.А
~ встречается в плавающих подписях и различных CSS-хаках на основе флажков. Также можно скрывать поля формы, если они зависят от выбора значения радио-кнопок, например, разные поля ввода при выборе способа связи телефон/email.Иногда хотелось бы инвертированного поведения, то есть выбрать предыдущего прямого соседа или соседа, который на том же уровне вложенности расположен где-то перед элементом. Раньше это было невозможно, но появился
:has():<div class="foo"></div>
<div class="bar"></div>
<div class="baz"></div>
<style>
/* foo сразу перед bar */
.foo:has(+ .bar) {
}
/* foo где-то перед baz */
.foo:has(~ .baz) {
}
</style>
Таким образом теперь можно выбирать как следующие элементы, так и предыдущие. Может показаться, что это не понадобится, но изредка бывает нужно. Например, если нужно подсветить предыдущие элементы перед тем, на который навели указатель:
.option:has(~ .option:hover) {
background-color: var(--color-hover);
}Или в компоненте формы с несколькими шагами или таймлайна закрасить все шаги, которые идут перед активным на данный момент. Это можно сделать, проставив всем предыдущим шагам классы. А можно сделать без дополнительных классов:
<ol>
<li class="step">
Обрабатываем заказ
<li>
<li class="step">
Готовим заказ
</li>
<li
class="step"
aria-current="step"
>
Курьер везёт заказ
<li>
<li class="step">
Заказ доставлен
</li>
</ol>
<style>
.step:has(~ [aria-current="step"]) {
background-color: var(--color-active);
}
</style>
#css
👍15🤝1
CSS-Only Custom Elements
Бартон Смит в блоге на dev.to написал про CSS-Only Custom Elements, где обобщает подход к созданию пользовательских элементов с использованием только CSS, без классов, методов жизненного цикла, регистрации и без JS как такового.
Я рассказывал в докладе и писал в канале про CSS Web Components, а также писал про слоты без Shadow DOM. Статья о том же подходе. Не всем компонентам нужен JS, а пользовательские элементы допустимо использовать без JS.
Некоторые компоненты лишь отображают своё содержимое в определённом виде, который определяется стилями. У таких компонентов нет функциональности и им не нужен JS для отображения. Обычно это
Компонент бейджа может быть определён как
Селектор типа
В статье дополнительно рассматриваются некоторые особенности:
- установка значения
- доступностью контента, который переставляется местами через
- выбор подходящего селектора для компонента в зависимости от ситуации;
- использование
В блоге есть статья-продолжение: «CSS-Only Custom Elements for Layouts», где показываются примеры кода пользовательских элементов
Будучи создателем инструментов для веб-компонентов, Бартон напоминает о своём проекте CEM Generator, который создаёт Custom Element Manifest на основе кода. Генератор работает и с CSS-компонентами, если они аннотированы.
Custom Elements Manifest — это формат описания метаданных о пользовательских элементах, который можно использовать для всплывающих подсказок в IDE, генерации типов, документации, тестов, линтинга, демо и анализа.
#html #css
Бартон Смит в блоге на dev.to написал про CSS-Only Custom Elements, где обобщает подход к созданию пользовательских элементов с использованием только CSS, без классов, методов жизненного цикла, регистрации и без JS как такового.
Я рассказывал в докладе и писал в канале про CSS Web Components, а также писал про слоты без Shadow DOM. Статья о том же подходе. Не всем компонентам нужен JS, а пользовательские элементы допустимо использовать без JS.
Некоторые компоненты лишь отображают своё содержимое в определённом виде, который определяется стилями. У таких компонентов нет функциональности и им не нужен JS для отображения. Обычно это
<div> с набором классов.<div> можно заменить неопределённым пользовательским элементом с более осмысленным именем. Атрибуты служат для настройки вариантов отображения, а слоты — контрактом для композиции дочерних элементов.Компонент бейджа может быть определён как
<ui-badge>, атрибут variant отвечает за предопределённые варианты, а slot на дочерних элементах помещает их в нужные позиции внутри бейджа. И всё это описано только средствами CSS.Селектор типа
ui-badge используется для описания пользовательских свойств и стилей компонента, а также объединяет под собой связанные стили вариантов и слотов дочерних элементов с помощью стандартной вложенности CSS.<style>
ui-badge {
--badge-padding: 4px;
--badge-bg-color: lightgray;
--badge-fg-color: black;
display: inline-flex;
align-items: center;
gap: 4px;
padding: var(--badge-padding);
background-color: var(--badge-bg-color);
color: var(--badge-fg-color);
border-radius: 4px;
font-size: 0.875rem;
line-height: 1;
&[variant="danger"] {
--badge-bg-color: red;
--badge-fg-color: white;
}
&[variant="success"] {
--badge-bg-color: seagreen;
--badge-fg-color: white;
}
/** Слот для иконки перед текстом */
[slot="icon-start"] {
order: -1;
}
/** Слот для иконки после текста */
[slot="icon-end"] {
order: 1;
}
}
</style>
<!-- Использование -->
<ui-badge variant="danger">
<svg
slot="icon-start"
width="12"
height="12"
aria-hidden="true"
>
<!-- Иконка -->
</svg>
Ошибка оплаты
<svg
slot="icon-end"
width="12"
height="12"
aria-hidden="true"
>
<!-- Иконка -->
</svg>
</ui-badge>
В статье дополнительно рассматриваются некоторые особенности:
- установка значения
display, так как пользовательские элементы по умолчанию inline;- доступностью контента, который переставляется местами через
order и grid-area;- выбор подходящего селектора для компонента в зависимости от ситуации;
- использование
@scope для частичной замены Shadow DOM.В блоге есть статья-продолжение: «CSS-Only Custom Elements for Layouts», где показываются примеры кода пользовательских элементов
<my-flex>, <my-grid>, <my-column> и <my-layout> для декларативной раскладки.Будучи создателем инструментов для веб-компонентов, Бартон напоминает о своём проекте CEM Generator, который создаёт Custom Element Manifest на основе кода. Генератор работает и с CSS-компонентами, если они аннотированы.
/**
* A one-dimensional CSS-only flex layout.
* @attr direction - Flex direction.
* @attr gap - Spacing token.
* @attr {boolean} wrap - Allow children
* to wrap onto multiple lines.
* @cssprop --my-layout-gap - Exact gap
* value override.
*/
my-flex {
/**
* Default spacing token resolved
* by the gap attribute.
*/
--my-layout-gap: 0;
&[direction="row"] {
flex-direction: row;
}
&[direction="column"] {
flex-direction: column;
}
&[wrap] {
flex-wrap: wrap;
}
/* ... */
}
/**
* A documented grid child.
* @attr span - Number of columns to span.
* @attr {string} area - Named grid area.
*/
my-column {
/* ... */
}
Custom Elements Manifest — это формат описания метаданных о пользовательских элементах, который можно использовать для всплывающих подсказок в IDE, генерации типов, документации, тестов, линтинга, демо и анализа.
#html #css
DEV Community
CSS-Only Custom Elements
Build real, composable web components using markup and CSS, with no JavaScript
👍8🤗1
Ещё один способ описания именованных медиа-запросов
Кажется, разработчикам настолько не хватает именованных медиа-запросов в CSS, что они продолжают придумывать способы их замены на существующих возможностях. Мне известно уже как минимум три способа.
Первый — использовать каскадные слои и возможность переопределения их порядка. Я узнал об этом способе из видео Кевина Пауэлла и написал соответствующий пост с объяснением механики. Это хак, который использует функцию не по назначению.
Работает это благодаря приоритету слоёв, последний в списке самый приоритетный. В зависимости от ширины экрана порядок слоёв меняется так, чтобы актуальный для текущего экрана слой был в конце списка. Это работает, но с нюансами:
- Стили без слоя (отмечены как «общие стили») всегда приоритетнее любых слоёв. Свойство в общих стилях не будет переопределено слоями. В общих стилях можно писать только то, что действительно общее для всех экранов и не переопределяется;
- Порядок слоёв указывается списком, при трёх и более экранах возникает вопрос в каком порядке их указывать. Это может неожиданно выстрелить в ногу при попытке задать свойство для мобильного экрана, которое также должно применяться на планшете.
Второй способ, разбор которого я также делал — запросы стиля контейнера. Он менее «хакерский», более понятный и удобный. Строится на пользовательском свойстве со значениями, отражающими соответствующий экран.
На корневом элементе
Через
Третий способ я увидел в треде на Bluesky. В ответ на пост с ссылкой на статью про именованные контрольные точки без Sass, Лия Веру предложила более лаконичную альтернативу. Она работает на
Корневому элементу
Это относительно новая возможность называется Named-only Containers. Через запятую можно указать несколько имён контейнеров, поэтому можно группировать стили, общие для нескольких экранов, как в стилевых запросах.
Что касается браузерной поддержки, то на момент написания она следующая:
- Каскадные слои — 97.01%, Baseline Widely Available. Можно использовать, но я бы не стал из-за подводных камней и в целом потому что это костыль;
- Стилевые запросы контейнера — 92.87%, Baseline Newly Available с 2026 года, так как недавно появилась в Firefox 151. Рановато использовать, если нужна широкая поддержка;
- Именованные контейнеры — 81.73%, Baseline Newly Available с 2026 года, недавно во всех браузерах и доехали в Safari 26.4. Также рановато.
С учётом того, как работает третий вариант с именованными контейнерами, лично я бы предпочёл его, а
#css
Кажется, разработчикам настолько не хватает именованных медиа-запросов в CSS, что они продолжают придумывать способы их замены на существующих возможностях. Мне известно уже как минимум три способа.
Первый — использовать каскадные слои и возможность переопределения их порядка. Я узнал об этом способе из видео Кевина Пауэлла и написал соответствующий пост с объяснением механики. Это хак, который использует функцию не по назначению.
/* Способ 1: каскадные слои */
@media (width < 640px) {
@layer desktop, tablet, mobile;
}
@media (640px <= width < 1280px) {
@layer desktop, mobile, tablet;
}
@media (width >= 1280px) {
@layer mobile, tablet, desktop;
}
body {
/* Общие стили */
@layer mobile {
/* Стили для мобильных */
}
@layer tablet {
/* Стили для планшетов */
}
@layer desktop {
/* Стили для десктопов */
}
}
Работает это благодаря приоритету слоёв, последний в списке самый приоритетный. В зависимости от ширины экрана порядок слоёв меняется так, чтобы актуальный для текущего экрана слой был в конце списка. Это работает, но с нюансами:
- Стили без слоя (отмечены как «общие стили») всегда приоритетнее любых слоёв. Свойство в общих стилях не будет переопределено слоями. В общих стилях можно писать только то, что действительно общее для всех экранов и не переопределяется;
- Порядок слоёв указывается списком, при трёх и более экранах возникает вопрос в каком порядке их указывать. Это может неожиданно выстрелить в ногу при попытке задать свойство для мобильного экрана, которое также должно применяться на планшете.
Второй способ, разбор которого я также делал — запросы стиля контейнера. Он менее «хакерский», более понятный и удобный. Строится на пользовательском свойстве со значениями, отражающими соответствующий экран.
/* Способ 2: запросы стиля контейнера */
:root {
@media (width < 640px) {
--viewport: mobile;
}
@media (640px <= width < 1280px) {
--viewport: tablet;
}
@media (width >= 1280px) {
--viewport: desktop;
}
}
body {
/* Общие стили */
@container style(--viewport: mobile) {
/* Стили для мобильных */
}
@container style(--viewport: tablet) {
/* Стили для планшетов */
}
@container style(--viewport: mobile)
or style(--viewport: tablet) {
/* Стили для мобильных и планшетов */
}
@container style(--viewport: desktop) {
/* Стили для десктопов */
}
}
На корневом элементе
:root объявляется пользовательское свойство --viewport, значение которого меняется от ширины экрана. Функция style() в @container проверяет значение свойства --viewport и применяются соответствующие стили.Через
or можно комбинировать стили для нескольких экранов. Имя свойства и его значения могут быть произвольными, на вкус автора. Главное не переопределять свойство на самом элементе, иначе будет использовано оно вместо корневого.Третий способ я увидел в треде на Bluesky. В ответ на пост с ссылкой на статью про именованные контрольные точки без Sass, Лия Веру предложила более лаконичную альтернативу. Она работает на
@container, но использует другую механику./* Способ 3: именованные контейнеры */
:root {
@media (width < 640px) {
container-name: mobile;
}
@media (640px <= width < 1280px) {
container-name: tablet;
}
@media (width >= 1280px) {
container-name: desktop;
}
}
body {
/* Общие стили */
@container mobile {
/* Стили для мобильных */
}
@container tablet {
/* Стили для планшетов */
}
@container mobile, tablet {
/* Стили для мобильных и планшетов */
}
@container desktop {
/* Стили для десктопов */
}
}
Корневому элементу
:root задаётся имя контейнера через container-name, которое соответствует экрану. Стили пишутся в директиве @container с указанием имени и без условий. Они применяются при наличии контейнера с указанным именем.Это относительно новая возможность называется Named-only Containers. Через запятую можно указать несколько имён контейнеров, поэтому можно группировать стили, общие для нескольких экранов, как в стилевых запросах.
Что касается браузерной поддержки, то на момент написания она следующая:
- Каскадные слои — 97.01%, Baseline Widely Available. Можно использовать, но я бы не стал из-за подводных камней и в целом потому что это костыль;
- Стилевые запросы контейнера — 92.87%, Baseline Newly Available с 2026 года, так как недавно появилась в Firefox 151. Рановато использовать, если нужна широкая поддержка;
- Именованные контейнеры — 81.73%, Baseline Newly Available с 2026 года, недавно во всех браузерах и доехали в Safari 26.4. Также рановато.
С учётом того, как работает третий вариант с именованными контейнерами, лично я бы предпочёл его, а
@custom-media в таком случае даже не нужен. Но лимитированная поддержка не даёт пока его использовать, разве что с трансформацией в @media.#css
👍8❤1🔥1
Система иконок на CSS
Иконки — неотъемлемая часть интерфейса. Существуют различные способы работы с ними: иконочные шрифты (Font Awesome, Material Symbols), встраиваемые SVG, CSS и SVG-спрайты с JS и без. Лия Веру в статье предлагает способ на CSS.
Её идея строится на том, что иконки живут как токены (пользовательские свойства) в CSS и с его же помощью используются. Без шрифтов, JS, специальных компонентов, спрайтов и прочего. В таком случае иконки могут настраиваться только в CSS.
Для начала иконки определяются в
Это можно сделать руками или написать скрипт, который будет генерировать файл
После определения иконок в CSS нужно добавить специальный спиппет, который делает всю магию. Лия выложила его как npm-пакет. На GitHub есть исходник с миксином и итоговый скомпилированный вариант на 173 строчки.
Что делает этот сниппет:
- Объявляет ненаследуемые пользовательские свойства
- Слушает с помощью
- Задаёт псевдо-элементу цвет фона из свойства
- Задаёт псевдо-элементу размеры из свойства
- Выравнивает псевдо-элемент с помощью
- Задаёт
- Добавляет псевдо-элементу маску через
- Добавляет псевдо-элементу отступ слева или справа из свойства
- Определяет то же для классов
- Обходит специфичные для WebKit баги.
Что в итоге:
- Иконки в свойствах
- Подключаются через
- Размер регулируется через
- Цвет регулируется через
- Вертикальное выравнивание регулируется через
- Отступ регулируется через
Иконками можно управлять в CSS, задавая свойства
Иконки можно задавать декларативно и в HTML через
При желании система расширяется и в сниппет можно добавить дополнительные пользовательские свойства для других псевдо-элементов и состояний, например:
Предложенный подход работы с иконками завязан на запросы стиля контейнера. Они только в мае этого года получили статус Baseline Newly Available м появлением в Firefox. Поддержка по Can I Use сейчас составляет 95.26% (all tracked), что не так уж плохо.
Однако если браузер не поддерживает запросы стиля контейнера, то иконки не будут отображаться вообще. Вёрстку это не сломает, но UX испортит. Лия предлагает критически важные иконки вешать на класс и добавлять его в HTML.
Иконки в псевдо-элементе декоративные и не озвучиваются в программах чтения с экрана. В большинстве случаев это то, что нужно, SVG-иконки обычно прячут через
Можно объявить новое пользовательское свойство
С точки зрения производительности всё выглядит неплохо. Иконки не раздувают HTML (как встроенные SVG), JS (как компоненты-иконки) и CSS (при внешних файлах). CSS работает максимально быстро и реактивный по своей природе.
Иконки в
Однако отложенная загрузка CSS-спрайта приведёт к тому, что иконки какое-то будут не видны и появятся чуть позже. Если основной сниппет в блокирующих стилях, то к прыжкам контента это не приведёт. В общем тут надо идти на компромисс.
У способа с иконками есть недостатки. Также есть перспективные функции CSS, обсуждаемые в CSSWG. В будущем они могут улучшить предложенный метод. Подробнее о самом методе, недостатках и новых функциях Лия пишет в статье.
Выглядит интересно и перспективно, но полагается на новые и грядущие функции CSS. Я бы выбрал этот подход, но позже. А пока встраиваемые на страницу SVG или стэковые SVG-спрайты — мой выбор для работы с иконками.
#css #svg
Иконки — неотъемлемая часть интерфейса. Существуют различные способы работы с ними: иконочные шрифты (Font Awesome, Material Symbols), встраиваемые SVG, CSS и SVG-спрайты с JS и без. Лия Веру в статье предлагает способ на CSS.
Её идея строится на том, что иконки живут как токены (пользовательские свойства) в CSS и с его же помощью используются. Без шрифтов, JS, специальных компонентов, спрайтов и прочего. В таком случае иконки могут настраиваться только в CSS.
Для начала иконки определяются в
:root как пользовательские свойства CSS. Они могут быть встроенными, что сделает файл больше, но компенсируется кэшированием. Или внешними, что увеличит количество сетевых запросов, но CSS будет меньше./*
Переносы строк через \ в url для
читаемости, этого можно не делать
*/
:root {
/* Встроенный SVG */
--icon-trash: url('\
data:image/svg+xml,\
<svg\
viewBox="0 0 32 32"\
xmlns="<http://www.w3.org/2000/svg>"\
>\
<path d="…">\
</svg>\
');
/* Внешний svg */
--icon-link-external: url("\
icons/link-external.svg\
");
/* Остальные иконки */
}
Это можно сделать руками или написать скрипт, который будет генерировать файл
icons.css на основе директории с SVG. Можно добавить SVGO для оптимизации. Скрипт может быть как плагином для сборщика, так и простым Node-скриптом.После определения иконок в CSS нужно добавить специальный спиппет, который делает всю магию. Лия выложила его как npm-пакет. На GitHub есть исходник с миксином и итоговый скомпилированный вариант на 173 строчки.
Что делает этот сниппет:
- Объявляет ненаследуемые пользовательские свойства
--icon-start и --icon-end для дальнейшего подключения иконок;- Слушает с помощью
@container style() эти свойства и если они заданы, добавляет псевдо-элемент ::before или ::after;- Задаёт псевдо-элементу цвет фона из свойства
--icon-style-color с резервным значением currentColor;- Задаёт псевдо-элементу размеры из свойства
--icon-style-size с резервным значением 1em;- Выравнивает псевдо-элемент с помощью
vertical-align, единиц cap, свойств --icon-style-size и --icon-style-voffset;- Задаёт
content псевдо-элементу с указанной иконкой;- Добавляет псевдо-элементу маску через
mask-image с указанной иконкой;- Добавляет псевдо-элементу отступ слева или справа из свойства
--icon-style-spacing с резервным значением 0.3em, если родительский элемент не пустой;- Определяет то же для классов
.icon, .icon-start и .icon-end;- Обходит специфичные для WebKit баги.
Что в итоге:
- Иконки в свойствах
--icon-<name>;- Подключаются через
--icon-start и --icon-end;- Размер регулируется через
--icon-style-size, по умолчанию 1em;- Цвет регулируется через
--icon-style-color, по умолчанию currentColor;- Вертикальное выравнивание регулируется через
--icon-style-voffset;- Отступ регулируется через
--icon-style-spacing, по умолчанию 0.3em.<style>
.button--delete {
/* Иконка мусорки в начале кнопки */
--icon-start: var(--icon-trash-outlined);
&:hover {
/* Замена иконки при наведении */
--icon-start: var(--icon-trash);
}
}
a[href^="https://"] {
@media print {
/* Иконка только при печати */
--icon-end: var(--icon-link-external);
}
}
a[href^="https://github.com"] {
--icon-end: var(--icon-github);
/* Изменение размера иконки */
--icon-style-size: 0.8em;
}
</style>
<!-- [] обозначены псевдо-элементы -->
<button
type="button"
class="button button--delete"
>
[иконка мусорки]
Удалить
</button>
<a href="https://github.com/...">
@nudeui/icons
[иконка github]
</a>
<div class="alert alert--success">
<span
class="icon"
style="--icon: var(--icon-check)"
>
[иконка галочки]
</span>
<p>Оплата прошла успешно</p>
</div>
Иконками можно управлять в CSS, задавая свойства
--icon-*. Можно менять иконку в зависимости от состояния: наведение, медиа-запрос, запрос от контейнера, соответствие условию в :has() и так далее. Иконку можно скрыть на определённом экране.Иконки можно задавать декларативно и в HTML через
style и соответствующие свойства или использовать отдельный элемент с классом .icon, если псевдо-элементы уже заняты для чего-то другого. Cпособ довольно универсальный.При желании система расширяется и в сниппет можно добавить дополнительные пользовательские свойства для других псевдо-элементов и состояний, например:
--icon-marker для ::marker или --icon-hover для состояния :hover.Предложенный подход работы с иконками завязан на запросы стиля контейнера. Они только в мае этого года получили статус Baseline Newly Available м появлением в Firefox. Поддержка по Can I Use сейчас составляет 95.26% (all tracked), что не так уж плохо.
Однако если браузер не поддерживает запросы стиля контейнера, то иконки не будут отображаться вообще. Вёрстку это не сломает, но UX испортит. Лия предлагает критически важные иконки вешать на класс и добавлять его в HTML.
Иконки в псевдо-элементе декоративные и не озвучиваются в программах чтения с экрана. В большинстве случаев это то, что нужно, SVG-иконки обычно прячут через
aria-hidden="true". Для важных иконок есть решения.Можно объявить новое пользовательское свойство
--icon-alt и передавать его вторым аргументом после / в content. Это добавит альтернативный текст псевдо-элементу. Или можно просто указать имя в aria-label или visually-hidden.С точки зрения производительности всё выглядит неплохо. Иконки не раздувают HTML (как встроенные SVG), JS (как компоненты-иконки) и CSS (при внешних файлах). CSS работает максимально быстро и реактивный по своей природе.
Иконки в
:root могут быть отдельным файлом icons.css, тогда получится, по сути, CSS-спрайт. Можно отложить его загрузку через трюк с media и сделать неблокирующим ресурсом. А у url() в CSS низкий приоритет.Однако отложенная загрузка CSS-спрайта приведёт к тому, что иконки какое-то будут не видны и появятся чуть позже. Если основной сниппет в блокирующих стилях, то к прыжкам контента это не приведёт. В общем тут надо идти на компромисс.
У способа с иконками есть недостатки. Также есть перспективные функции CSS, обсуждаемые в CSSWG. В будущем они могут улучшить предложенный метод. Подробнее о самом методе, недостатках и новых функциях Лия пишет в статье.
Выглядит интересно и перспективно, но полагается на новые и грядущие функции CSS. Я бы выбрал этот подход, но позже. А пока встраиваемые на страницу SVG или стэковые SVG-спрайты — мой выбор для работы с иконками.
#css #svg
lea.verou.me
The best way to use icons in Web UIs • Lea Verou
What if I told you, you could use CSS to insert and tweak icons without touching any HTML? And that it could be more flexible and maintainable than what you’ve been using?
👍9👀4❤1👎1
Почему в HTML нет include?
Джейк Арчибальд озадачен вопросом: почему в HTML нет механизма включений? В CSS есть директива
Не только Джейк озадачен этим. Вопрос периодически всплывает в разных местах. Многие разработчики хотели бы включать повторяющиеся фрагменты HTML простым способом и не полагаться на инструменты и хаки для такой фундаментальной функции.
Базовый принцип DRY: при разработке сайта вынести шапку, подвал и некоторые разделы в отдельные файлы и вставлять их в нужных местах. HTML ничего не даёт для решения этой задачи и разработчики вынуждены прибегать к инструментам.
Какие решения есть сегодня:
- Элементы
- Fetch API. Встроенная функция
- Веб-компоненты. Разметку можно упаковать в веб-компоненты. Но чтобы они работали как включения, разметку нужно поместить в JS или загружать через Fetch API или HTML Modules, что снова требует JS;
- Server Side Includes (SSI). Специальные HTML-комментарии, обрабатываемые сервером позволяют включать файлы. Это требует специальной настройки сервера и есть риски безопасности;
- Edge Side Includes (ESI). Примерно то же, что и SSI, но вместо комментариев XML-подобный синтаксис со специальными элементами. Требуется инфраструктура, доступная лишь на некоторых серверах;
- Серверные шаблоны. Серверные языки и фреймворки на их основе предлагают механизмы для генерации шаблонов. Требуется сервер, сторонний язык и его механизм шаблонов;
- Системы сборки. Инструменты, от простых скриптов до фреймворков, позволяют предварительно обработать файлы локально. Это требует установки и настройки инструментов.
Решения есть и их много. Но они зависят от сторонних языков, инструментов, серверов или требуют JS для работы. Комментарии к статье содержат разные мнения, почему такой базовой функции до сих пор нет в HTML. А хотелось бы чего-то такого:
Чтобы на страницу можно было включить внешний файл так, чтобы это было в HTML, декларативно, работало в браузерах, без JS и сторонних инструментов, доступно для поисковиков, быстро и безопасно. В этом всём много подводных камней.
Циклические зависимости, выполнение JS, обработка стилей, сдвиги макета, ошибки загрузки, асинхронность, кэширование, CORS, сканер предварительной загрузки, приоретизация ресурсов, работа парсера и много других вопросов.
Но, похоже, часть этих проблем решили. В статье с анонсом Declarative Partial Updates было сказано, что потенциально в будущем на их основе могут появиться включения. Declarative Partial Updates Project шире, чем подстановка фрагментов в маркеры.
Ноам Розенталь активно работает над предложением Fragment Include в рамках Declarative Partial Updates Project и намерен представить его на грядущем TPAC вместе с рядом других интересных предложений. Пока рано делать полноценный обзор.
Тем не менее, Fragment Include предлагают те самые включения в HTML, не требующие JS, специальных серверов и сторонних инструментов:
Возможно, в обозримом будущем они появятся в браузерах и станут стандартом. Посмотрим, какие новости будут с TPAC.
#html
Джейк Арчибальд озадачен вопросом: почему в HTML нет механизма включений? В CSS есть директива
@import, в JS есть ES-модули. Все текущие решения ограничены или зависят от сторонних инструментов.Не только Джейк озадачен этим. Вопрос периодически всплывает в разных местах. Многие разработчики хотели бы включать повторяющиеся фрагменты HTML простым способом и не полагаться на инструменты и хаки для такой фундаментальной функции.
Базовый принцип DRY: при разработке сайта вынести шапку, подвал и некоторые разделы в отдельные файлы и вставлять их в нужных местах. HTML ничего не даёт для решения этой задачи и разработчики вынуждены прибегать к инструментам.
Какие решения есть сегодня:
- Элементы
<iframe>, <object> и <embed>. Строго говоря, это и есть HTML-включения. Но внутрь этих элементов загружается отдельный изолированный документ со своими ресурсами и вытекающими из этого ограничениями;- Fetch API. Встроенная функция
fetch() позволяет запросить HTML по URL, извлечь содержимое и вставить его в нужное место. Это требует JS;- Веб-компоненты. Разметку можно упаковать в веб-компоненты. Но чтобы они работали как включения, разметку нужно поместить в JS или загружать через Fetch API или HTML Modules, что снова требует JS;
- Server Side Includes (SSI). Специальные HTML-комментарии, обрабатываемые сервером позволяют включать файлы. Это требует специальной настройки сервера и есть риски безопасности;
- Edge Side Includes (ESI). Примерно то же, что и SSI, но вместо комментариев XML-подобный синтаксис со специальными элементами. Требуется инфраструктура, доступная лишь на некоторых серверах;
- Серверные шаблоны. Серверные языки и фреймворки на их основе предлагают механизмы для генерации шаблонов. Требуется сервер, сторонний язык и его механизм шаблонов;
- Системы сборки. Инструменты, от простых скриптов до фреймворков, позволяют предварительно обработать файлы локально. Это требует установки и настройки инструментов.
Решения есть и их много. Но они зависят от сторонних языков, инструментов, серверов или требуют JS для работы. Комментарии к статье содержат разные мнения, почему такой базовой функции до сих пор нет в HTML. А хотелось бы чего-то такого:
<include src="header.html">
<main>
<!-- контент страницы -->
<include src="contact-form.html">
</main>
<include src="footer.html">
Чтобы на страницу можно было включить внешний файл так, чтобы это было в HTML, декларативно, работало в браузерах, без JS и сторонних инструментов, доступно для поисковиков, быстро и безопасно. В этом всём много подводных камней.
Циклические зависимости, выполнение JS, обработка стилей, сдвиги макета, ошибки загрузки, асинхронность, кэширование, CORS, сканер предварительной загрузки, приоретизация ресурсов, работа парсера и много других вопросов.
Но, похоже, часть этих проблем решили. В статье с анонсом Declarative Partial Updates было сказано, что потенциально в будущем на их основе могут появиться включения. Declarative Partial Updates Project шире, чем подстановка фрагментов в маркеры.
Ноам Розенталь активно работает над предложением Fragment Include в рамках Declarative Partial Updates Project и намерен представить его на грядущем TPAC вместе с рядом других интересных предложений. Пока рано делать полноценный обзор.
Тем не менее, Fragment Include предлагают те самые включения в HTML, не требующие JS, специальных серверов и сторонних инструментов:
<!--
Асинхронно загружает header.html,
выполняет санитизацию (удаляет скрипты)
и вставляет вместо <template>
-->
<template for src="header.html">
</template>
<!--
Асинхронно загружает content.html,
выполняет санитизацию (удаляет скрипты)
и вставляет вместо маркера main-content
-->
<div id="content">
<?start name="main-content">
Загрузка...
<?end>
</div>
<template
for="main-content"
src="content.html"
>
</template>
Возможно, в обозримом будущем они появятся в браузерах и станут стандартом. Посмотрим, какие новости будут с TPAC.
#html
👍8
Slab elements
Тони Уард поделился проектом Slab. Это библиотека headless веб-компонентов с открытым исходным кодом (лицензия Apache 2.0). Написана на Lit, предлагает на момент написания 42 элемента и активно пополняется новыми.
Библиотека выполнена в стиле headless. По умолчанию компоненты поставляются без стилей, только семантическая разметка, логика, доступность и API. Потребители сами добавляют необходимые стили через предоставляемые API.
Компоненты Slab используют Shadow DOM. Слоты, части, состояния, события, методы, свойства и атрибуты задокументированы. Есть описание поведения, клавиатурной навигации, примеры подключения в разные фреймворки и пример стилизованного элемента.
У Slab два сценария применения. Первый — точечное использование компонентов. Они подключаются, стилизуются через части и слоты и используются как обычные HTML-элементы. Причём в любом фреймворке или без них.
Второй сценарий — основа собственной UI-библиотеки. Через наследование создаются новые элементы с заданными стилями и, возможно, дополнительными свойствами и методами. Затем они регистрируются как новые пользовательские элементы.
В этом Slab похож на другой проект — Lion, который уже давно существует и также предоставляет headless компоненты. Судя по редизайну сайта, Lion, похоже, готовится к релизу новой версии. Когда это случится, Lion и Slab будут прямыми конкурентами.
Что касается Slab, то библиотека ещё в стадии активной разработки. Стоит за ней посматривать, может выйти неплохой источник headless компонентов, которые работают везде.
#html #css #js
Тони Уард поделился проектом Slab. Это библиотека headless веб-компонентов с открытым исходным кодом (лицензия Apache 2.0). Написана на Lit, предлагает на момент написания 42 элемента и активно пополняется новыми.
Библиотека выполнена в стиле headless. По умолчанию компоненты поставляются без стилей, только семантическая разметка, логика, доступность и API. Потребители сами добавляют необходимые стили через предоставляемые API.
Компоненты Slab используют Shadow DOM. Слоты, части, состояния, события, методы, свойства и атрибуты задокументированы. Есть описание поведения, клавиатурной навигации, примеры подключения в разные фреймворки и пример стилизованного элемента.
У Slab два сценария применения. Первый — точечное использование компонентов. Они подключаются, стилизуются через части и слоты и используются как обычные HTML-элементы. Причём в любом фреймворке или без них.
<slab-switch
label="Уведомления"
name="notifications"
>
</slab-switch>
<style>
slab-switch {
&::part(switch) {
/* ... */
}
&::part(label) {
/* ... */
}
&:state(checked)::part(switch) {
/* ... */
}
}
</style>
<script type="module">
import 'slab-elements/switch.js';
const toggle = document.querySelector(
'slab-switch'
);
toggle.addEventListener('change', () => {
// ...
});
</script>
Второй сценарий — основа собственной UI-библиотеки. Через наследование создаются новые элементы с заданными стилями и, возможно, дополнительными свойствами и методами. Затем они регистрируются как новые пользовательские элементы.
import Switch from 'slab-elements/switch.js';
import tokens from './tokens.css'
with { type: 'css' };
import styles from './switch.css'
with { type: 'css' };
declare global {
interface HTMLElementTagNameMap {
'ui-switch': UISwitch;
}
}
export default class UISwitch extends Switch {
static override styles = [tokens, styles];
/* ... */
}
customElements.define('ui-switch', UISwitch);
В этом Slab похож на другой проект — Lion, который уже давно существует и также предоставляет headless компоненты. Судя по редизайну сайта, Lion, похоже, готовится к релизу новой версии. Когда это случится, Lion и Slab будут прямыми конкурентами.
Что касается Slab, то библиотека ещё в стадии активной разработки. Стоит за ней посматривать, может выйти неплохой источник headless компонентов, которые работают везде.
#html #css #js
Slab
Slab · Headless Web Components
Headless Web Components. Slab handles keyboard, focus, ARIA, and forms on top of native elements. What it looks like is up to you.
✍1👍1