Несколько способов упростить CSS в 2023 году

Источник: «A Few Ways CSS Is Easier To Write In 2023»
Мы переживаем некий ренессанс CSS: новые возможности, техники, эксперименты и идеи появляются в таком количестве, какого мы не видели со времён "CSS3". Легко почувствовать себя подавленным, когда, кажется, что твоя профессия развивается с бешеной скоростью, но Джефф Грэм (Geoff Graham) считает, что "современный" CSS в 2023 году фактически сделал CSS "проще" в написании.

Некоторое время назад я ознакомился с рядом "современных" возможностей CSS и открыто оценил, действительно ли они повлияли на то, как я пишу стили.

Предупреждение о спойлере: Ответ — не очень. Немного, но не настолько, чтобы стили, которые я пишу сегодня, выглядели бы чужеродно, если бы их сравнить со стилями двух-трёхлетней давности.

Это был интересный мыслительный процесс, но скорее академический, чем практический. Продолжая размышлять о том, как я подхожу к CSS сегодня, я понял, что различия гораздо более тонкие, чем я мог ожидать или даже заметить.

Писать на CSS стало проще, а не по другому.

И дело не в одной кричаще новой функции, которая меняет все — скажем, Каскадные Слои (Cascade Layers) или новые цветовые пространства, — а в том, что многие новые функции работают вместе, делая мои стили более лаконичными, устойчивыми и даже немного защитными.

Позвольте мне объяснить.

Эффективные группы стиля

Вот краткое описание. Вместо связывания состояний :hover и :focus через запятую, использование нового псевдокласса :is() позволяет сделать это более читабельным однострочником:

/* Традиционно */
a:hover,
a:focus
{
/* Стили */
}

/* Более читабельно */
a:is(:hover, :focus) {
/* Стили */
}

Я говорю более читабельно, потому что это не совсем более эффективно. Мне просто нравится, как это читается в обычном разговоре: Ссылка, которая находится в состоянии наведённого курсора или фокуса, стилизуется следующим образом…

Конечно, :is() может сделать селектор более эффективным. Вместо того чтобы придумывать какой-то безумный пример, вы можете посмотреть пример из MDN, чтобы убедиться в эффективности :is(), и порадоваться.

Центрирование

Это классический пример, верно? "Традиционный подход" к выравниванию элемента по центру его родительского контейнера обычно не вызывает сомнений. Мы прибегаем к помощи разновидности margin: auto, чтобы подтолкнуть элемент со всех сторон внутрь, пока он не расположится ровно посередине.

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

/* Традиционно */
margin-left: auto;
margin-right: auto;

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

/* Проще! */
margin-inline: auto;

И знаете, что ещё? Тот простой факт, что в этом примере осуществлён тонкий переход от физических свойств к логическим, означает, что этот маленький фрагмент не только не уступает по эффективности броскому margin: auto, но и устойчив к изменениям режима написания. Если страница вдруг окажется в вертикальном режиме справа налево, он все равно выдержит, автоматически центрируя элемент по направлению содержимого, когда направление содержимого идёт вверх-вниз, а не влево и вправо.

Настройка режимов написания, в целом

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

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

Традиционно мы можем написать один набор стилей для любого "нормального" направления письма, а затем нацелить его на режим письма на уровне HTML с помощью [dir="rtl"] или чего-то ещё. Однако сегодня мы забываем обо всем этом и вместо этого используем логические свойства. Таким образом, макет следует за режимом написания!

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

/* Традиционно */
body {
margin-left: 1rem;
}

body[dir="rtl"] {
margin-left: 0; /* сброс левого margin'а */
margin-right: 1rem; /* применяется к правому */
text-align: right; /* сдвинуть текст в другую сторону */
}

…это не нужно, если мы работаем с логическими свойствами:

/* Намного проще! */
body {
margin-inline-start: 1rem;
}

Удаление лишних отступов

Вот ещё один распространённый паттерн. Наверняка вам приходилось использовать неупорядоченный список ссылок внутри <nav> для основной или глобальной навигации по проекту.

