Перейти к основному содержимому

Требования к стилизации

В этом разделе описаны технические требования к конкретным инструментам и общие правила написания стилей.

Tailwind CSS

MUST

  1. Конфигурация использует дизайн-токены:
module.exports = {
theme: {
extend: {
colors: {
'text-primary': 'var(--color-text-secondary)',
},
spacing: {
s: 'var(--space-s)',
},
},
},
};
  1. Автоматическая сортировка классов: prettier-plugin-tailwindcss

  2. Сканирование CSS Modules (если используются оба подхода):

module.exports = {
content: [
'./src/**/*.{ts,tsx}',
'./src/**/*.module.css', // Предотвращает удаление классов Tailwind в CSS Modules
],
};

SHOULD

Компонентный класс при повторении 3+ раза:

@layer components {
.card {
@apply bg-bg-surface rounded-m p-m shadow-1;
}
}

MAY

@apply допускается для:

  • Сторонних компонентов без доступа к разметке
  • Комбинаций из 4+ модификаторов

FORBIDDEN

  • Кастомные утилиты в глобальных стилях (только в tailwind.config.js)

⚠️ ВАЖНО

@apply увеличивает bundle size — каждый @apply дублирует утилиты в итоговом CSS.


CSS/SCSS Modules

MUST

  1. Собственный .module.(s)css файл для каждого компонента
  2. camelCase именование классов: s.cardHeader
  3. CSS variables используют дизайн-токены:
.card {
background: var(--color-bg-surface);
padding: var(--space-m);
}

MAY

Нативный CSS Nesting (Chrome 112+, Safari 16.5+, Firefox 117+):

.card {
background: var(--color-bg-surface);

& .header {
padding: var(--space-m);
}

&:hover {
transform: scale(1.02);
}
}

SHOULD

CSS variables предпочтительнее SCSS-переменных (runtime, темизация, JavaScript доступ)

FORBIDDEN

  1. Избыточное использование @extend в SCSS
  2. :global() без привязки к родителю
  3. Вложенные :global() внутри :global()

⚠️ ВАЖНО

CSS Modules не имеют автоматического tree-shaking — используйте Knip для обнаружения неиспользуемых стилей.


MUI System

sx prop (MUI v5+)

MUST

Конфигурация темы использует дизайн-токены:

const theme = createTheme({
palette: {
text: {
primary: 'var(--mui-palette-text-primary)',
},
},
spacing: (factor) => `${0.25 * factor}rem`, // 1 unit = 4px
});

SHOULD

Повторяющиеся sx-стили выносятся при повторении 3+ раза:

// shared/styles/card.ts
export const cardSx = {
p: 2,
borderRadius: 1,
bgcolor: 'background.paper',
};

MAY

Мемоизация sx-объекта:

// Статичный — вне компонента
const cardSx = { p: 2, borderRadius: 1 };

// Динамический — useMemo
const cardSx = useMemo(
() => ({ bgcolor: isActive ? 'primary.main' : 'background.paper' }),
[isActive]
);

FORBIDDEN

sx для высокочастотных динамических значений (>1 раз в несколько секунд):

/* ❌ Неправильно */
<Box sx={{ top: scrollY }}>

/* ✅ Правильно */
<Box style={{ top: scrollY }}>

CSS Cascade Layers

SHOULD

Используйте @layer для управления приоритетами:

@layer reset, base, components, utilities;

@layer components {
.card {
background: var(--color-bg-surface);
}
}

@layer utilities {
.text-center {
text-align: center;
}
}

Преимущества:

  • Предсказуемое переопределение без !important
  • Совместимость с Tailwind v4
  • Поддержка >95% браузеров

Поддержка: Chrome 99+, Safari 15.4+, Firefox 97+


Написание стилей

Селекторы

FORBIDDEN

  1. Составные селекторы через SCSS &:
/* ❌ Неправильно */
.card {
&__header {
&__title {
}
}
}

/* ✅ Правильно */
.cardHeader {
}
.cardHeaderTitle {
}
  1. Селекторы по тегам: div, ul, span
  2. Селекторы по утилитарным классам: .text-sm, .color-light

MAY

Современные селекторы с проверкой поддержки:

/* :has() — Chrome 105+, Safari 15.4+, Firefox 121+ (>90%) */
.card:has(.badge) {
padding-top: var(--space-xl);
}

.form:has(input:invalid) {
border-color: var(--color-error);
}
/* @scope — Chrome 118+, Safari 17.4+ (<90%, требует fallback) */
@supports (selector(:scope)) {
@scope (.card) {
.header {
padding: var(--space-m);
}
}
}

/* Fallback для старых браузеров */
@supports not (selector(:scope)) {
.card .header {
padding: var(--space-m);
}
}

Вложенность

Максимум 3 уровня вложенности

MAY

