Как сделать темную тему на сайте
Недавно передо мной встала задача — добавить темную тему на несколько проектов сразу: обычный лендинг, сайт на WordPress и сайт на Laravel. Хочу поделиться опытом и показать, как это реализовать без фреймворков, чистым CSS с современными подходами.
Почему CSS-переменные — это база
Забудьте про дублирование стилей для светлой и темной темы. Весь секрет в CSS custom properties (переменных) и атрибуте data-theme. Это современный стандарт, который поддерживают все актуальные браузеры.
Темная тема на лендинге
Начну с самого простого случая — обычного лендинга без CMS.
Шаг 1. Определяем переменные
:root {
--bg-color: #ffffff;
--text-color: #1a1a1a;
--card-bg: #f5f5f5;
--accent-color: #3b82f6;
--border-color: #e0e0e0;
--shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
[data-theme="dark"] {
--bg-color: #121212;
--text-color: #e8e8e8;
--card-bg: #1e1e1e;
--accent-color: #60a5fa;
--border-color: #333333;
--shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}
* {
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
}
body {
background: var(--bg-color);
color: var(--text-color);
}
.card {
background: var(--card-bg);
border: 1px solid var(--border-color);
box-shadow: var(--shadow);
}
Шаг 2. Переключатель темы
HTML:
<button id="theme-toggle" aria-label="Переключить тему">🌓</button>
JS:
const toggle = document.getElementById('theme-toggle');
const currentTheme = localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', currentTheme);
toggle.addEventListener('click', () => {
const theme = document.documentElement.getAttribute('data-theme');
const newTheme = theme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
});
Обратите внимание на prefers-color-scheme — это медиа-запрос, который считывает системные настройки пользователя. Хорошая практика — уважать выбор ОС по умолчанию.
Шаг 3. Избегаем мигания при загрузке
Чтобы страница не «мигала» светлой темой перед применением темной, скрипт установки темы нужно вставить в <head> до подключения основных стилей:
<script>
(function() {
const theme = localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
})();
</script>
Темная тема в WordPress
С WordPress все немного сложнее из-за структуры темы.
Шаг 1. Подключаем стили в functions.php
function enqueue_theme_styles() {
wp_enqueue_style('main-style', get_stylesheet_uri());
wp_enqueue_script(
'theme-switcher',
get_template_directory_uri() . '/js/theme-switcher.js',
array(),
'1.0',
false
);
}
add_action('wp_enqueue_scripts', 'enqueue_theme_switcher');
Шаг 2. Добавляем переменные в style.css темы
:root {
--wp-bg: #ffffff;
--wp-text: #23282d;
--wp-header-bg: #ffffff;
--wp-link: #0073aa;
}
[data-theme="dark"] {
--wp-bg: #1a1a1a;
--wp-text: #f0f0f0;
--wp-header-bg: #0d0d0d;
--wp-link: #4db8ff;
}
body {
background-color: var(--wp-bg);
color: var(--wp-text);
}
.site-header {
background-color: var(--wp-header-bg);
}
a {
color: var(--wp-link);
}
Шаг 3. Виджет переключения через шорткод
Чтобы переключатель можно было вставить в любое место через редактор Gutenberg, так же добавляем в functions.php:
function theme_switcher_shortcode() {
return '<button id="wp-theme-toggle" class="theme-toggle-btn" aria-label="Тема">
<span class="icon-sun">☀️</span>
<span class="icon-moon">🌙</span>
</button>';
}
add_shortcode('theme_toggle', 'theme_switcher_shortcode');
Использование: [theme_toggle] на любой странице.
Шаг 4. JS для WordPress с учетом кастомайзера
Если хотите дать администратору возможность выбирать тему по умолчанию через Customizer:
function register_theme_customizer($wp_customize) {
$wp_customize->add_setting('default_theme_mode', array(
'default' => 'light',
));
$wp_customize->add_control('default_theme_mode', array(
'label' => 'Тема по умолчанию',
'section' => 'colors',
'type' => 'select',
'choices' => array('light' => 'Светлая', 'dark' => 'Темная'),
));
}
add_action('customize_register', 'register_theme_customizer');
Если используете кастомные блоки Gutenberg, переменные нужно продублировать и в редакторе через theme.json (json файл нужно создать в корне вашей темы):
{
"version": 2,
"settings": {
"color": {
"palette": [
{
"slug": "background",
"color": "var(--wp-bg)",
"name": "Фон"
}
]
}
}
}
Темная тема на Laravel
Покажу, как реализовать темную тему используя Blade-компоненты и нативный JS без Alpine или Livewire — чтобы подход оставался универсальным.
Шаг 1. Регистрируем логику в AppServiceProvider
Файл app/Providers/AppServiceProvider.php создается автоматически и уже зарегистрирован в bootstrap/providers.php, поэтому дополнительно ничего подключать не нужно:
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Blade::directive('themeAttribute', function () {
return "<?php echo 'data-theme'; ?>";
});
}
}
Шаг 2. Layout с поддержкой темы
resources/views/layouts/app.blade.php:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script>
(function() {
const theme = localStorage.getItem('theme')
|| (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
})();
</script>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
<header class="site-header">
<x-theme-toggle />
</header>
<main>
{{ $slot ?? '' }}
@yield('content')
</main>
</body>
</html>
Скрипт установки темы стоит выше @vite, чтобы страница не «мигала» светлым фоном перед применением темной темы.
Шаг 3. Blade-компонент переключателя
Создаем компонент стандартной командой artisan:
php artisan make:component ThemeToggle
Laravel создаст два файла: класс-компонент и шаблон. Для нашей задачи класс app/View/Components/ThemeToggle.php можно оставить пустым — вся логика на фронтенде:
<?php
namespace App\View\Components;
use Illuminate\View\Component;
use Illuminate\Contracts\View\View;
class ThemeToggle extends Component
{
public function render(): View
{
return view('components.theme-toggle');
}
}
Шаблон resources/views/components/theme-toggle.blade.php:
<button
id="theme-toggle"
class="theme-toggle"
type="button"
aria-label="Переключить тему"
>
<svg class="icon-sun" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<circle cx="12" cy="12" r="5"/>
<path d="M12 1v2m0 18v2M4.22 4.22l1.42 1.42m12.72 12.72l1.42 1.42M1 12h2m18 0h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
</svg>
<svg class="icon-moon" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/>
</svg>
</button>
Шаг 4. Стили
Проект по умолчанию поставляется с настроенным Vite, поэтому просто редактируем основной файл стилей resources/css/app.css:
:root {
--laravel-bg: #ffffff;
--laravel-surface: #f8fafc;
--laravel-text: #1e293b;
--laravel-border: #e2e8f0;
--laravel-accent: #f53003;
}
[data-theme="dark"] {
--laravel-bg: #0f172a;
--laravel-surface: #1e293b;
--laravel-text: #f1f5f9;
--laravel-border: #334155;
--laravel-accent: #ff6b4a;
}
body {
background: var(--laravel-bg);
color: var(--laravel-text);
transition: background 0.25s ease, color 0.25s ease;
}
.card {
background: var(--laravel-surface);
border: 1px solid var(--laravel-border);
border-radius: 8px;
padding: 1.5rem;
}
.theme-toggle {
background: transparent;
border: 1px solid var(--laravel-border);
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--laravel-text);
}
.icon-sun { display: none; }
.icon-moon { display: block; }
[data-theme="dark"] .icon-sun { display: block; }
[data-theme="dark"] .icon-moon { display: none; }
Шаг 5. JS-логика
Добавляем в resources/js/app.js:
document.addEventListener('DOMContentLoaded', () => {
const toggle = document.getElementById('theme-toggle');
toggle?.addEventListener('click', () => {
const html = document.documentElement;
const current = html.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
html.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
});
});
Если в проекте уже используется Axios для API-запросов (он подключен по умолчанию в файле bootstrap.js), можно добавить синхронизацию с сервером — покажу это в следующем шаге.
Шаг 6. Сохранение темы на сервере через маршрут
Для авторизованных пользователей удобно хранить выбор темы в БД, чтобы она подхватывалась на любом устройстве.
Добавляем миграцию:
php artisan make:migration add_theme_preference_to_users_table
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('theme_preference')->default('light');
});
}
Выполняем миграцию:
php artisan migrate
Добавляем роут в routes/web.php:
use Illuminate\Http\Request;
Route::post('/theme/toggle', function (Request $request) {
$request->validate(['theme' => 'required|in:light,dark']);
$request->user()?->update(['theme_preference' => $request->theme]);
return response()->json(['status' => 'ok']);
})->middleware(['auth']);
Обновляем JS для отправки запроса:
toggle?.addEventListener('click', async () => {
const html = document.documentElement;
const current = html.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
html.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
if (window.axios) {
await window.axios.post('/theme/toggle', { theme: next });
}
});
И подтягиваем сохраненное значение в layout для авторизованных пользователей:
<html data-theme="{{ auth()->check() ? auth()->user()->theme_preference : 'light' }}">
Такая связка работает без лишних зависимостей: Blade-компоненты дают чистую структуру, Vite собирает стили и скрипты «из коробки», а CSS-переменные остаются универсальным механизмом независимо от версии фреймворка.
Итоги и рекомендации
За годы работы я выделил несколько правил:
- Никогда не забывайте про
prefers-color-scheme— это уважение к выбору пользователя на уровне ОС. - Transition только на нужные свойства, а не на все через
*в продакшене — это может замедлить рендеринг на слабых устройствах. - localStorage — хорошо, но для авторизованных пользователей лучше синхронизировать с БД.
- Проверяйте контрастность — темная тема не значит просто «инвертировать цвета», тексты должны оставаться легко читаемыми.
- Скрипт установки темы должен быть первым в <head> — это избавит от неприятного мигания при загрузке страницы.
Такой подход с CSS-переменными универсален и легко переносится между разными технологиями — будь то простой лендинг, WordPress или Laravel-приложение. Никаких лишних зависимостей, только базовые возможности CSS и JS.