<nav>
<ul>
<li><a href="/products">Products</a></li>
<li><a href="/products">Services</a></li>
<li><a href="/products">Docs</a></li>
<!-- etc. -->
<ul>
</nav>

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

/* Традиционно */
li {
display: inline-block;
}

Между элементами списка необходим отступ. Ведь они уже не занимают всю доступную ширину родительского элемента, поскольку ширина inline-block элементов равна только ширине содержимого, которое они содержат, плюс все border, padding, margin и offset'ы, которые мы добавляем. Традиционно это означало, что мы обращались к margin, как и к центрированию, но только к составному свойству margin, которое применяло отступ в нужном нам направлении, будь то margin-left/margin-inline-start или margin-right/margin-inline-end.

Предположим, что мы работаем с логическими свойствами и хотим получить отступ в конце списка элементов в направлении содержимого:

/* Традиционно */
li {
display: inline-block;
margin-inline-end: 1rem;
}

Но подождите! Теперь у нас есть отступ у всех элементов списка. Правда, для последнего элемента списка отступ не нужен, потому что после него нет других элементов.

Три стилизованные ссылки слева направо с выделением дополнительного поля у последнего элемента.
Три стилизованные ссылки слева направо с выделением дополнительного поля у последнего элемента.

В подавляющем большинстве ситуаций это, может быть, и неплохо, но это оставляет макет уязвимым. Что, если позже мы решим отобразить рядом с <nav> ещё один элемент? Вдруг мы столкнёмся с лишними отступами, которые могут повлиять на стиль этого нового элемента. Это одна из форм технического долга.

Было бы лучше убрать это и решать проблему отступов между элементами без этой проблемы. Мы можем обратиться к более современной функции, например, к псевдоклассу :not(). Таким образом, мы можем исключить последний элемент списка из участия в вечеринке margin.

/* Немного современнее */
li {
display: inline-block;
}
li:not(:last-of-type) {
margin-inline-end: 1rem;
}

Ещё проще? Ещё современнее? Можно воспользоваться свойством margin-trim, которое, будучи применённым к родительскому элементу, обрезает лишние интервалы, как хорошая стрижка, эффективно сворачивая поля, позволяющие дочерним элементам прилегать к краям родительского элемента.

/* Проще, современнее */
ul {
margin-trim: inline-end;
}

li {
display: inline-block;
margin-inline-end: 1rem;
}

Прежде чем поднимать вилы, отметим, что функция margin-trim является экспериментальной и на момент написания этой статьи поддерживается только в Safari. Так что, да, это современная разработка, не совсем та, которую стоит сразу же отправлять в продакшен. Если что-то "современное", это ещё не значит, что это подходящий инструмент для работы!

Фактически, существует, вероятно, даже лучшее решение без всех этих оговорок, и оно находится прямо у нас под носом: Flexbox. Превращение неупорядоченного списка в гибкий контейнер отменяет стандартный блочный поток элементов списка, не изменяя их отображения, что позволяет получить желаемую компоновку "бок о бок". Кроме того, мы получаем доступ к свойству gap, которое можно рассматривать как margin со встроенной функцией margin-trim, поскольку оно применяет отступы только между дочерними элементами, а не по всем сторонам от них.

/* Менее современно, но ещё проще! */
ul {
display: flex;
gap: 1rem;
}

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

Раз уж мы затронули тему стилизации списков, которые не похожи на списки, стоит отметить, что обычная задача удаления стилей списков как упорядоченных, так и неупорядоченных (list-style-type: none) имеет побочный эффект в Safari, который лишает элементы списка роли доступности по умолчанию. Один из способов "исправить" это (если вы считаете это разрушающим изменением) — добавить роль обратно в HTML a là <ul role="list>. Мануэль Матузович (Manuel Matuzović) предлагает другой подход, который позволяет нам остаться в CSS, удалив тип стиля списка со значением пустых кавычек:

ul {
list-style-type: "";
}

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

Сохранение пропорций

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

/* Традиционно */
height: 500px;
width: 500px;

