Skip to content

Дизайн-токены, светлая и темная темы

Задаем смысловые роли цветов и проверяем обе темы с начала работы.

On this page

Для Next.js 16.3.8 (App Router) и React 19.3.0, Tailwind CSS 4.3.3, CLI shadcn 4.21.1. Обновлено .

Представьте, что фон и текст каждой карточки записаны отдельными цветами. Для второй темы придется найти все такие места и подобрать новые пары. Дизайн-токен – именованное значение, которое используется в оформлении. Например, кнопка берет цвет из --primary, а для каждой темы в этой переменной задан свой цвет.

Значение, роль и компонент

УровеньПримерЗачем нужен
Исходное значение#2563ebХранит конкретный цвет
Смысловая роль--primaryОбозначает основной акцент
Роль компонента--button-backgroundНужна, если кнопке требуется отдельная настройка

Токены бывают и для отступов, шрифтов, радиусов, теней. Начнем с цветов: здесь проще увидеть ошибку сразу.

--blue говорит о цвете, --primary – о его назначении. Отдельный токен кнопки нужен, если её оформление должно меняться независимо от остальных элементов. Для общих цветов достаточно смысловых токенов вроде --primary.

Меняем значения токенов

Токен --primary:

Афиша: 19:00, большой зал

Кинопоказ

Текущие токены и цветовые роли
:root {  --background: #ffffff;  --foreground: #171717;  --muted-foreground: #525252;  --border: #e5e5e5;  --primary: #2563eb;  --primary-foreground: #ffffff;}
.event-card {  background: var(--background);  color: var(--foreground);  border: 1px solid var(--border);}
.event-card p { color: var(--muted-foreground); }.event-card button {  background: var(--primary);  color: var(--primary-foreground);}

Компонент обращается к CSS-переменным по смысловым именам. Тема меняет набор значений, а кнопка палитры меняет значение --primary.

Переключайте тему и акцент. Превью меняется за счет значений переменных; разметка карточки остается той же. Тема внутри демонстрации не меняет тему всего сайта.

Пары цветов

Цвет фона и цвет текста проверяются вместе:

  • --background и --foreground – фон страницы и основной текст.
  • --primary и --primary-foreground – акцентный фон и текст на нем.
  • --muted и --muted-foreground – приглушенная поверхность и текст на ней.
  • --border – границы элементов.

--muted не является цветом второстепенного текста. Для этого есть --muted-foreground. Если перепутать роли, подпись может исчезнуть на фоне – даже если названия переменных выглядят правильными.

Минимальные токены без библиотеки

Добавьте ниже @import "tailwindcss"; в app/globals.css. Если уже установили shadcn/ui, используйте его существующие определения и не создавайте второй набор.

app/globals.css
@custom-variant dark (&:where(.dark, .dark *));
 
:root {
  --background: #ffffff;
  --foreground: #171717;
  --primary: #2563eb;
  --primary-foreground: #ffffff;
  --muted: #f5f5f5;
  --muted-foreground: #525252;
  --border: #d4d4d4;
}
 
.dark {
  --background: #171717;
  --foreground: #fafafa;
  --primary: #93c5fd;
  --primary-foreground: #172554;
  --muted: #262626;
  --muted-foreground: #d4d4d4;
  --border: #525252;
}
 
@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);
  --color-muted: var(--muted);
  --color-muted-foreground: var(--muted-foreground);
  --color-border: var(--border);
}
 
body {
  background: var(--background);
  color: var(--foreground);
}

:root задает значения по умолчанию. Класс dark на html заменяет их. @theme inline связывает переменные с именами классов Tailwind: теперь доступны bg-background, text-foreground, border-border.

В учебном проекте удалите прежние конфликтующие определения этих же переменных и цвета body из сгенерированного стартового CSS. Сам импорт Tailwind сохраните. Не оставляйте рядом отдельное правило prefers-color-scheme, которое независимо управляет той же темой.

Обновите классы карточки:

app/event-card.tsx
<article className="border-border bg-background text-foreground flex flex-col gap-3 rounded-2xl border p-6">
  <h2 className="text-xl font-semibold">{name}</h2>
  <p className="text-muted-foreground">
    {price === 0 ? "Бесплатно" : `${price} ₽`}
  </p>
</article>

Переключатель с сохранением выбора

next-themes хранит выбор темы и применяет класс к html. Установите:

Терминал
bun add next-themes
app/theme-provider.tsx
"use client";
 
import { ThemeProvider as NextThemesProvider } from "next-themes";
import type { ReactNode } from "react";
 
export function ThemeProvider({ children }: { children: ReactNode }) {
  return (
    <NextThemesProvider attribute="class" defaultTheme="system" enableSystem>
      {children}
    </NextThemesProvider>
  );
}

В корневом layout импортируйте ThemeProvider и оберните всё существующее содержимое body, включая навигацию:

app/layout.tsx
<html lang="ru" suppressHydrationWarning>
  <body>
    <ThemeProvider>
      {/* Существующая навигация и {children} */}
      {children}
    </ThemeProvider>
  </body>
</html>

Это фрагмент для изменения layout, а не его полная замена. suppressHydrationWarning нужен на html, потому что провайдер меняет его атрибуты до запуска React в браузере. Не ставьте этот параметр повсюду, чтобы скрыть настоящие ошибки.

Сначала сделаем три кнопки выбора темы. Им не нужно знать текущую тему, поэтому их можно показать сразу:

app/theme-switch.tsx
"use client";
 
import { useTheme } from "next-themes";
 
export function ThemeSwitch() {
  const { setTheme } = useTheme();
 
  return (
    <div aria-label="Выбор темы">
      <button type="button" onClick={() => setTheme("light")}>
        Светлая
      </button>
      <button type="button" onClick={() => setTheme("dark")}>
        Темная
      </button>
      <button type="button" onClick={() => setTheme("system")}>
        Как в системе
      </button>
    </div>
  );
}

Импортируйте ThemeSwitch в layout и добавьте <ThemeSwitch /> внутри ThemeProvider. Если захотите показывать текущий выбор, учтите, что сохраненная тема известна только после запуска компонента в браузере; готовый пример есть в документации next-themes.

Проверяйте обе темы с начала работы

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

Токены не гарантируют хороший контраст. Проверяйте пары в DevTools или инструменте измерения: для обычного текста ориентир WCAG AA – 4.5:1, для крупного – 3:1. Текст на фотографии проверяется отдельно.

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

Документация: темы Tailwind, dark mode, next-themes, контраст WCAG.