Темизация Markdown проекта с next-themes и Tailwind Typography
Светлая и тёмная темы. Сначала попробуем разобраться с одной из них, а потом уж добавим вторую. Я сам лично работаю в светлой теме. Но давайте начнём.
Прежде всего нам нужно установить пакет next-themes
:
npm install next-themes
Так же нам, возможно, понадобится @tailwindcss/typography
. Для быстрого прототипирования пойдёт. Для удобной работы с css свойствами можно поставить tailwind-merge
.
@tailwindcss/typography
— это плагин для оформления длинных текстов (например, статей), он добавляет стилизацию под типографику.
tailwind-merge
— это утилита, которая объединяет классы Tailwind и автоматически устраняет конфликты между ними. Например, если указаны одновременно p-2
и p-4
, она оставит только актуальный (p-4
). Также с недавних версий поддерживает условную логику классов, подобно clsx
, что делает её удобным и универсальным инструментом для работы со стилями в Tailwind.
npm i tailwind-merge @tailwindcss/typography -D
Оба модуля в ssg используются при сборке. Так что их можно занести в devDependencies.
После установки @tailwindcss/typography нам нужно сделать всего 2 вещи:
-
Проверить классы в
app/posts/[slug]/page.tsx
, а именно вdangerouslySetInnerHTML
:Они могут или должны выглядеть примерно так:
<div className="prose" dangerouslySetInnerHTML={{ __html: post.content }} />
Classname конечно же можно расписать немного больше и подготовить к тёмной теме. Например так:
className="prose-sm sm:prose lg:prose-xl dark:prose-invert"
-
В наш
app/globals.css
нужно добавить вверху файла после импорта@import "tailwindcss";
одну строчку@plugin '@tailwindcss/typography';
.
Настройка светлой и тёмной тем
Подготовим css:
@import "tailwindcss";
@plugin '@tailwindcss/typography';
@custom-variant dark (&:where(.dark, .dark *));
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
Установим theme provider:
// app/lib/theme/theme-provider.tsx
"use client";
import {
ThemeProvider as NextThemesProvider,
ThemeProviderProps,
} from "next-themes";
import { ReactNode } from "react";
interface Props extends ThemeProviderProps {
children: ReactNode;
}
export default function ThemeProvider({ children, ...props }: Props) {
return (
<NextThemesProvider
{...props}
attribute="class"
defaultTheme="system"
enableSystem
>
{children}
</NextThemesProvider>
);
}
И обернуть содержимое body
в layout.tsx
:
// (app/layout.tsx)
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<main className="container mx-auto px-4">{children}</main>
</ThemeProvider>
🧩 Пояснение параметров
Свойство | Описание |
---|---|
attribute="class" |
Использует класс (class="dark" ) для применения темы. Это стандартный способ для Tailwind (darkMode: 'class' ). |
defaultTheme="system" |
Устанавливает тему по умолчанию в соответствии с системными настройками пользователя (светлая или тёмная). |
enableSystem |
Разрешает использовать системную тему (prefers-color-scheme ). Также слушает изменения настроек системы. |
disableTransitionOnChange |
Отключает CSS-переходы при смене темы, чтобы избежать визуальных "морганий" или анимаций. |
🎯 Почему это важно
- ⚡️ Мгновенное применение темы при загрузке
- 🌍 Уважает предпочтения пользователя
- 🌘 Совместимо с Tailwind и SSG/SSR
- 🧼 Убирает визуальные артефакты при переключении
Принципиально мы сделали распознавание системных настроек и включаем пользователю на основании системных настроек предпочитаемую тему.
Переключатель темы
Самый простой компонент переключателя темы:
//app/features/theme-toggle-button.tsx
"use client";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
export default function ThemeToggleButton() {
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
// Нужно, чтобы избежать ошибок на стороне сервера
useEffect(() => setMounted(true), []);
if (!mounted) return null;
return (
<button
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
className="rounded border px-4 py-2"
>
Сменить тему ({theme})
</button>
);
}
Добавить можно прямо в layout.tsx
:
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<ThemeToggleButton />
<main className="container mx-auto px-4">{children}</main>
</ThemeProvider>
Можем кнопку сделать немного красивее:
import { SunIcon, MoonIcon } from "@heroicons/react/24/solid";
<button
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
type="button"
className="cursor-pointer rounded-md border-2 border-yellow-400 p-1.5 transition-colors duration-150 hover:bg-gray-100 active:bg-gray-200 dark:hover:bg-gray-800 dark:active:bg-gray-700"
title="Переключить тему"
aria-label="Переключить тему"
>
<SunIcon className="h-4 w-4 text-yellow-400 dark:hidden" />
<MoonIcon className="hidden h-4 w-4 text-yellow-400 dark:block" />
</button>;
Только поставьте для красивых иконок @heroicons/react
:
npm i @heroicons/react
Если вдруг возникнут сложности, то для отладки из useTheme можно вывести больше данных:
const { theme, systemTheme, resolvedTheme, setTheme } = useTheme();
// Логирование для отладки
useEffect(() => {
if (mounted) {
console.log({ systemTheme });
console.log({ resolvedTheme });
console.log({ theme });
}
}, [systemTheme, resolvedTheme, theme, mounted]);
Могу себе представить, что вы захотите кастомизировать немного некоторые стили. Вот пример стилизации обычными стилями, так и tailwind:
.markdown {
max-width: 100ch;
border: 2px solid orange;
padding: 1rem;
border-radius: 0.5rem;
}
.markdown h1 {
@apply text-red-500 dark:text-green-500;
}
Не забудьте только тогда новый класс к нашему компоненту добавить:
<div
className="markdown prose-sm sm:prose lg:prose-xl dark:prose-invert"
dangerouslySetInnerHTML={{ __html: post.content }}
/>
Если использовать tailwindMerge, то можно сделать так:
<div
className={twMerge(
"markdown",
"dark:prose-invert",
"prose-sm sm:prose lg:prose-xl",
)}
dangerouslySetInnerHTML={{ __html: post.content }}
/>
Разложить длинные стили на части и сделать это как минимум более читаемым. Сейчас мы поделили наши классы на кастомные, светлые и тёмные. Если у вас километр тайлвинд стилей, то этот плагин сделан для вас.
На этом всё. Я собираюсь снять видеоурок и вместе с ним выложу код на github.