Дизайн-токены, светлая и темная темы
Задаем смысловые роли цветов и проверяем обе темы с начала работы.
На этой странице
Для 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, используйте его существующие определения и не создавайте второй набор.
@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, которое независимо управляет той же темой.
Обновите классы карточки:
<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"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, включая навигацию:
<html lang="ru" suppressHydrationWarning>
<body>
<ThemeProvider>
{/* Существующая навигация и {children} */}
{children}
</ThemeProvider>
</body>
</html>Это фрагмент для изменения layout, а не его полная замена. suppressHydrationWarning нужен на html, потому что провайдер меняет его атрибуты до запуска React в браузере. Не ставьте этот параметр повсюду, чтобы скрыть настоящие ошибки.
Сначала сделаем три кнопки выбора темы. Им не нужно знать текущую тему, поэтому их можно показать сразу:
"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.