Допустимая вложенность:

  • Псевдоклассы (:hover, :focus)
  • Псевдоэлементы (::before, ::after)
  • Медиа-запросы (@media, @container)
  • Модификаторы состояния (.isActive, .isDisabled)

Единицы измерения

ЕдиницаПрименениеПричина
remШрифты, отступы, размеры, медиа-запросыAccessibility (масштабирование в настройках браузера/ОС)
pxborder, outline, box-shadowМелкие декоративные детали
emОтносительное масштабированиеМасштабируется от font-size родителя

Базовый размер: 16px (browser default)

Логические свойства (для RTL проектов)

/* ❌ Неправильно */
.element {
margin-left: var(--space-m);
padding-right: var(--space-s);
}

/* ✅ Правильно */
.element {
margin-inline-start: var(--space-m); /* left в LTR, right в RTL */
padding-inline-end: var(--space-s);
padding-block: var(--space-s); /* top + bottom */
}

Поддержка: Chrome 87+, Safari 14.1+, Firefox 66+ (>98% браузеров)

⚠️ Комбинация с Container Queries

.card {
container-type: inline-size;
container-name: card; /* Обязательно для корректной работы в RTL */
}

@container card (min-width: 400px) {
.content {
margin-inline-start: var(--space-m); /* Работает корректно в RTL */
}
}

Глобальные стили

MAY

  1. CSS Reset (только один):
@layer reset {
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
}
  1. Базовая типографика через дизайн-токены:
@layer base {
body {
font-family: var(--font-family-base);
color: var(--color-text-primary);
}
}
  1. CSS variables (дизайн-токены)

  2. Стили фокуса: :focus-visible

  3. Оптимизация загрузки шрифтов:

@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-display: swap; /* MUST для Core Web Vitals */
font-weight: 100 900; /* Variable fonts поддержка */
}

FORBIDDEN

  1. Стили конкретных компонентов в глобальных стилях
  2. Переопределение UI-библиотеки через глобальные селекторы
  3. !important (см. раздел "Использование !important" ниже)

Использование !important

!important не рекомендуется и допускается только в исключительных случаях:

/* ❌ Избегайте */
.button {
color: red !important;
}

/* ✅ Если необходимо — с обязательным комментарием */
.button {
/* !important: Переопределение стилей сторонней библиотеки,
которая использует inline styles с высокой специфичностью */
color: red !important;
}

Альтернативы:

  1. Использование @layer для управления приоритетами
  2. Увеличение специфичности селектора
  3. Рефакторинг архитектуры стилей

Адаптивность

MUST

  1. Mobile-first подход (базовые стили = мобильные)
  2. min-width для больших экранов:
.card {
padding: var(--space-s); /* мобильные */
}

@media (min-width: 768px) {
.card {
padding: var(--space-m); /* планшеты+ */
}
}

Контейнерные запросы

Применяются на уровне переиспользуемых компонентов:

.card {
container-type: inline-size;
}

@container (min-width: 400px) {
.cardContent {
flex-direction: row;
}
}

Поддержка: Chrome 105+, Safari 16+, Firefox 110+ (>92% браузеров)

Брейкпоинты

Значения из дизайн-токенов: sm(640px), md(768px), lg(1024px), xl(1280px), 2xl(1536px)


Темизация

MUST

  1. Смена темы без перезагрузки страницы
  2. Семантические токены вместо конкретных значений:
   /* ❌ Неправильно */
<Box sx={{ color: 'grey.800' }}>

/* ✅ Правильно */
<Box sx={{ color: 'text.primary' }}>

Пример

:root {
--color-text-primary: #1b1b1b;
--color-bg-surface: #ffffff;
}

[data-theme='dark'] {
--color-text-primary: #f5f5f5;
--color-bg-surface: #121212;
}

Анимации

MUST

  1. GPU-friendly свойства: transform, opacity
  2. prefers-reduced-motion обязателен:
.animated {
animation: slide 0.3s ease;
}

@media (prefers-reduced-motion: reduce) {
.animated {
animation: none;
}
}

MAY

will-change точечно:

.modal {
will-change: transform, opacity;
animation: slideIn 0.3s ease;
}

.modal.isVisible {
will-change: auto; /* убираем после анимации */
}

Progressive Enhancement: View Transitions

View Transitions API — Chrome 111+, Safari 18+, Firefox — (покрытие ~70%)

/* С проверкой поддержки */
@supports (view-transition-name: none) {
.page {
view-transition-name: page-transition;
}

::view-transition-old(page-transition),
::view-transition-new(page-transition) {
animation-duration: 0.3s;
}
}

/* Fallback для браузеров без поддержки */
@supports not (view-transition-name: none) {
.page {
transition: opacity 0.3s ease;
}
}

/* Обязательно: respecting reduced motion */
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}