Урок 13. Домашнее задание: тёмная тема проекта

📁 Раздел: Состояния и темы ⏱️ Время изучения: ~50 мин 🎯 Сложность: Средняя

⚡ Кратко

Добавить в проект курса полноценную тёмную тему на семантических токенах плюс переключатель на три положения без мигания.

Задание

  • описать в @theme inline набор ролей: фон, текст, поверхность карточки, приглушённый текст, граница, акцент;
  • задать значения для светлой и тёмной темы;
  • перевести все ранее собранные страницы проекта на токены;
  • сделать переключатель «светлая / тёмная / системная» с сохранением выбора;
  • убрать вспышку светлой темы при загрузке.

Подготовка окружения

cd tailwind-course
npm run dev

Пошаговое решение

Шаг 1. Роли и значения

📄 src/index.css
/* src/index.css */
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));

@theme inline {
  --color-surface: var(--surface);
  --color-surface-foreground: var(--surface-foreground);
  --color-brand: var(--brand);
}

:root {
  --surface: oklch(1 0 0);
  --surface-foreground: oklch(0.21 0.01 265);
  --brand: oklch(0.55 0.21 264);
}

.dark {
  --surface: oklch(0.21 0.01 265);
  --surface-foreground: oklch(0.98 0 0);
  --brand: oklch(0.62 0.19 264);
}

Начните с шести токенов — этого достаточно. Расширять будете по мере появления новых поверхностей.

Шаг 2. Замена цветов в разметке

Пройдитесь по компонентам и замените bg-white → bg-card, text-slate-900 → text-foreground, text-slate-500 → text-muted-foreground, border-slate-200 → border-border.

Шаг 3. Переключатель

📄 src/theme.ts
// src/theme.ts — переключение темы и запоминание выбора
const KEY = 'theme'

export function applyTheme(theme: 'light' | 'dark' | 'system') {
  const isDark =
    theme === 'dark' ||
    (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)

  document.documentElement.classList.toggle('dark', isDark)
  localStorage.setItem(KEY, theme)
}

// вызывать как можно раньше, до первой отрисовки
applyTheme((localStorage.getItem(KEY) as 'light' | 'dark' | 'system') ?? 'system')

Кнопки переключателя должны показывать текущее состояние — например, подсветкой активного варианта через aria-pressed.

Шаг 4. Проверка

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

Как проверить

  • При перезагрузке страницы вспышки нет ни в одной теме.
  • В классах компонентов не осталось dark: (кроме особых случаев вроде теней).
  • Контраст основного текста в тёмной теме не ниже 4.5:1.
  • Режим «системная» реагирует на смену настройки ОС без перезагрузки.

Ориентир

Мой проект

Карточка на токенах

Выглядит правильно в обеих темах.

<div class="space-y-3 rounded-xl border border-border bg-background p-4">
  <div class="flex items-center justify-between">
    <h3 class="text-sm font-semibold text-foreground">Мой проект</h3>
    <div class="inline-flex rounded-lg border border-border p-0.5 text-xs">
      <button class="rounded-md px-2 py-1 text-muted-foreground">☀️</button>
      <button class="rounded-md bg-primary px-2 py-1 text-primary-foreground">🌙</button>
      <button class="rounded-md px-2 py-1 text-muted-foreground">💻</button>
    </div>
  </div>
  <div class="rounded-lg border border-border bg-card p-3 text-card-foreground">
    <p class="text-sm font-medium">Карточка на токенах</p>
    <p class="mt-1 text-xs text-muted-foreground">Выглядит правильно в обеих темах.</p>
  </div>
  <button class="w-full rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground hover:opacity-90">Основное действие</button>
</div>