Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Стили и дизайн-системы
styling_design_systems

Стили и дизайн-системы

Каскад, CSS Modules, токены дизайна, варианты, компоненты без оформления, темы, адаптивность и визуальные контракты.

Открыть лабораториюv0.2.0Запускается локально из публичного репозитория

Стили и дизайн-системы

Дизайн-система — это набор ограничений и контрактов, а не папка случайных компонентов одного цвета.

#Результат урока

Вы построите компоненты с темами на основе токенов, разделите структуру и оформление и зафиксируете варианты и адаптивное поведение.

Понадобятся проектирование API компонентов, доступность и современный CSS. Итог урока — базовые Button, Card и Dialog с токенами, темой, адаптацией к контейнеру и визуальными тестами.

#1. Каскад и локальная область

Каскад учитывает источник, слой, специфичность и порядок. CSS Modules дают локальные имена классов, но глобальные токены и каскад продолжают работать. Не решайте конфликт постоянным повышением специфичности и !important; заранее задайте слои (cascade layers) и владельцев правил.

Слои задают порядок один раз и снимают гонку специфичности: правило из позднего слоя выигрывает независимо от числа селекторов.

/* src/styles/index.css */ @layer reset, base, components, utilities; @layer components { .card { padding: var(--space-4); background: var(--color-surface); } } @layer utilities { /* победит без !important: слой utilities объявлен позже */ .p-0 { padding: 0; } }

Без слоёв та же задача решается либо !important, либо селектором вида .page .card div span — и оба варианта делают компонент зависимым от страницы, на которой он оказался.

Проверьте себя. Разделите слои сброса, основы, компонентов и утилит.

Частая ошибка. Селектор .page .card div span как API компонента.

#2. Семантические токены дизайна

Базовые цвета и интервалы преобразуются в семантические токены (design tokens) поверхности, текста, акцента, опасности и фокуса. Компоненты используют семантический слой, а тема меняет соответствие значениям. Имя токена описывает роль, а не конкретный цвет или экземпляр компонента.

Двухслойная схема выглядит так: сырые значения отдельно, роли отдельно.

