Skip to content

Темизация Markdown проекта с next-themes и Tailwind Typography

Светлая и тёмная темы. Сначала попробуем разобраться с одной из них, а потом уж добавим вторую. Я сам лично работаю в светлой теме. Но давайте начнём.

light-dark.webp

Прежде всего нам нужно установить пакет 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.

theme-for-markdown.webp

После установки @tailwindcss/typography нам нужно сделать всего 2 вещи:

  1. Проверить классы в 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"

  2. В наш app/globals.css нужно добавить вверху файла после импорта @import "tailwindcss"; одну строчку @plugin '@tailwindcss/typography';.

Результат стилей prose от @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.