Или, возможно, вам нужно, чтобы размер элемента немного изменялся, поэтому вы предпочитаете относительные единицы измерения. В этом случае сложно использовать что-то вроде процентов, поскольку такое значение, как 50%, относится к размеру родительского контейнера элемента, а не к самому элементу. Тогда родительский элемент нуждается в фиксированных размерах или в чем-то другом, полностью предсказуемом. Это почти бесконечный цикл попыток сохранить пропорцию 1:1 одного элемента путём задания пропорции другого элемента, содержащегося в нем.

Так называемый "Padding Hack", безусловно, был умным решением, и на самом деле это не столько "хак", сколько демонстрация владения моделью CSS Box на уровне мастер-класса. Его возникновение датируется 2009 годом, но Крис Койер (Chris Coyier) хорошо объяснил его в 2017 году:

Если мы установим высоту элемента равной нулю (height: 0;) и не будем иметь никаких border'ов, то padding будет единственной частью модели box, влияющей на высоту, и мы получим наш квадрат.

Крис Койер

Как бы то ни было, для этого потребовалось много изобретательного CSS. Давайте поприветствуем рабочую группу CSS, которая предложила гораздо более элегантное решение: свойство aspect-ratio.

/* Проще! */
aspect-ratio: 1;
width: 50%;

Теперь мы имеем идеальный квадрат независимо от того, как ширина элемента зависит от его окружения, что обеспечивает нам более простой и эффективный набор правил, более устойчивый к изменениям. В последнее время я часто использую aspect-ratio вместо явных значений height или width в своих стилях.

Эффекты при наведении на карточку

Это не совсем CSS-специфично, но стилизация эффекта наведения на карточку традиционно представляет собой сложный процесс, когда мы оборачиваем элемент в <a> и подключаемся к нему, чтобы соответствующим образом стилизовать карточку при наведении. Но с помощью :has() — теперь поддерживаемого во всех основных браузерах, начиная с Firefox 121! — мы можем поместить ссылку в карточку как дочерний элемент, как это и должно быть, и стилизовать карточку как родительский элемент, когда на неё наводится курсор.

.card:has(:hover, :focus) {
/* Стилизовано! */
}

Это намного круче, удивительнее и легче для чтения, чем, скажем:

a.card-link:hover > .card {
/* Стиль что?! */
}

Создание и поддержка цветовых палитр

Давным-давно я рассказывал о том, как я называю цветовые переменные в своих Sass-файлах. Суть в том, что я определяю переменные шестнадцатеричными значениями, как бы в более современном контексте, используя переменные CSS вместо Sass:

/* Традиционно */
:root {
--black: #000;
--gray-dark: #333;
--gray-medium: #777;
--gray-light: #ccc;
--gray-lighter: #eaeaea;
--white: #fff;
}

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