/* src/styles/tokens.css */ :root { /* слой значений: сюда не смотрят компоненты */ --blue-600: #2563eb; --slate-50: #f8fafc; /* слой ролей: только это используется в компонентах */ --color-primary: var(--blue-600); --color-surface: var(--slate-50); --color-ring: var(--blue-600); }

Компонент ссылается на роль, поэтому смена палитры или темы не требует правок в компонентах:

.button--primary { background: var(--color-primary); color: var(--color-background); }

Имя --color-button-blue вместо --color-primary разрушает смысл слоя: при смене фирменного цвета на зелёный имя станет ложью, а найти все места использования будет нечем.

Проверьте себя. Перенесите цвета Button в семантические пользовательские свойства.

Частая ошибка. Шестнадцатеричные цвета и случайные интервалы в каждом компоненте.

#3. Варианты как конечный контракт

Свойства вариантов — объединения строковых литералов, согласованные с правилами CSS. Составные варианты описывают реальные сочетания; не создавайте имена классов из произвольной пользовательской строки. Размер, тон и выразительность независимы лишь тогда, когда поддержаны все комбинации.

Конечное перечисление вариантов проверяется компилятором и не даёт собрать несуществующий класс:

// src/shared/ui/button.tsx type Variant = 'primary' | 'secondary' | 'ghost'; type Size = 'sm' | 'md' | 'lg'; const styles: Record<Variant, string> = { primary: 'bg-primary-600 text-background', secondary: 'bg-surface border border-border', ghost: 'bg-transparent hover:bg-surface', }; export function Button({ variant = 'primary', size = 'md', ...rest }: ButtonProps) { return <button className={cn(styles[variant], sizes[size])} {...rest} />; }

Сборка имени из строки выглядит короче, но ломает и типизацию, и очистку неиспользованных стилей:

// ❌ класса button-${userValue} может не существовать, а сборщик его не найдёт <button className={`button-${userValue}`} />

Матрица вариантов и состояний нужна именно для того, чтобы обнаружить неподдерживаемые сочетания: ghost + disabled + тёмная тема часто оказывается нечитаемым.

Проверьте себя. Составьте матрицу вариантов и состояний.

Частая ошибка. className={"button-" + userValue}.

#4. Темы через переменные, а не ветви JSX

Провайдер темы задаёт атрибут или класс и семантические пользовательские свойства. Компоненты остаются теми же; SSR должен выдать начальную тему или ранний скрипт без вспышки и расхождения гидратации. Системная тема служит значением по умолчанию, выбор пользователя — сохраняемым переопределением.

Тема меняет значения ролей, а не структуру дерева:

:root { --color-surface: #ffffff; --color-foreground: #0f172a; } :root[data-theme='dark'] { --color-surface: #0f172a; --color-foreground: #e2e8f0; } @media (prefers-color-scheme: dark) { :root:not([data-theme='light']) { --color-surface: #0f172a; --color-foreground: #e2e8f0; } }

Переключение — это запись атрибута, поэтому поддерево не перемонтируется и состояние компонентов не теряется:

// src/shared/theme/ThemeProvider.tsx useEffect(() => { document.documentElement.dataset.theme = theme; }, [theme]);

Вариант isDark ? <DarkButton /> : <LightButton /> даёт другую идентичность элементов: при переключении темы React размонтирует поддерево, и открытая форма, положение прокрутки и фокус будут потеряны.

Проверьте себя. Переключите светлую и тёмную темы без повторного монтирования поддерева.

Частая ошибка. isDark ? <DarkButton> : <LightButton> для каждого компонента.

#5. Responsive и container queries

Медиазапросы области просмотра отвечают за глобальную среду, контейнерные запросы (container queries) — за доступное компоненту место. Переиспользуемая карточка не должна знать страницу; объявите контейнер и адаптируйте внутреннюю разметку. Используйте логические свойства и точки перелома, выбранные по содержимому.

Контейнерный запрос описывает правило «если мне дали мало места», и компонент становится переносимым:

.card-slot { container-type: inline-size; } .card { display: grid; grid-template-columns: 1fr; } @container (min-width: 480px) { .card { grid-template-columns: 96px 1fr; } /* картинка слева, когда место есть */ }

Одна и та же карточка теперь корректно выглядит и в узкой боковой панели, и в широкой сетке — при одинаковой ширине окна. Медиазапрос этого не умеет: он знает только про область просмотра. Чтение window.innerWidth в JavaScript добавляет к этому ещё и расхождение при гидратации, потому что на сервере такого значения нет.

Проверьте себя. Покажите Card в боковой панели и основной сетке при одной ширине окна.

Частая ошибка. Читать window.innerWidth в JavaScript для каждого решения о расположении.

#6. Motion и reduced motion

Движение объясняет изменение и сохраняет непрерывность, но не задерживает основное действие. Переходы CSS подходят простым состояниям; сложное появление и уход требуют отдельного инструмента и очистки. prefers-reduced-motion убирает несущественное движение, сохраняя состояние понятным.

Уменьшенное движение не означает «без обратной связи»: изменение состояния остаётся, исчезает только перемещение.

.dialog { animation: dialog-in 180ms ease-out; } @keyframes dialog-in { from { opacity: 0; transform: translateY(8px) scale(0.98); } to { opacity: 1; transform: none; } } @media (prefers-reduced-motion: reduce) { .dialog { animation: fade-in 120ms linear; } /* появление есть, движения нет */ }

Анимация длиной 800 мс перед доступностью результата — это не оформление, а задержка работы: пользователь уже знает, что нажал кнопку, и ждёт данные. Ориентир для интерфейсных переходов — 120–200 мс.

Проверьте себя. Добавьте вариант перехода диалога с уменьшенным движением.

Частая ошибка. Анимация 800ms перед доступностью результата.

#7. Базовое поведение и оформление

Слой без оформления (headless) реализует модель состояния, aria-* и клавиатуру; оформленный слой задаёт токены и расположение. Разделение полезно при нескольких визуальных системах, но обязательные свойства и ref нельзя потерять. Иногда один цельный компонент проще и безопаснее.

Оформленный компонент оборачивает поведение и пробрасывает ref — иначе внешний код потеряет доступ к узлу для фокуса и измерений:

// src/shared/ui/select.tsx export const Select = forwardRef<HTMLButtonElement, SelectProps>(function Select( { label, ...rest }, ref, ) { return ( <SelectPrimitive.Root {...rest}> {/* поведение и aria-* берём у примитива, задаём только оформление */} <SelectPrimitive.Trigger ref={ref} className="border border-border rounded-xl px-3 py-2"> <SelectPrimitive.Value placeholder={label} /> </SelectPrimitive.Trigger> </SelectPrimitive.Root> ); });

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

Проверьте себя. Соберите оформленный Select поверх контракта поведения.

Частая ошибка. Отдать вызывающей стороне низкоуровневые детали aria-* без безопасных значений по умолчанию.

#8. Визуальная регрессия как дополнение

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

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

// e2e/button.visual.spec.ts for (const theme of ['light', 'dark'] as const) { test(`варианты Button, тема ${theme}`, async ({ page }) => { await page.goto(`/__stories/button?theme=${theme}`); await page.clock.setFixedTime(new Date('2026-01-01T00:00:00Z')); // убираем время await expect(page.getByTestId('button-matrix')).toHaveScreenshot(`button-${theme}.png`); }); }

Фиксация времени, отключение анимаций и стабильные шрифты обязательны: иначе тест начнёт падать от миллисекунд и сглаживания, и его отключат. Снимок главной страницы, наоборот, падает на любом изменении контента и при этом не покрывает состояния disabled, focus-visible и loading.

Проверьте себя. Зафиксируйте матрицу вариантов в двух темах.

Частая ошибка. Один снимок главной страницы вместо состояний компонентов.

#Лаборатория

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

Работа готова, когда темы меняются без ветвления JSX, компоненты не зависят от селекторов страницы, а контрастность, фокус и визуальные состояния проверены.

#Источники для сверки

  • MDN CSS cascade
  • W3C Design Tokens draft
  • MDN container queries

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

Далее: Производительность и React Compiler