Если вы когда-нибудь заглядывали за кулисы пользовательских веб-интерфейсов, то знаете для чего нужно свойство
class
. Оно ведь нужно для связи HTML с CSS, правда? Сейчас я расскажу о том, почему настало время отказаться от него. Имена классов — это архаичная система, используемая как неудачный посредник для примитивов UI; ещё хуже то, что они создают ужасные сочетания, приводящие к комбинаторному взрыву странных пограничных случаев. Давайте изучим этот вопрос, начав со скучного урока истории, который вы уже слышали миллион раз.Классы очень старые, даже древние
HTML 2.0 (1996 год) стала первой опубликованной спецификацией HTML, в ней имелся фиксированный список имён тегов, и у каждого тега был фиксированный список разрешённых атрибутов. Документы HTML 2.0 нельзя было стилизовать, ведь какой в этом смысл? Экраны многих компьютеров в то время были чёрно-белыми! Самым близким к стилизации в HTML 2.0 был тег
<pre>
, имевший атрибут width
. Для разработки HTML 3.0 потребовалось несколько лет, а Netscape и Microsoft тем временем добавляли всевозможные странные расширения, включая любимые нами теги <marquee>
и <blink>
. В конце концов, все разногласия были разрешены, и в 1997 году появился HTML 3.2, что позволило «стилизовать» тег <body>
такими атрибутами, как bgcolor
и text
.Тем временем изобрели CSS, ставший способом добавления в веб-страницы некой структуры и стилизации. Чтобы веб был повеселее. История HTML 3.2 оказалась короткой, потому что в том же 1997 году был опубликован HTML 4.0, в котором появились механизмы поддержки CSS, в том числе новые «идентификаторы элементов» — атрибуты
id
и class
:Чтобы повысить удобство управления элементами, в HTML был добавлен новый атрибут [2] CLASS. Всем элементам внутри элемента BODY можно добавлять классы, а настраивать их можно в таблице стилей CSS Level 1
Эти атрибуты позволили нам определять «классы» элементов, которые можно стилизовать при помощи ограниченного набора тегов. Например,
<div class="panel">
может внешне сильно отличаться от <div class="card">
, несмотря на одинаковое имя тега. Концептуально это можно воспринимать как классическое наследование (то есть класс Card расширяет Div
) — наследование семантики и базовых стилей div
с созданием стиля для класса Card
с возможностью его многократного использования.С 1997 года мы пережили более двадцати лет инноваций в вебе. Теперь существует множество способов структурирования CSS.
«Классы очень стары» — это не аргумент против классов, однако это показывает, что классы решали свою задачу в период серьёзных ограничений. Веб был молодым, браузеры были менее сложными, а цифровой дизайн — гораздо менее зрелым. В то время нам не нужно было более сложное решение.
Масштабирование селекторов Class
Если продолжить рассуждать о свойстве
class
как об аналоге классов ООП, то можно отметить, что классы редко не получают параметров и не имеют состояния. Ценность классов в C заключается в том, что они имеют «режимы» благодаря параметрам, и мы можем менять их состояние при помощи методов. CSS имеет псевдоселекторы, описывающие ограниченные части состояния, например, :hover
, но для описания индивидуального состояния или модальности внутри класса нужно использовать ещё больше классов. Проблема в том, что class
получает только список строк…Вернёмся к нашему примеру с
Card
. Допустим, нам нужно параметризировать Card
так, чтобы он получал опцию size
, имеющую одно из значений Big
/Medium
/Small
, булеву опцию rounded
и опцию align
со значением Left
/Right
/Center
. Пусть также наш Card
может загружаться «лениво», так что нам нужно описать состояние Loading
и Loaded
. В нашем распоряжении имеется множество опций, но каждая из них обладает ограничениями:- Мы можем представить их в виде дополнительных классов, например,
<div class="Card big">
. Проблема такого подхода заключается в том, что ему не хватает пространств имён; могут появиться какие-то другие CSS и вобрать в себя значениеbig
для своего компонента, что может привести к конфликту. Обойти эту проблему можно при помощи комбинирования селекторов в CSS:.Card.big {}
, но это может поднять вопросы специфичности, что в дальнейшем способно привести к проблемам. - Мы можем представить их в виде отдельных «конкретных» классов, например,
<div class="BigCard">
. Проблема такого подхода в том, что потенциально мы создадим множество дублирующихся CSS, так какBigCard
иSmallCard
с большой вероятностью будут иметь много общего CSS. Такой подход создаёт и проблемы масштабируемости, приводя к проблеме комбинаторного взрыва; для одной лишь опцииsize
нам нужно создать три класса, но если добавитьrounded
, то их уже нужно шесть, а если ввести ещё иalign
, то придётся создавать 18 классов. - Мы можем создать пространства имён для параметров классов, например,
<div class="Card Card--big">
. Это помогает смягчить конфликты и избежать проблемы комбинаторного взрыва, но такая система будет слишком многословной, с большим объёмом дублирующейся типизации; к тому же она страдает от ещё одной проблемы, связанной с неправильным использованием: что произойдёт, если я использую классCard--big
безCard
?
Современный CSS может решить некоторые из этих проблем, например, функции псевдоклассов
:is()
и :where()
могут манипулировать со специфичностью селектора (.Card:is(.big)
имеет равную специфичность с .Card
). Также при разработке таких систем можно использовать языки наподобие SASS, способные снизить неудобства дублирования благодаря вложенности и примесям. Это повышает удобство для разработчиков, но не решает фундаментальные проблемы.А ещё у нас есть множество проблем, которые классы не могут решить по своей природе:
- При работе с классами переходных состояний наподобие
loading
иloaded
код может произвольно применять эти классы к элементу, даже когда элемент на самом деле не загружается. Противодействовать этому можно путём дисциплины разработки (плохо масштабируется на большие команды) или инструментарием (сложно поддерживать). - При работе со взаимоисключающими классами наподобие
Big
иSmall
элементы могут применять оба класса одновременно, и никакие системы именования классов не помогут в решении этой проблемы, если только вы не будете решать их целенаправленно добавлением инструментария или кода (например,.Card.big.small { border: 10px solid red }
).
Кроме того, существует «кустарная промышленность» псевдоспецификаций CSS, пытающихся решить эти проблемы, но какого-то полного решения не существует:
▍ BEM — это не решение
BEM, или Block Element Modifier, обеспечивает достаточно надёжное и масштабируемое решение задачи параметризации классов. Он использует пространства имён, позволяющие предотвращать проблемы многократного использования, но ценой многословности. У него есть жёсткие правила именования, что немного упрощает анализ кода.
.Card { }
.Card--size-big { width: 100%; }
.Card--size-small { width: 25%; }
.Card--rounded { border-radius: 6px }
.Card--align-left { text-align: left }
.Card--align-right { text-align: right }
.Card--align-center { text-align: center }
.Card__Title { /* Подкомпоненты! */ }
BEM даёт нам небольшую степень согласованности, но не решает двух фундаментальных проблем классов (контроль за инвариантностью). Я могу применить
class="Card--size-big Card--size-small"
к одному элементу, а предоставляемый BEM фреймворк не в силах этому помешать. Аналогично, в BEM нет понятия защищённых свойств, так что я должен положиться на то, что вы не будете добавлять к элементу .Card--is-loading
. Эти проблемы легче выявить благодаря фреймворку именования, но по уровню удобства они находятся на одном уровне с префиксами _
методов JavaScript. Всё работает, если вы соблюдаете правила, но вас никто не заставит это делать.Ещё одна большая проблема BEM заключается в том, что для описания динамического состояния через JS требуется огромный объём бойлерплейта:
/* Это наименьшее, что я придумал без добавления вспомогательных функций */
function changeCardSize(card, newSize: 'big' | 'small' | 'medium') {
card.classList.toggle('.Card--size-big', newSize === 'big')
card.classList.toggle('.Card--size-medium', newSize === 'medium')
card.classList.toggle('.Card--size-small', newSize === 'small')
}
Для решения проблемы бойлерплейта можно в том числе использовать вспомогательные функции, но это просто проталкивает проблему дальше, а не решает её.
▍ Атомарный CSS — это не решение
Атомарный CSS, или «utility-классы» отходят от концепции ООП описания компонентов систем дизайна наподобие Card, вместо этого используя классы как абстракцию от свойств CSS. Это хорошо подходит большинству систем дизайна, которые, по сути, являются подмножеством самого CSS (например, CSS позволяет использовать практически неограниченное количество цветов, в то время как в вашей палитре бренда может быть меньше сотни цветов). Наиболее примечательной реализацией атомарного CSS, вероятно, является популярная библиотека Tailwind, но это может выглядеть примерно так:
.w-big { width: 100% }
.w-small { width: 25% }
.h-big { height: 100% }
.al-l { text-align: left }
.al-r { text-align: right }
.br-r { border-radius: 6px }
/* и так далее... */
При этом, повторюсь: атомарный CSS не решает двух главных проблем классов. Я всё равно могу применить к своему элементу
class="w-big w-small"
, и по-прежнему отсутствует возможность использования защищённых классов.Кроме того, атомарный CSS обычно приводит к хаосу в разметке. Чтобы снизить многословность, в таких системах обычно предпочитают краткие имена классов из нескольких символов наподобие
br
вместо border-radius
. Чтобы описать наш пример с Card в этой системе, требуется китайская грамота из непостижимых имён классов, а ведь это тривиальный пример:<!-- Big Card -->
<div class="w-big h-big al-l br-r"></div>
Кроме того, при использовании атомарного CSS теряются многие преимущества CSS. Атомарный CSS заставляет всех изучать документацию; опытным дизайнерам с большим опытом в написании CSS приходится обращаться к таблице поиска: «мне нужно сделать
flex-shrink: 0
, это будет flex-shrink-0
или shrink-0
»? Все utilities в общем случае представляют собой одно имя класса, то есть мы теряем все преимущества специфичности; если же мы добавляем специфичность при помощи смешения методологий или использования media query или встроенных стилей, то всё становится ещё хуже и начинает разваливаться на части. Обычно проблему специфичности решают добавлением ещё большей специфичности; в Primer CSS GitHub эту проблему обходят добавлением !important
к каждому utility-классу, что в дальнейшем создаёт новые проблемы.Если уж мы коснулись темы media query, то скажу, что считаю самой большой проблемой атомарного CSS то, что он оставляет адаптивный дизайн (responsive design) открытым к интерпретациям. Многие реализации предоставляют классы, которые применяются только в контрольной точке адаптивности, что лишь ещё больше загрязняет разметку и подвержено проблеме комбинаторного взрыва. Вот фрагмент со всего лишь двумя width в двух контрольных точках, определённый в CSS Tailwind:
.w-96 { width: 24rem }
.w-80 { width: 20rem }
@media (min-width: 640px) {
.sm\:w-96 { width: 24rem; }
.sm\:w-80 { width: 20rem; }
}
@media (min-width: 768px) {
.md\:w-96 { width: 24rem; }
.md\:w-80 { width: 20rem; }
}
<!-- Big Card на больших экранах, Small Card на маленьких экранах -->
<div class="w-96 sm:w-80 al-l br-r"></div>
На первый взгляд система utility-классов кажется благословением для системы дизайна, но если применить её в разметке, становятся заметны проблемы. Невозможность удобного представления компонентов в разметке заставляет систему дизайна искать другие решения, например, добавление разметки с прикреплёнными именами классов для описания компонента, что обычно приводит к тому, что система дизайна реализует компоненты во множестве фреймворков.
У методологии Utility CSS есть куча других проблем. Если вы считаете, что это решение вам подходит, то рекомендую потратить время на изучение его недостатков.
▍ CSS-модули — это не решение
На самом деле, CSS-модули решают только одну проблему: «коллизию селекторов». Можно написать CSS в одном файле, который станет пространством имён классов, а затем обработать его инструментом, добавляющим в начало пространств имён, а в конец — случайные символы. Случайные символы генерируются во время сборки, чтобы избежать коллизий самописных стилей, не использующих CSS-модули, с теми, которые их используют. При этом наш CSS Card
.card { /* "Базовый" компонент */ }
.big { width: 100% }
.small { width: 25% }
/* ... и так далее ... */
после преобразования на этапе сборки превращается в
.card_166056 { /* ... */ }
.card_big_166056 { width: 100% }
.card_small_166056 { width: 25% }
/* ... и так далее ... */
Похоже, что это решает проблемы с BEM, потому что теперь не нужно везде прописывать пространства имён! Но вместо этого нужен инструментарий, который необходимо разработать и поддерживать во всём стеке, описывающем UI. Для этого нужно, чтобы фреймворк шаблонизации, среда исполнения JS (если она отличается) и компилятор CSS понимали и использовали одну систему CSS-модулей, что создаёт множество зависимостей в кодовой базе. Если вы работаете в большой организации с кучей обслуживаемых веб-сайтов, которые, вероятно, написаны на разных языках, то вам нужно разработать и поддерживать инструментарий для всего этого. Вашей команде разработки системы дизайна придётся заняться оснасткой этого инструментария (или переложить эту ношу на другие команды разработчиков).
Но у нас всё равно остаются две фундаментальные проблемы! Повторюсь, что проблема
class="big small"
по-прежнему не решена. Можно реализовать как бы защищённые классы, если добавить в кодовую базу ещё больше инструментария, чтобы гарантировать, что только один компонент использует один файл CSS-модуля, но это решение обладает всеми недостатками крупной технологии: нужна ещё куча инструментария.Кроме того, CSS-модули полностью устраняют возможность кэширования CSS вне рамок одного развёртывания. Единственный способ кэширования CSS в такой ситуации — это сделать преобразование имён классов детерминированным, из-за чего теряется сам смысл использования хэшей — без дисциплины разработки (сложно масштабируемой на большие команды) разработчик может жёстко прописывать хэшированные имена классов в своём HTML.
▍ Проблема всех этих решений
Основная проблема всех этих решений заключается в том, что они ставят в центр свойство
class
как единственный способ описания состояния объекта. Будучи списком произвольных строк, классы не имеют ключей и значений, приватных состояний, сложных типов (это ещё и означает очень ограниченную поддержку IDE). И чтобы сделать их хотя быть чуть более удобными, приходится пользоваться специальными DSL наподобие BEM. Мы постоянно пытаемся реализовать параметры в виде Set<string>
, хотя на самом деле нам нужно Map<string, T>
.Решение всех этих проблем
Я утверждаю, что современная веб-разработка предоставляет нам все средства, чтобы отказаться от имён классов и реализовать нечто гораздо более надёжное; для этого достаточно внести довольно простые изменения:
▍ Атрибуты
Атрибуты позволяют нам параметризировать компонент при помощи описания «ключ-значение», что очень похоже на
Map<string, T>
. У браузеров есть куча функций селекторов для парсинга значений атрибута. Если взять наш пример с Card, весь CSS можно выразить так:.Card { /* ... */ }
.Card[data-size=big] { width: 100%; }
.Card[data-size=medium] { width: 50%; }
.Card[data-size=small] { width: 25%; }
.Card[data-align=left] { text-align: left; }
.Card[data-align=right] { text-align: right; }
.Card[data-align=center] { text-align: center; }
HTML-атрибуты можно выразить только один раз, то есть
<div data-size="big" data-size="small">
будет соответствовать только data-size=big
. Это решает проблему инвариантов, на что неспособны другие решения.Это может показаться похожим на BEM и обладает многими его преимуществами. При создании CSS они определённо похожи, но моё предложение демонстрирует свои достоинства, когда дело доходит до создания HTML — ведь тогда становится гораздо проще дискретно различать каждое из состояний:
<div class="Card" data-size="big" data-align="center"></div>
Кроме того, становится гораздо проще сделать значения динамическими при помощи JS:
function changeCardSize(card, newSize: 'big' | 'small' | 'medium') {
card.setAttribute('data-size', newSize)
}
Префикс
data-
может быть немного неуправляемым, но он обеспечивает широчайшую совместимость с инструментами и фреймворками. Использование атрибутов без какого-либо пространства имён может быть немного опасным, ведь мы рискуем случайно переписать глобальные атрибуты HTML. Если имя атрибута содержит дефис, то это должно быть достаточно безопасно. Например, можно изобрести своё собственное пространство имён для работы с параметрами CSS, что повышает читаемость:.Card[my-align=left] { text-align: left; }
Это даёт и другие осязаемые преимущества. Селекторы атрибутов наподобие
[attr~"val"]
позволяют работать со значением так, как будто оно является списком. Это может быть полезным, если вам нужна гибкость в стилизации частей компонента, например, применение стиля к одной или нескольким сторонам границы:.Card { border: 10px solid var(--brand-color) }
.Card[data-border-collapse~="top"] { border-top: 0 }
.Card[data-border-collapse~="right"] { border-right: 0 }
.Card[data-border-collapse~="bottom"] { border-bottom: 0 }
.Card[data-border-collapse~="left"] { border-left: 0 }
<div class="card" data-border-collapse="left right"></div>
Готовая к выпуску спецификация CSS Values 5 также позволяет атрибутам проникать в свойства CSS, подобно переменным CSS. В системах дизайна применяются различные уровни размеров, абстрагирующие значения в пикселях (например,
pad-size
может иметь значение от 1 до 6, где каждое число обозначает величину от 3px до 18px):<div class="card" pad-size="2"></div>
.Card {
/* Берём атрибут `pad-size` и приводим его к значению `px`. */
/* Если оно отсутствует, откатываемся к 1px */
--padding-size: attr(pad-size px, 1px)
/* Делаем размер padding кратным 3px */
--padding-px: calc(var(--padding-size) * 3px);
padding: var(--padding-px);
}
Разумеется, при достаточном объёме типизации эту проблему можно решить уже сегодня, по крайней мере, для ограниченных значений (которые выражают большинство систем дизайна):
.Card {
--padding-size: 1;
--padding-px: calc(var(--padding-size) * 3px)
padding: var(--padding-px);
}
.Card[pad-size=2] { --padding-size: 2 }
.Card[pad-size=3] { --padding-size: 3 }
.Card[pad-size=4] { --padding-size: 4 }
.Card[pad-size=5] { --padding-size: 5 }
.Card[pad-size=6] { --padding-size: 6 }
Согласен, это достаточно большой объём бойлерплейта, но как временное решение подойдёт.
▍ Собственные имена тегов
Если вы дочитали до этого места, то, вероятно, уже кричите в монитор: «Автор, ты клоун, ты ведь по-прежнему использует имена классов!
.Card
— это класс!» Ну, тут всё просто. HTML5 позволяет использовать собственные теги, любой тег, не распознанный парсером — это неизвестный элемент, который можно свободно стилизовать как угодно. Неизвестные теги не имеют стандартной стилизации user-agent: по умолчанию они ведут себя как <span>
. Это полезно, потому что мы можем выразить компонент при помощи литерального имени тега вместо class
:<my-card data-size="big"></my-card>
my-card { /* ... */ }
my-card[data-size="big"] { width: 100% }
Эти элементы представляют собой абсолютно валидный синтаксис HTML5 и не требуют никаких дополнительных определений, никакого специального DTD или метатега, никакого JavaScript. Как и в случае с атрибутами, хорошей идеей будет добавление
-
, что соответствует спецификации и предотвращает случайное переписывание. Кроме того, использование -
также позволит использовать ещё более мощные инструменты наподобие Custom Element Definitions, что обеспечивает возможность интерактивности на JavaScript. С Custom Elements можно использовать собственные состояния CSS, благодаря чему мы переходим на новый уровень возможностей:▍ Custom State (собственные псевдоселекторы)
Если у ваших компонентов есть любой уровень интерактивности, то им может потребоваться изменение стиля из-за какого-нибудь изменения состояния. Возможно, вам знакомы элементы
input[type=checkbox]
, имеющие псевдокласс :checked
, позволяющий CSS связываться с их внутренним состоянием. В случае нашего примера с Card мы хотели добавить состояние loading, чтобы можно было декорировать его в CSS; дополнить его анимированными спиннерами, в то время как полностью загруженная card может отображаться с зелёной рамкой. Добавив немного JavaScript, можно определить тег как Custom Elements, взять объект внутреннего состояния и манипулировать им для представления этого как собственных псевдоселекторов для собственного тега:customElements.define('my-card', class extends HTMLElement {
#internal = this.attachInternals()
async connectedCallback() {
this.#internal.states.add('loading')
await fetchData()
this.#internal.states.delete('loading')
this.#internal.states.add('loaded')
}
})
my-card:state(loading) { background: url(./spinner.svg) }
my-card:state(loaded) { border: 2px solid green }
Custom states могут быть очень мощными, потому что они позволяют элементу представить себя как модальность с определёнными условиями без изменения его разметки, то есть элемент может сохранить полный контроль за своими состояниями, и их нельзя будет контролировать снаружи (если только элемент не разрешит это). Можно даже назвать это внутренним состоянием. Они поддерживаются всеми современными браузерами, а для старых или необычных браузеров создан полифил (хотя он имеет некоторые тонкости).
Заключение
Существует множество замечательных способов выражения состояний и параметров компонента без необходимости привязки их к архаичной системе наподобие атрибута
class
. Сегодня у нас есть механизмы для его замены, нам просто нужно освободиться от собственных оков. Будущие стандарты позволяют нам выражать свои идеи новыми способами.Telegram-канал со скидками, розыгрышами призов и новостями IT ?
Комментарии (68)
infectedtrauma
19.07.2024 13:27+2Правильно, навалим еще на клиента нагрузку, чтобы точно тупило :D
Зачем вот это нужно, если на бэкенде можно организовать любой какой тебе нужно промежуточный сервис, который из твоей придуманной восхитительной (заменим часть название на кей-значение, ага, стало сразу круче) системы стилей сгенерирует то что тебе нужно? Эта задача решается дополнительным слоем абстракции, причем только на билде
P.S> комментарий про автора-клоуна - самый полезный.
gmtd
19.07.2024 13:27+27При этом, повторюсь: атомарный CSS не решает двух главных проблем классов. Я всё равно могу применить к своему элементу
class="w-big w-small"
, и по-прежнему отсутствует возможность использования защищённых классов.Галиматья какая-то...
А топором по ноге своей автору кто мешает ударить? Никто? Нехай рубит.
ilekarev
19.07.2024 13:27И аналогично с подходом автора
<div data-size="small" data-size="big">
можно написатьKasperGreen
19.07.2024 13:27+9Автор пишет, что первый атрибут затирается вторым, а вот в случае
class="first second"
— применятся оба.В целом подход автора — весьма интересен. Я всë ждал когда же он уже напишет про CSSinJS, но закончилось всë неожиданно новой фичей и как-бы не CSSinJS, но
— не без JS.
Это не повод прямо сейчас бросить React, но приятно, что отступные пути есть и всë новые появляются — как часть стандарта.
artptr86
19.07.2024 13:27+2К сожалению, до сих пор custom elements требуют регистрации в глобальном реестре, а это значит, что в большом приложении может возникнуть коллизия имён элементов.
ImagineTables
19.07.2024 13:27+13По-моему, это очередное столкновение сторонников и противников компонентности в UI. Эта борьба, раз уж мы вспоминаем историю, пожалуй, ещё древнее, чем CSS и классы. Когда-то одни писали приложения в терминах представлений документов. Это требовало бОльших усилий для создания «фастфуда», но давало больше свободы и, в конечном итоге, оправдывало себя по мере роста сложности и юзерских хотелок. Другие наслаждались тем, что можно кинуть компонент(ы) на форму и получить сразу почти готовое приложение, но по мере роста сложности приложения упирались в ограничения, диктуемые архитектурой компонентов. Как в анекдоте: «как ни собирали детали, а всё выходит пулемёт». Я своими глазами видел, как аргумент «используемые нами компоненты это не поддерживают» возникал при обсуждении требований со стороны дизайнеров. Конечно же, я стал убеждённым противником сборки UI из компонентов.
А вот автор явно хочет вернуться к компонентности. Он 13 раз использует слово «компонент» (я подсчитал, не поленился). В данном случае, ему нужен компонент карточки:
Вернёмся к нашему примеру с Card. Допустим, нам нужно параметризировать Card так, чтобы он получал опцию size, имеющую одно из значений Big/Medium/Small, булеву опцию rounded и опцию align со значением Left/Right/Center. Пусть также наш Card может загружаться «лениво», так что нам нужно описать состояние Loading и Loaded.
Но к счастью для тех, кто разделяет мой подход, современный HTML не про компоненты, современный HTML про стилизации. И в этом его сила. Например,
rounded
, который автор хочет видеть булевским свойством компонента карточки — на самом деле универсальная стилизация, равно подходящая и к карточке, и к кнопке, и к даже к некоторым из объектов UI, которые сегодня ещё никто не изобрёл. Нет никакого смысла ограничивать карточки (как объект UI) при создании приложения тем жалким набором свойств, которые предусмотрел автор карточного компонента. Так теряется универсальность. Гораздо лучше составить словарь стилизаций и уже на этом языке описывать UI.Конечно, от составителя этого словаря требуется ДУМОТЬ, чего многие не любят и не умеют. И часто получается
TailWindдикая смесь всех стилей и направлений. Но это не значит, что плох сам подход.KasperGreen
19.07.2024 13:27+4Конечно же, я стал убеждённым противником сборки UI из компонентов.
А как вы следите за консистентностью и соблюдаете DRY — если не используете в том или ином виде компоненты? Вы пишите один раз и больше не занимаетесь поддержкой? Это лендосы или PWA?
Цвет автомобиля может быть любым, при условии, что он черный © Генри Форд
Это я к тому, что либо вы выпускаете штучный товар за дорого с кастомизацией, либо живëте с ограничениями и делаете много, но не так дорого за штуку.
vvzvlad
19.07.2024 13:27+1Например,
rounded
, который автор хочет видеть булевским свойством компонента карточки — на самом деле универсальная стилизация, равно подходящая и к карточке, и к кнопке, и к даже к некоторым из объектов UI, которые сегодня ещё никто не изобрёл.А зачем? Ну, т.е. в моем мире это или стандартные штуки типа скругления углов, и они применимы к чему угодно, либо это уже мета-параметры типа loading, про которые никто кроме меня не знает, как они должны выглядеть на карточке/кнопке/неизбретенном еще обьекте UI. Ну, т.е., loading не будет подходить к чему угодно, потому что на карточке я хочу спиннер, а на кнопке — котика. Поэтому мне все равно придется описать как будет выглядеть loading кнопки. А коль я это описываю — то мне несложно и даже удобнее не пытаться впихнуть это в стандартное “loading”(если оно существует), а придумать loading-kitty, и именно его-то и описать, и оперировать именно loading-kitty=true, или loading-dog=true.
ILaeeeee
19.07.2024 13:27+5Мне не нравится делать лишние движения, поэтому управление визуальным стилем только в CSS. Бегать туда и сюда, между HTML и CSS - нафиг этот геморой. Поэтому я отдельно посылаю лучи поноса тем верстальщикам, которые привязывают стилизацию к тегу. А потом приходят какие-нибудь SEOшники и говорят типа того: "нам тут тег A не нужен, давай H2 на H3 меняй" и аналогичное. Вот и бегаешь туда сюда, меняя и HTML и CSS.
Хотя есть и противоположный пример, когда нужно привязывать только к тегам, это места, где клиенты в админке WYSYWIG редактором что-то меняют. Нафиг никому не упало в админке ещё какие то классы выставлять. Вот просто там жирным, цитатку или ещё что-то одним кликом тыц, и визуально это все видно. А на стороне сайта это как-нибудь стилизуется.
gun_dose
19.07.2024 13:27+24У меня есть альтернативное решение проблемы class="big small". А что, если фронтендеры будут сперва немного думать, прежде чем совать все подряд классы в элемент?
doox911
19.07.2024 13:27+5Какправило этим грешат бэкендеры тимлиды, которые быстро делают таску чуть поковыряв палкой код в редакторе и найдя подходящие классы..
gun_dose
19.07.2024 13:27+1Да, конечно, бэкендеры и тимлиды дураки, потому что не понимают все те 800 слоёв абстракций, которые придумали, чтобы вёрстку гордо называть фронтендом.
Зато фронтендеры умные и городят селекторы пятого уровня вложенности, да ещё и через ">", чтобы когда глупый тимлид ради a11y обернёт радиобаттоны в филдсет, то вся форма пренепременнейше разлетится.
dom1n1k
19.07.2024 13:27+6Блин, если читать статью внимательнее, то автор даёт объяснение, что дело не только в банальной невнимательности. Если нужно программно поменять модификатор, то нельзя просто установить ему новое значение - нужно ещё сбросить все другие возможные значения. И лишние строки кода это полбеды, главное здесь то, что нужно знать исчерпывающий список возможных значений. Но что если было три размера, а потом добавили четвертый?
Либо придётся парсить список классов, чтобы убрать лишние по какому-то префиксу.
Либо использовать фреймворк, который не будет менять дом точечно, а просто перерендерит компонент с нуля :) Но автор, очевидно, ищет более общее решение.gun_dose
19.07.2024 13:27Я прекрасно понимаю, о чём пишет автор. Но вот эти все вещи, вроде исчерпывающего списка значений и т.д. - это всё абсолютно легко решается с помощью всё той же внимательности. А решение автора обязывает вместо классов учить все возможные имена атрибутов. Кто-нибудь всё так же ошибётся, только вместо class="small big" будет сочетание атрибутов data-text-size="small" и data-font-size="big". То есть предложенное решение вообще ничего не решает.
Мы тут вплотную подходим к тезису о том, что HTML - это не язык программирования, со всеми вытекающими, а именно с тем, что валидация всего написанного на таких языках - это головная боль пишущего.
dom1n1k
19.07.2024 13:27+1Но вот эти все вещи, вроде исчерпывающего списка значений и т.д. - это всё абсолютно легко решается с помощью всё той же внимательности.
На длинной дистанции плохо решается.
Но что если было три размера, а потом добавили четвертый?
И случилось это спустя время, когда старый сотрудник уже уволился, и задачу передали другому.
gun_dose
19.07.2024 13:27На длинной дистанции неизбежно появятся новые атрибуты для одних и тех же вещей. В итоге всё равно придётся читать документацию, которой скорее всего не будет, и быть внимательным.
dom1n1k
19.07.2024 13:27+2Внимательность - штука хорошая и нужная, но её недостаточно.
Одно дело, если нужно добавить новый модификатор в стили, которые вот здесь, перед глазами - внимательно разобрался и добавил. Возможно, этот код уже легаси, возможно с наслоениями говна, но важно что он известен и локализован.
Но если новый модификатор ещё неявно влияет на некий другой код, который возможно есть (но может и нет) где-то в другом месте (и одном ли?), тут придётся быть ещё и немного телепатом.
Вот автор пытается, помимо прочего, снижать вероятность таких ситуаций.
gun_dose
19.07.2024 13:27Я же привёл наглядный пример, почему подход автора абсолютно не рабочий. Почему вы упорно продолжаете это игнорировать? Или вам не хватает той самой внимательности?
dom1n1k
19.07.2024 13:27+2Кто-нибудь всё так же ошибётся, только вместо class="small big" будет сочетание атрибутов data-text-size="small" и data-font-size="big". То есть предложенное решение вообще ничего не решает.
Это ведь тот самый пример, да?
Так я на протяжении всей ветки пытаюсь донести мысль, что это меньшая часть проблемы, потому что такая ошибка легко обнаруживается и фиксится.А вот разница между
element.classList.add('big')
иelement.dataset.size = "big"
веселее, потому что такой код может находиться вообще в другом месте, а ошибки в нём могут иметь малопредсказуемые проявления (сложение стилей вместо подмены). Ключевая идея тут - уйти от сложения групп стилей (add) к их прямому присваиванию (set).То есть кое-что всё-таки решает. Хотя и не серебряная пуля.
gun_dose
19.07.2024 13:27Это не решает опять же практически ничего, потому что 99% атрибутов задаётся в шаблонах, где программист оперирует HTML-строками, а не DOM-элементами.
То, что вы привели, хорошо работает для смены состояния уже отрисованного элемента. Но во время реализации смены состояния программист прекрасно отдаёт себе отчёт в том, что если нужно сменить цвет, то надо старый удалить, а новый добавить
Spyman
19.07.2024 13:27+5А давайте сразу весь код писать без багов и проблем и так, чтобы он никогда не становился legacy. А то чего как дураки, попишут багов, потом сидят их чинят, и кому это нужно было — непонятно.
gun_dose
19.07.2024 13:27А что плохого в том, чтобы писать код без багов?
Spyman
19.07.2024 13:27+2Ничего, это прекрасно, дёшево и хорошо для пользователей, только в среднем — невозможно.
Буквально парой комментариев ниже, в ветке на похожее замечание, я более подробно раскрыл, что имею в виду, если интересно, можете ознакомиться там, не буду уж тут повторяться.
gun_dose
19.07.2024 13:27А я вот тут же, чуть выше описал, почему метод, предложенный автором вообще никак не решает описанную проблему:
Кто-нибудь всё так же ошибётся, только вместо class="small big" будет сочетание атрибутов data-text-size="small" и data-font-size="big"
vvzvlad
19.07.2024 13:27Только в случае классов это приведет к произвольной каше из частей двух стилей, а во втором это культурно свалится либо в small, либо в big.
gun_dose
19.07.2024 13:27В обоих случаях результат будет абсолютно одинаковый, это же селекторы одинаковой специфичности. Или вы не увидели, что там названия data-атрибутов разные? В этом же вся суть, захочет кто-то поменять размер текста, придумает свой атрибут, и никто не будет проверять весь список уже существующих, будут добавлять дубли с похожими названиями
vvzvlad
19.07.2024 13:27О боже, придется читать документацию, да?
gun_dose
19.07.2024 13:27Проблема не в том, что документацию придётся читать, а в том, что её придётся писать.
А теперь вопрос: стал бы автор вообще писать эту статью, если бы наличие документации к каждому проекту подразумевалось само собой, а её знание всеми разработчиками было обязательным?
vvzvlad
19.07.2024 13:27А, и у вас наверное есть решение, которое обеспечит наличие и знание документации?
gun_dose
19.07.2024 13:27Я не предлагаю никаких решений, я всего лишь указываю на то, что решение, предложенное автором не меняет абсолютно ничего и не решает никаких проблем.
Не говоря уже о том, что сама проблема, которую автор безуспешно пытается решить, выглядит надуманной.
Sadler
19.07.2024 13:27+12Использую, в зависимости от размера проекта, и БЭМ, и CSS Modules, и кастомные атрибуты, и голый CSS без всяких методологий и обвязок (главное, чтобы это подчинялось какой-то логике, а не всё вместе). Я слишком часто переусложнял, теперь стараюсь использовать технологию минимальной сложности, достаточную для решения задачи. Потому что с кодом разбираться придётся не только мне: чем проще вкатиться другим программистам, тем лучше.
Goodzonchik
19.07.2024 13:27Если HTML5 может любой произвольный тег считать чем-то валидным, то Angular, например, будет пытается распарсить это тег как компонент, не сможет его найти и сломается на этапе билда.
DimoniXo
19.07.2024 13:27+5Автор сам придумал проблему и сам героически её решает. Если недопустимо сочетать названия классов big и small для одного элемента - не сочетайте.
Spyman
19.07.2024 13:27+11Если нельзя вызывать методы у null — не вызывайте, если переполнение переменной приводит к ошибке — не переполняйте, если неочистка ссылки приводит к утечке памяти — очищайте, если ваш код со временем станет тяжело поддерживаемым legacy — пишите так, чтобы не стал. И самое главное, если после реализации бизнес придёт к вам с доработками и правками - сделайте их ещё при первой интерации, зачем ждать.
Это я к чему — CSS написал один человек, в голове которого left и right несочетаемые понятия, использовал в разметке страницы другой, для которого это «левый» и «правильный» и они сочетаются, рефакторил третий, который ничего не понял и переименовал right в correct. А потом четвёртый внёс взаимоисключающие свойства на этапе редизайна, и вёрстка в хорошо спрятанных местах сползла в штаны. Все очевидно на короткой дистанции но на длинной далеко не всегда.
delphinpro
19.07.2024 13:27+2HTML-атрибуты можно выразить только один раз, то есть
<div data-size="big" data-size="small">
будет соответствовать толькоdata-size=big
. Это решает проблему инвариантов, на что неспособны другие решения.То есть будет использовано первое значение?
А если прописать два класса
class="big small"
, то тоже будет использовано одно значение, только объявленное последним в css. Точнее оно перебьет стили предыдущего, но результат тот же.Те же яйца, только в профиль =)
dom1n1k
19.07.2024 13:27+3Нет, яйца далеко не те же.
Во-первых, главная идея тут в том, что порядок атрибутов в html однозначно определяет, кто кого побеждает (что вижу, то и получаю), а порядок классов нет. Порядок стилей задаётся не в коде перед глазами, а где-то в другом месте, причем часто даже не в одном, а в нескольких разных (проведи расследование, чтобы выяснить это).
Во-вторых, стили классов не обязательно заменят друг друга, они могут сложиться.delphinpro
19.07.2024 13:27+2Не согласен. Если мы говорим о модификаторах, а в статье упор как раз на этот кейс, то каждый такой селектор будет определять один и тот же набор свойств, и уж складываться они точно не будут. И определяются такие вещи как правило рядом друг с другом, а вовсе не разбросаны по проекту.
Соглашусь лишь с тем, что глядя только на разметку, действительно невозможно определить какой стиль будет применен, для этого нужно знать порядок объявления классов в css.
dom1n1k
19.07.2024 13:27+4Модификаторы тоже не застрахованы. Потому что big/small это слишком базовый пример, а жизнь разнообразнее. Вот прямо сходу что пришло на ум:
Тултип с модификатором, определяющим положение стрелочки. Там будут комбинации свойств left/right/top/bottom. Они могут сложиться.
Многочисленные модификаторы кнопок (primary/secondary/success..., hover/disabled/active...) — там разница не ограничивается значением цвета. Там могут быть отличия в обводках, тенях, прозрачности и пр. И это тоже может сложиться. Теоретически можно прописать все свойства для всех и менять только значения переменных, но это техника со своими минусами.
Модификаторы могут зависеть от медиа-запросов, причем опять же не только по значениям, но и наличию/отсутствию свойств. Например, в светлой/темной теме кнопки часто отличаются не только цветом, но и присутствием той же теньки или обводки. Ну потому что особенности человеческого восприятия так работают. И я сомневаюсь, что все ваши медиа-запросы описаны прямо рядом.
delphinpro
19.07.2024 13:27Да, пожалуй, вы правы.
И я сомневаюсь, что все ваши медиа-запросы описаны прямо рядом.
У меня медиазапросы всегда пишутся рядом с тем классом, который они переопределяют.
dom1n1k
19.07.2024 13:27+1Да, я тоже пишу медиа-запросы в том же модуле, к которому они относятся. Но с учетом всех модификаторов, состояний, переменных и прочего добра суммарно блок/компонент может занимать несколько экранов кода. И если какие-то переопределения разнесены на пару экранов - уже можно считать, что это не совсем рядом.
Плюс ещё бывает, что используются какие-то общие миксины, которые описаны отдельно. Ну то есть стремиться к компактности стилей конечно нужно, но это не достижимо на 100%.
dom1n1k
19.07.2024 13:27+3Я размышлял на эту тему и тоже приходил к похожим мыслям - модификаторы в атрибутах объективно имеют некоторые преимущества. Но, конечно, выглядит всё это странно, непривычно, в прод такое тащить страшно, потому что народ не одобрит... Хотя немного экспериментировал в верстке для себя.
В целом соглашусь, что штука спорная, но здравое зерно в рассуждениях автора точно есть.
alexnozer
19.07.2024 13:27+4Например, можно изобрести своё собственное пространство имён для работы с параметрами CSS, что повышает читаемость
Вообще по стандарту HTML нельзя. У элемента могут быть указаны глобальные атрибуты, специфические для этого элемента атрибуты и любые
data-*
атрибуты. Любые другие атрибуты с точки зрения стандарта считаются невалидными. Из-за лояльности HTML как-бы и ладно, ничего не будет, а селекторы по атрибуту и JS сработают. Но всё-таки так нельзя делать.любой тег, не распознанный парсером — это неизвестный элемент, который можно свободно стилизовать как угодно
Не совсем так. Элемент считается неизвестным
HTMLUnknownElement
, если он не является одним из стандартных или не содержит в названии как минимум одного дефиса. Если дефис есть, то парсер рассматривает элемент как потенциальный пользовательский элемент, который пока ещё не зарегистрирован. В таком случае он будетHTMLElement
.
AlekseyStepp
19.07.2024 13:27+2Автор упорно пытается решить проблему с одновременным big и small, но эта какая-то надуманная высланная из пальца проблема. Если классы рендерит бэкенд или шаблонизатор на фронте, то такой ситуации не возникает. Если классы захардкожены, и по ошибке написаны оба, это просто будет заметно в браузере и сразу исправлено. Если это незаметно и применялись нужные в этом месте свойства, да и бог с ним.
DennisP
19.07.2024 13:27+3Это чего вдруг в 1996 году многие мониторы были чёрно белыми? В то время VGA уже считался устаревшим, а в ходу был SVGA 1024*768 256 цветов
dom1n1k
19.07.2024 13:27+1Ну насчет "многих" можно спорить, но ч/б действительно встречались. Да, считались уже устаревшими, но это не мешало им быть и использоваться.
DennisP
19.07.2024 13:27В наших условиях это могда быть разве что ЕС-ка в каком-то совсем Богом забытом НИИ, и то сомнительно. На Западе вообще сложно себе представить, что во время 200 Мгц Pentium и Windows 95 кто-то мог работать на каком-нить IBM PC XT образца 80-х годов, да еще и использовать его для разработки новых стандартов в новейшем на тот момент направлении - интернете.
Revolt-or-die
19.07.2024 13:27Особенно смешно, что дальше он пишет (преувеличиваю) «а вот уже в 1997 все стали цветными и выпустили новый стандарт»
isumix
19.07.2024 13:27Очень интересный подход! Возьму себе на вооружение. Это это выглядит более удобно, кратко и семантически корректно и с нативной производительностью.
Мне тоже уже порядком надоели всевозможные штучки типа (BEM,SASS,TAILWIND,CssInJs), которые привносят дополнительную когнитивную нагрузку и бьют по производительности. Когда нативный HTML/CSS уже научился делать все вещи для которых они создавались изначально. Пользуюсь только CssModules.
ПС: Также разрабатываю сейчас замену переусложенному React/Solid https://github.com/fusorjs/dom
monochromer
19.07.2024 13:27+2Вы хотя бы не тупо переводите статьи, а делайте их редактуру/ревью. Например,
.Card:is(.big) имеет равную специфичность с .Card
Это не так.
LittleMeN
19.07.2024 13:27+2<div class="card" data-border-collapse="left right"></div>
Мне показалось, или вы тут классы переизобрели?
Desprit
19.07.2024 13:27Частично проблема
class="big small"
решается линтерами. Да, если это прямо пользовательские классы, то не поможет, но если что-то из разряда tailwind, то IDE укажет на лишний класс.
aamonster
19.07.2024 13:27+1Кажется, 70% текста из начала статьи можно было б скипнуть, если просто понять (и сказать), что классы в CSS – это не классы из ООП, а скорей классы (или множества) из математики.
Тогда и проблемы вроде конфликта small и big стали бы более понятными (и их можно было бы записать в виде ограничения small ∩ big = ∅), и его предложения было бы проще сформулировать и проанализировать.
davidaganov
19.07.2024 13:27+1Так как я разрабатываю чаще всего под Vue, то с такой проблемой давно уже не сталкивался. Самое приятное это делать в компоненте все аттрибуты через пропсы, условно, кнопка это <UiButton /> с классом по умолчанию <button class="btn">, а все остальные классы прокидываются в типизированные пропсы, например type="primary|stroke|white", size="sm|md|lg" и каждый тип имеет ограниченное количество вариантов которые можно прокинуть, благодаря чему решается проблема описанная в статье. (а тайпскрипт сильно в этом помогает, подчеркивая неверный тип пропса). Но способ делать это нативно через аттрибуты - интересный. Страшно, но интересно)) Страшно потому что такого ранее не видел и непонятно как это внедрить так чтобы не побили за это
aigen31
19.07.2024 13:27Если использовать Tailwind в IDE, допустим VSCode, то там есть официальное расширение Tailwind Intellisence, которая предупреждает об использовании взаимоисключающих атомарных классах
thebazel
19.07.2024 13:27Мое мнение по вопросу построения селекторов для UI-компонентов.
Взаимоисключающие селекторы не используются совместно по договоренности внутри команды и по здравому смыслу.
Компонент имеет уникальный для себя в рамках текущей компонентной системы класс, желательно с префиксом, например, ".ui-": .ui-button, .ui-card, .ui-slider, .ui-popup. Префикс ui-системы позволяет однозначно понять, что увиденный в коде класс относится к ui-системе, а не является проектным, созданным для использования в конкретном проекте.
Различные вариации компонента могут быть выражены классами или data-атрибутами. Выбираем то, что удобно команде и лучше подходит для конкретного случая. Опять же желателен короткий префикс (по сути namespace). Например, .ui-xl, .ui-sm и т.п., .ui-primary, .ui-secondary и т.п. Для свойств состояния (булевых свойств) используются префиксы "-is" и "-has". Например, .ui-is-disabled, .ui-is-active, .ui-is-loading и т.п.
Вопрос специфичности селекторов решается с помощью :where. Таким образом будут подобные селекторы: .ui-button:where(.ui-primary.ui-disabled), .ui-card:where(.ui-xl) и т.п. Это удобно, так как :where не меняет специфичность селектора и поддерживается всеми современными браузерами (Safari 14+).
В UI системе удобно создать набор shortcut классов (tailwind классов) для некоторых (именно части) свойств. Например, для отступов .ui-margin-top-8, .ui-margin-top-12 и т.п., для обложки .ui-border-top, ui-border-y и т.п.
Учитывая сохранение специфичности через :where можно будет легко переопределить (или дополнить) стили в нужных местах проекта. Например, добавить отступы к карточке снизу добавив класс .ui-margin-bottom-24.
Если вашей команде нравится использовать data-атрибуты вместо классов, то совмещая это с :where ничего не меняется, например, .ui-button:where([data-variant="primary"]), .ui-button:where([data-is-disabled]). Но лично я считаю этот вариант более громоздким визуально и придерживаюсь той позиции, что data-атрибуты - это про данные, а не про стилизацию.
modulor
19.07.2024 13:27<my-card data-size="big"></my-card>
Вы не задумывались о том, как поисковая машина Google проиндексирует контент, размеченный вашими собственными HTML-тегами? Возможно, такой контент будет либо вообще проигнорирован, либо обработан некорректно?
TerrorDroid
Интересно как это отразится на производительности всего этого добра, ибо может оказаться что наличие этих атрибутов и кастомные псевдоселекторы в итоге вызовую резкий рост нагрузки или ещё чего-то, ибо никто не предусматривал, что их будут настолько агрессивно использовать. Было бы классно увидеть тесты какие-то, сверстав единтичные макеты разными способами.
atomic1989
Явно потери будут. Вопрос на сколько сильны. Из опыта использования переменных в css думаю 99% современных пользователей и не заметят разницы. Также вопрос производительности отнимает много ресурсов, т. е. денег. Если заказчик готов тратить на это ресурсы, то это одно дело, если нет, то оно надо париться за 0.5 сек первичной отрисовки). Как по мне лучше выбрать более комфортные механизмы разработки. Классы, по 1000 раз переопределяющие свойства элемента, тоже могут снижать производительность