Это руководство по написанию поддерживаемого, производительного и соответствующего стандартам CSS.
- 1. Основы
- 2. Наследование и каскад
- 3. Селекторы и область применения
- 4. Интерактивность
- 5. Дизайн-токены и тематизация
- 6. Отзывчивый дизайн
- 7. Типографика
- 8. Визуальные эффекты
- 9. Переходы и анимация
- 10. Генерируемый контент
Избегайте дублирования. Используйте переменные вместо повторения, но, по-возможности, используйте встроенные вещи, такие как:
currentColor, вместо определения переменной и установке ейcolor;- ключевое слово
inherit, вместо определения переменной у родителя, и ее использования для родителя и потомка; - единицы измерения
em, вместоfont-size: var(--size); - единицы измерения
cqw/cqh(или их логические версии -cqi/cqb), вместо повторения значений блочной модели; - дублирование кода - это не дублирование знаний. Цель - надежность и поддерживаемость, а не экономия символов;
- используйте логические свойства и значения, вместо физических (например,
margin-inline-start, вместоmargin-left) для адаптирования стилей к разным направлениям письма и ориентациям. Даже если автор не планировал локализацию, внешние инструменты перевода часто отображают переведенный текст в контексте; - не используйте логические свойства без разбора - спрашивайте себя "хочу ли я, чтобы это переворачивалось в RTL?" - если ответ "нет", используйте физическое свойство;
- учитывайте разные режимы отображения (темный, высококонтрастный режим), разные размеры области просмотра и разные режимы ввода (касания, клавиатура, указатель).
Не используйте методологию BEM (Block, Element, Modifier - блок, элемент, модификатор) для управления специфичностью. Используйте современные возможности CSS, такие как каскадные слои (cascade layers) и :where() для обеспечения большей предсказуемости каскадного поведения и следования намерениям автора.
Используйте каскадные слои (@layer) для явного определения приоритетных зон (например, reset, base, theme, components, utilities), и определения их порядка (например, @layer reset, base, theme, components, utilities;).
Внутри каждого слоя используйте :where(), чтобы селекторы конкурировали только на основе значимых сигналов, а не случайных фильтров (граничные случаи :not(), далекие предки и т.д.), или для одноразовых легко переопределяемых значений по умолчанию.
Используйте такие ключевые слова, как inherit, initial, unset или revert, вместо явных значений для улучшения поддерживаемости и лучшего выражения намерений. Примеры:
- при определении перехода потомка, который должен совпадать с родительскими свойствами
transition-*, вместо повторения свойств переходов у потомка, используйтеtransition: inherit(уменьшает дублирование, улучшает поддерживаемость); - используйте
initialдля сброса свойства к его начальному значению, вместо явного определения значения (более четкое выражение намерений).
Современные нативные браузерные селекторы уменьшают необходимость в препроцессорах и отслеживании состояния с помощью JS.
- Используйте
:has()для стилизации предков на основе состояния потомков, вместо управления классами с помощью JS (например,label:has(:checked)вместо классаlabel.has-checked). - Не используйте вложенные
:has()или псевдоэлементы в них (ограничение браузерного API). - Используйте
:nth-child(<An+B> of <selector>)для стилизации каждого n-го элемента определенного типа. Например,details:nth-child(1 of [open])будет применяться к первому найденному открытому элементу<details>, аdetails[open]:first-childбудет применяться только к первому потомку, если он открыт.
Не дублируйте правила CSS в качестве резерва для псевдоклассов, которые могут не поддерживаться - используйте :is() или :where() и получите преимущество их мягких правил парсинга.
/* ПЛОХО: дублирование правил, вместо использования `:where()` */
[popover]:popover-open {
/* стили нативных поповеров */
}
[popover].\:popover-open {
/* те же стили для полифиллов */
}
/* ХОРОШО */
[popover]:where(:popover-open, .\:popover-open) {
/* все стили в одном правиле */
}Не используйте это для псевдоэлементов, поскольку они не поддерживаются в :is() и :where().
Пишите селекторы способом, явно выражающим ваши намерения.
Когда намерение состоит в исключении определенных состояний или элементов, которые фундаментально нерелевантны, используйте :not().
Например, для применения нижних границ между элементами списка не делайте так:
.fancy-list li {
border-bottom: 1px solid silver;
}
.fancy-list li:last-child {
border-bottom: none;
}Это может случайно переписать желаемый набор border-bottom из другого правила. Реальное намерение состоит в неприменении нижней границы к последнему li. Приведенный выше код плохо выражает это намерение. Следующий код делает это более явно:
.fancy-list li:not(:last-child) {
border-bottom: 1px solid silver;
}Аналогично, не делайте так:
button:hover {
background: var(--color-blue);
}
button:disabled {
background: var(--color-neutral);
}Если мы поменяем порядок двух правил, то получим изменение цвета фона при наведении у заблокированных кнопок! Лучше делайте так:
button:hover:not(:disabled) {
background: var(--color-blue);
}
button:disabled {
background: var(--color-neutral);
}Это работает независимо от порядка, поскольку первое правило не является чрезмерным.
Хотя :not() + селекторы потомков могут исключить поддеревья, это плохо работает для глубоко вложенных структур. Например, .card :not(.content *) не будет работать ожидаемо для вложенных карточек. @scope исправляет это, поскольку учитывает иерархическую близость:
@scope (.card) to (.content) {
/* стили для элементов внутри .card, но не внутри .content */
}Это будет работать ожидаемо даже для вложенных карточек.
Такое допустимо:
button {
background: var(--color-neutral);
}
button.primary {
background: var(--color-blue);
}Оба правила выражают легитимное намерение: все кнопки изначально нейтральные, но первичные синие.
Не используйте глобальные сбросы (стили на *), поскольку они не могут быть перезаписаны веб-компонентами или низкоприоритетными каскадными слоями (без !important). Вместо этого, применяйте стили сброса к определенным типам элементов и/или условиям.
Используйте нативную вложенность CSS для группировки связанных стилей с целью улучшения их поддерживаемости и читаемости.
Используйте @scope, вместо вложенности, когда близость имеет большее значение, чем чистая специфичность. Это распространено в селекторах, которые могут быть вложены в любом порядке, но ближайший подходящий (в элементе -> порядок потомков) должен побеждать, например, классы темы.
Например, это не будет работать ожидаемо:
.dark .invert { color-scheme: light }
.light .invert { color-scheme: dark }Если .invert вложен и в .dark, и в .light, он будет всегда разрешаться в темный режим, поскольку оба правила имеют одинаковую специфичность. Использование @scope исправляет это:
@scope (.dark) {
.invert { color-scheme: light }
}
@scope (.light) {
.invert { color-scheme: dark }
}- Используйте
:focus-visibleдля определения кастомных колец фокуса, а не:focus. - Не удаляйте дефолтные браузерные кольца фокуса (с помощью
outline: none) без предоставления альтернативных видимых стилей фокуса. - Используйте
outlineвместо других свойств (например,box-shadow) для колец фокуса. При использованииbox-shadowдля колец фокуса, предоставьте основанный наoutlineрезерв для высококонтрастного режима с помощью медиа-запросаforced-colors. - Используйте
outline-offsetдля визуального отделения кольца от элемента.
- Интерактивные элементы должны быть, как минимум, 24×24 пикселей CSS (WCAG 2.5.8 AA). Используйте
min-block-size/min-inline-sizeили паддинг, вместоwidth/height, чтобы контент мог расширять цель, но не сужать ее. - Не используйте
touch-action: noneдля кастомных жестов - это отключает прокрутку страницы сквозь элемент. Ограничивайтесь только необходимой осью:pan-yдля горизонтальных свайпов (страница по-прежнему прокручивается вертикально),pan-x- для вертикальных. Используйтеnoneтолько для элементов, для которых нативные касания не имеют смысла (например, холст для рисования).
Используйте кастомные свойства CSS на :root для определения основных переменных дизайна (цвета, шрифты, размеры и т.д.), которые используются во всем дизайне, для визуальной согласованности и для распределения UI между командами.
Не определяйте нетривиальные стилевые значения инлайн. Например, background: transparent или padding: 0 - это нормально, а background: #f06 или padding: .3em - нет. Исключением являются случаи, когда сохранение малого объема кода и его простоты важнее долгосрочной поддержки и эволюции, как в случае с тестами.
Как правило, переменные организуются по уровням, когда каждый следующий уровень основан на предыдущем. Например:
- Уровень 1: литеральные дизайн-токены (например,
--color-blue-10,--color-gray-90,--font-sans-serif,--size-xlи т.д.). - Уровень 2: семантические дизайн-токены (например,
--color-accent,--color-neutral,--font-body,--font-headingи т.д.). - Уровень 3: общие дизайн-токены UI (например,
--ui-border,--surface-bg-subtleи т.д.). - Уровень 4: дизайн-токены компонентов (например,
--button-bg-primary-hover,--button-border-color-secondaryи т.д).
Чем меньше масштаб случая, тем меньше уровней ему требуется. Например, быстрое демо или игрушечное приложение может обойтись одним уровнем. Не переусердствуйте. Проверяйте существующие соглашения о наименованиях и уровнях перед изобретением собственных.
- Используйте
color-scheme: light darkна:rootдля поддержки темного режима, который автоматически адаптируется к настройкам системы. Вы также можете определитьcolor-schemeна отдельных элементах для применения другого значения для конкретного поддерева. - Используйте
light-dark()для альтернатив, которые автоматически применяются на основеcolor-schemeэлемента. Обычно, это делается на уровне токенов 2 или 3. - Важно: при использовании
light-dark()на унаследованном свойстве<color>, оно разрешится в определенный цвет на основеcolor-schemeэтого элемента и наследуется как этот разрешенный цвет, а не как значениеlight-dark(). Оно не будет адаптироваться к перезаписямcolor-schemeна уровне потомков. Для сохранения динамичности цветовых токеновlight-dark()разрешайте их как можно позже, передавая их как незарегистрированные кастомные свойства, и не полагайтесь на наследование значений цвета в границахcolor-scheme.
В режиме принудительных цветов (высокий контраст в Windows), браузер перезаписывает цвета автора системными ключевыми словами и удаляет background-image, box-shadow и border-image.
- Определяйте системные цветовые резервы для токенов цвета с помощью
@media (forced-colors: active). - Не полагайтесь на
background-image,box-shadowилиborder-imageдля определения границ, разделителей или состояния - они исчезнут в режиме принудительных цветов (и часто при печати). В противном случае, убедитесь в наличии альтернатив, таких какoutlineилиborderс системными цветовыми ключевыми словами (CanvasText,LinkText,ButtonText,Highlight,GrayTextи т.д.). - Используйте
forced-color-adjust: none, когда цвет является существенной информацией (подсветка синтаксиса, инпут выбора цвета). Не используйтеforced-color-adjust: noneпросто для сохранения эстетичности.
Перед динамической генерацией оттенков, попробуйте использовать существующий, предопределенный дизайн-токен. Это обеспечивает больший дизайнерский контроль и согласованность.
При динамической генерации более светлых и темных цветов:
- Не ограничивайтесь регулировкой канала яркости в
oklch/oklabилиlch/lab, например,oklab(from var(--primary) 0.9 a b). Хотя это теоретически верный способ, браузеры пока не реализовали отображение гаммы (gamut mapping), поэтому результирующий цвет непредсказуем. - Вы можете использовать
color-mix()для смешивания с белым или черным (предпочтительно вoklab). Это сохраняет цвет в гамме, но иногда приводит к чрезмерному обесцвечиванию цветов и появлению размытых оттенков и теней. - Вы можете комбинировать регулировку яркости с любыми другими методами (например,
color-mix(in oklab, oklch(from var(--primary) 0.9 c h), white 30%)) для баланса между двумя цветами, но с 30%-м ограничением регулировки яркости.
Большая часть генерируемого браузером UI может быть кастомизирована в той или иной степени с помощью CSS. Даже если это требует современных возможностей, в старых браузерах происходит изящная деградация, и часто не нужен ни полифилл, ни резерв.
Перед воссозданием браузерного UI (контролы формы, скроллбары, селекторы, сообщения об ошибках и т.д), сначала проверьте, что:
- Браузерный UI не может быть кастомизирован в достаточной степени для ваших нужд, даже с помощью современного CSS.
- Желаемая кастомизация достаточно важна, чтобы оправдать компромиссы при воссоздании встроенного UI - потеря семантики доступности, обработки клавиатуры, интеграции с IME (Input Method Editor - редактор метода ввода) и AT (Assistive Technology - вспомогательные технологии), которые нативный UI предоставляет бесплатно.
Примеры допустимых кастомизаций:
- используйте
::selectionдля кастомизации цветов выделенного текста; - не применяйте
user-select: noneдля текстового контента - это ломает копирование-вставку, инструменты перевода и жесты AT "читать отсюда"; Применяйте его только при крайней необходимости (маркеры перетаскивания, панели инструментов, лишние подписи к кнопкам); - используйте
accent-colorдля применения акцентного цвета страницы к любому генерируемому браузером UI; - используйте
color-scheme, чтобы UI браузера адаптировался к светлому/темному режиму; - используйте
scrollbar-colorдля кастомизации цветов полосы прокрутки иscrollbar-widthдля управления шириной скроллбара - визуально отделяйте бегунок от дорожки (≥3:1) и не устанавливайтеscrollbar-width: noneпрокручиваемым областям (можно устанавливать только в случае полной замены прокрутки другим решением); - используйте
:user-invalid/:user-validдля стилей валидации - они применяются только после взаимодействия пользователя с полем (в отличие от:invalid/:valid), что, например, позволяет избежать отображения ошибок незаполненности обязательных полей при загрузке страницы; - кнопки и текстовые поля (включая
<textarea>), как правило, могут стилизоваться как обычные элементы. - используйте
font-sizeдля масштабирования и другие свойства текста для управления типографикой.
Для большинства целей стилизации (например, цвета, границы, фоны, типографика и т.д.) элементы могут рассматриваться, как обычные текстовые контейнеры.
- Используйте
:placeholder-shownи::placeholderдля стилизации плейсхолдеров инпутов. - Используйте
field-sizing: contentдля растягивания текстовых полей по ширине контента. - Для элементов
<textarea>, используйтеresize: verticalдля отключения горизонтального масштабирования илиresize: noneдля отключения масштабирования по обеим осям.
- Для выбора одного из нескольких вариантов в выпадающем списке: используйте
<select>+appearance: base-select+::picker(select). - Для выбора одного или нескольких вариантов из списка, отображаемого на странице: используйте
<input type=checkbox>или<input type=radio>внутри<label>для каждого варианта. Стилизуйте его с помощьюlabel:has(:checked). - Стилизуйте чекбоксы, радио-кнопки и переключатели с помощью
appearance: none+ генерируемого контента (::before/::after) или фоновых изображений для стилизации выбранного состояния.
- Файловые инпуты: используйте
::file-selector-buttonдля стилизации кнопки. - Не используйте
<input>с атрибутомtypeсо значениемbutton,submitилиreset. Вместо этого, используйте<button>и стилизуйте его как обычный элемент. - Слайдеры: используйте
appearance: none+ псевдоэлементы бегунка (::-webkit-slider-thumb,::-moz-range-thumbи т.д.) и псевдоэлементы дорожки (::-webkit-slider-runnable-track,::-moz-range-trackи т.д.) для более точечного управления.
- Используйте запросы
@containerдля создания отзывчивых макетов на основе компонентов, которые адаптируются под размеры их родительского контейнера, а не под размеры области просмотра. - Используйте динамические единицы области просмотра (
dvh,dvw) вместоvh/vwдля предотвращения поломки макета при появлении/исчезновении элементов UI мобильных браузеров (таких как строка поиска). - Используйте
aspect-ratioдля медиа-элементов (таких как<img>и<video>) для резервирования пространства во время загрузки страницы для предотвращения совокупного сдвига макета (CLS).
- Комбинируйте основанные на области просмотра и основанные на шрифте единицы измерения в
clamp()для размера шрифта, которые увеличиваются по мере увеличения области просмотра в пределах установленного диапазона. Например,clamp(2rem, 1rem + 5vw, 4rem). Меняйте пропорции этих единиц измерения для управления скоростью изменения размера шрифта. - Не используйте только
vwдля размера шрифта безclamp(), поскольку это сделает шрифт слишком мелким или слишком большим на "экстремальных" экранах.
- Используйте числа без единиц измерения для
line-height(например,1.5) для обеспечения относительного масштабирования в процессе наследования размера шрифта. - Используйте
overflow-wrap: break-word(илиanywhere) для длинных URL. - Не используйте
pxдляfont-size. Используйтеremдля учета настроек браузера пользователя (корневой размер шрифта) илиemдля контекстного определения размера.
- Используйте
text-wrap: balanceдля балансировки заголовков или похожего контента (например,<th>). - Используйте
text-wrap: prettyдля длинного основного текста (параграфы, цитаты и т.д.). - Используйте
text-wrap: balanceилиtext-wrap: prettyосознанно, не применяйте их к*, поскольку это снизит производительность. - Не применяйте
text-wrap: balanceк элементам с видимым блоком (фон, границы, тени и т.д.), поскольку это свойство не меняет ширину контейнера, оно лишь определяет, как переносится текст в пределах этой ширины. Это может привести к образования пустого пространства в конце контейнера, что обычно нежелательно.
- Наслаивайте несколько теней для создания реалистичных эффектов мягкой глубины.
- Используйте
filter: drop-shadow(), вместоbox-shadow, для непрямоугольных фигур или прозрачных PNG. - Используйте
mix-blend-modeиbackground-blend-modeдля подсветки оверлеев (ограничивайте их область применения с помощьюisolation: isolate)
.hero {
background-image: url('texture.png'), linear-gradient(to bottom, #fff, #eee);
background-blend-mode: soft-light;
}- Используйте
corner-shape: squircleдля более эстетичных изгибов в качестве прогрессивного улучшения вместо обычных скругленных углов. - Используйте эллиптические
border-radius(например,10px / 20px) для пропорциональных изгибов без дополнительных элементов.
Используйте in oklch или in oklab для явного определения интерполяции цветового пространства для градиентов или color-mix().
in oklchлучше сохраняет цветность, но легче выходит за пределы гаммы устройства, особенно, в случае большой разницы между цветами.in oklabлучше вписывается в гамму (при условии, что конечные точки находятся внутри гаммы), но может создавать размытые бледные цвета в середине, особенно, при интерполяции между противоположными оттенками.- Не используйте
in srgb, пока у вас не будет для этого веской причины (например, вы разрабатываете инпут выбора цвета с функционалом интерполяции в srgb).
Некоторые версии браузеров, вышедшие до 2024 года, не поддерживают пространство интерполяции цвета градиента. Для поддержки этих браузеров используйте токен, только когда его использование безопасно, путем определения переменной:
:root {
--in-oklab: ;
--in-oklch: ;
}
@supports (linear-gradient(in oklab, white, black)) {
:root {
--in-oklab: in oklab;
--in-oklch: in oklch;
}
}Пример использования:
.card {
background: linear-gradient(to bottom var(--in-oklab), var(--accent-color), var(--darker));
}- Это не требуется для
color-mix(). Если браузер поддерживаетcolor-mix(), он также поддерживает его аргументin <color-space>.
С помощью градиентов CSS + жестких контрольных точек можно создать много интересных паттернов, которые могут быть более гибкими и производительными, чем SVG или внешние изображения, поскольку они имеют доступ к переменным CSS и длинам из окружающего контекста. Не нужно дублировать положение - просто используйте 0 или 0% и исправление градиента автоматически отрегулирует его.
Примеры.
Вертикальные полосы шириной 1em:
background: linear-gradient(to right, var(--color-1) 50%, var(--color-2) 0) 0 / 2em;Диагональные полосы шириной 1em:
background: repeating-linear-gradient(-45deg, var(--color-1) 0 1em, var(--color-2) 0 2em);Паттерн шахмат с квадратами размером 1em:
background: repeating-conic-gradient(var(--color-1) 0 25%, var(--color-2) 0 50%) 0 / 2em 2em;Паттерн горошка с радиусом точек .5em, расположенными на расстоянии 2em друг от друга (по горизонтали/вертикали - умножьте на sqrt(2) для расстояния по диагонали):
--distance: 2em;
--radius: .5em;
--polka: radial-gradient(circle, var(--color-1) var(--radius), transparent calc(var(--radius) + 1px));
background: var(--polka) 0 0, var(--polka) var(--distance) var(--distance) var(--color-2);
background-size: calc(var(--distance) * 2) calc(var(--distance) * 2);Простая круговая диаграмма:
.pie {
--p: 80%;
width: 60px;
aspect-ratio: 1;
border-radius: 50%;
background: conic-gradient(var(--color-1) var(--p), transparent 0%) var(--color-2);
}Важно: при использовании градиентов для рендеринга диаграмм, убедитесь в наличии текстового резерва для устройств чтения с экрана.
- Используйте
clip-pathиmask-imageдля кастомных геометрических проявлений и плавных затуханий. - Используйте анимацию, управляемую прокруткой (Scroll-Driven Animations) (
animation-timeline: scroll()) для несущественных эффектов прокрутки, вместо обработчиков JS. - Используйте переходы отображения (View Transitions) для плавной анимации между сложными состояниями макета.
Производительность рендеринга является критичной для положительного пользовательского опыта, особенно, для тяжелых деревьев DOM.
- Анимируйте
opacityиtransform(включая индивидуальные свойства трансформации, например,translate, вместоleft/right/top/bottom), поскольку такие анимации выполняются в потоке компоновщика, а не в основном потоке. - Используйте
transition-behavior: allow-discrete+@starting-styleдля нативного анимирования таких свойств макета, какdisplay, или состояния<dialog>. - Всегда используйте
content-visibilityсовместно сcontain-intrinsic-sizeдля предотвращения прыжков скроллбара (CLS). - При установке
contain-intrinsic-size, используйте ключевое словоautoи значение, которое вычисляется на основе знаний о контенте (например, размер текста, доступное пространство, размер графики, количество символов). Используйте такие единицы измерения, какrem,lh,capилиch, которые соответствуют значениям, используемым для элементов внутри содержимого, а неpx. Если элементы группы имеют разные размеры, используйте средний размер. - Используйте
contain: layout style paintдля изоляции рендеринга обновлений компонента.
.large-section {
content-visibility: auto;
contain-intrinsic-block-size: auto 800px;
}
.row {
--row-gap: .4rem;
--title-height: 1lh;
--description-height: 0.85lh;
display: grid;
row-gap: var(--row-gap);
content-visibility: auto;
/* Сумма высоты заголовка, отступа между строками и высоты описания должна быть размером контента при пропуске рендеринга. */
contain-intrinsic-block-size: auto calc(var(--title-height) + var(--row-gap) + var(--description-height));
}
.popover-reveal {
/* Разрешаем дискретную анимацию для переходов display */
transition: display 0.2s allow-discrete;
}- Не используйте
contentдля передачи осмысленного текста (подписи, состояние, инструкции) - он должен содержаться в DOM (WCAG F87). - Используйте
contentдля предоставления альтернативного текста для устройств чтения с экрана, например,content: url(cloud.svg) / "Save"; - Используйте
content: "text" / "";для предотвращения озвучки чисто декоративного текста. - Не используйте пустой альтернативный текст для изображений - они являются презентабельными по умолчанию. Например, это неправильно:
content: url(cloud.svg) / "";. - Не используйте альтернативный текст для эмодзи, если описание не отличается от официального названия эмодзи. Например, не делайте
content: "🎉" / "celebration";, ноcontent: "🎉" / "Yay!";допустимо.
Используйте альтернативный текст только в случае, когда он отличается от основного значения и отсутствует в DOM. Например, это неправильно:
HTML:
<button class="save">Save</button>CSS:
button.save::before {
content: url(cloud.svg) / "Save";
}Устройство чтения с экрана прочитает это как "Save save".