/* Проще поддерживать! */
:root {
--primary-color: #000;
--gray-dark: color-mix(in srgb, var(--primary-color), #fff 25%);
--gray-medium: color-mix(in srgb, var(--primary-color), #fff 40%);
--gray-light: color-mix(in srgb, var(--primary-color), #fff 60%);
--gray-lighter: color-mix(in srgb, var(--primary-color), #fff 75%);
}

Это не совсем конвертация 1:1. Но мне лень делать это в реальности, но вы поняли идею, верно? Верно?! "Простой" способ может показаться сложнее, но если вы хотите изменить основной цвет, обновите переменную --primary-color и на этом можно закончить.

Возможно, лучшим подходом было бы изменение названия --primary-color на --grayscale-palette-base. Таким образом, мы сможем использовать тот же подход для многих других цветовых шкал, чтобы создать надёжную систему цветов.

/* Проще поддерживать! */
:root {
/* Базовая палитра */
--black: hsl(0 0% 0%);
--white: hsl(0 0% 100%);
--red: hsl(11 100% 55%);
--orange: hsl(27 100% 49%);
/* etc. */

/* Палитра оттенков серого */
--grayscale-base: var(--black);
--grayscale-mix: var(--white);

--gray-100: color-mix(in srgb, var(--grayscale-base), var(--grayscale-mix) 75%);
--gray-200: color-mix(in srgb, var(--grayscale-base), var(--grayscale-mix) 60%);
--gray-300: color-mix(in srgb, var(--grayscale-base), var(--grayscale-mix) 40%);
--gray-400: color-mix(in srgb, var(--grayscale-base), var(--grayscale-mix) 25%);

/* Красная палитра */
--red-base: var(--red);
--red-mix: var(--white);

--red-100: color-mix(in srgb, var(--red-base), var(--red-mix) 75%);
/* и т.д. */

/* Повторить по необходимости */
}

Управление цветовыми системами — это целая наука, поэтому не стоит воспринимать все вышесказанное как рецепт того, как это делается. Дело в том, что сегодня мы имеем более простые способы работы с ними, в то время как раньше нам приходилось обращаться к инструментарию, не связанному с CSS, чтобы получить доступ к переменным.

Управление длиной строки

Две новые вещи в CSS, которые мне очень нравятся:

Что касается первого варианта, то мне он нравится тем, что позволяет устанавливать максимальную ширину контейнеров, особенно тех, которые предназначены для размещения длинного контента. Согласно общепринятым представлениям, идеальная длина строки текста составляет 50-75 символов в строке, в зависимости от источника. В мире, где размер шрифта может адаптироваться к размеру контейнера или области просмотра, предсказание количества символов в строке — это игра в угадайку с движущейся целью. Но если мы зададим максимальную ширину контейнера, которая никогда не превысит 75 символов через ch, и минимальную ширину, которая заполнит большую часть, если не всю, ширины контейнера в меньших контекстах, эта проблема отпадёт, и мы сможем обеспечить комфортное пространство для чтения в любой точке разрыва, причём без использования медиа.

article {
width: min(100%, 75ch);
}

То же самое происходит и с заголовками. Мы не всегда располагаем необходимой информацией — размер шрифта, размер контейнера, режим написания и так далее — для создания хорошо сбалансированного заголовка. Но знаете, у кого она есть? У браузера! Использование нового значения text-wrap: balance позволяет браузеру решать, когда необходимо перенести текст таким образом, чтобы предотвратить появление "сиротливых" слов или грубого дисбаланса длин строк в многострочном заголовке. Это ещё один из тех случаев, когда мы ждём полной поддержки браузера (в данном случае Safari). Тем не менее это одна из тех вещей, которые я могу с комфортом запустить в продакшн в качестве прогрессивного улучшения, поскольку никаких негативных последствий не будет ни с ним, ни без него.

Однако хотелось бы предостеречь тех, у кого может возникнуть соблазн применить это правило ко всему тексту:

/* 👎 */
* {
text-wrap: balance;
}

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

/* 👍 */
article:is(h1, h2, h3, h4, h5, h6) {
text-wrap: balance;
}

text-wrap: pretty — ещё один вариант, находящийся в стадии эксперимента. Похоже, что он похож на balance, но позволяет браузеру пожертвовать некоторым выигрышем в производительности ради компоновки. Однако я с ним не работал, а поддержка его ещё более ограничена, чем balance.

А как у вас

Это лишь те возможности, которые предлагает CSS в конце 2023 года, и которые, как мне кажется, оказывают наибольшее влияние на то, как я пишу стили сегодня. По сравнению с тем, как я подходил к подобным ситуациям в те времена, когда во время написания таблицы стилей мне приходилось идти в гору по двум путям, создавая таблицу стилей.

Я могу назвать и другие возможности, которые я использовал, но не полностью внедрил в свой инструментарий. К ним можно отнести следующие:

Что скажете вы? Я знаю, что был период, когда некоторые из нас открыто задавались вопросом, не слишком ли много CSS в наши дни, и высказывали мнение, что кривая обучения CSS становится сложным барьером для новичков. Какие новые возможности вы используете, и помогают ли они вам писать CSS новыми и другими способами, которые облегчают чтение и поддержку кода, или, возможно, "переучивают" ваше представление о стилях?

Дополнительные материалы

Предыдущая Статья

Защита от ленивой загрузки не перехватывает все N+1 запросы

Следующая Статья

Новое в Symfony 6.4: Улучшения DX (часть 2)