پیادهسازی تم روشن و تاریک در Next.js 16 بدون وابستگی
روشی بدون وابستگی برای ساخت سیستم تم روشن و تاریک در Next.js 16 با Tailwind CSS.
پیادهسازی تم روشن و تاریک در Next.js 16 بدون وابستگی
یک برنامه وب مدرن باید تجربهای راحت و مناسب را در محیطهای روشن و تاریک فراهم کند.
پشتیبانی از تم اکنون به یک ویژگی مورد انتظار در بیشتر برنامههای مدرن تبدیل شده است. کاربران میخواهند رابط کاربری با ترجیح سیستم عامل یا انتخاب شخصی آنها هماهنگ باشد.
بسیاری از توسعهدهندگان از کتابخانههایی مانند @wrksz/themes استفاده میکنند، اما درک نحوه کار سیستم تم در داخل برنامه به ما کمک میکند برنامههای بهتری بسازیم.
در این مقاله، یک سیستم کامل تم روشن و تاریک را در Next.js 16 بدون نصب هیچ کتابخانه تم خارجی ایجاد میکنیم.
چیزی که خواهیم ساخت
سیستم تم ما از موارد زیر پشتیبانی خواهد کرد:
- حالت روشن
- حالت تاریک
- ترجیح سیستم
- ذخیره انتخاب کاربر
- یکپارچهسازی با Tailwind CSS
- رندر امن در برابر Hydration
جریان نهایی:
کاربر تم را تغییر میدهد
|
v
Theme Provider وضعیت را بهروزرسانی میکند
|
v
کلاس HTML تغییر میکند
|
v
متغیرهای CSS بهروزرسانی میشوند
|
v
رابط کاربری بهصورت خودکار تغییر میکنداستک پروژه
این پیادهسازی از موارد زیر استفاده میکند:
- Next.js 16 App Router
- React
- TypeScript
- Tailwind CSS v4
نحوه کار تغییر تم
یک سیستم تم بر اساس تغییر کلاسها روی عنصر ریشه HTML کار میکند.
<html class="dark">
وقتی کلاس dark وجود داشته باشد، Tailwind استایلهای حالت تاریک را اعمال میکند.
وقتی این کلاس حذف شود، برنامه دوباره به حالت روشن بازمیگردد.
تشخیص ترجیح سیستم
مرورگرها یک API داخلی برای تشخیص تم سیستم عامل ارائه میدهند.
window.matchMedia('(prefers-color-scheme: dark)');این API به برنامه ما اجازه میدهد از ترجیح تم سیستم کاربر پیروی کند.
ایجاد متغیرهای CSS
پایه سیستم تم ما متغیرهای CSS هستند.
:root {
--background: #ffffff;
--foreground: #171717;
}
.dark {
--background: #0a0a0a;
--foreground: #ededed;
}نام متغیرهای CSS یکسان باقی میماند، اما مقادیر آنها بر اساس تم فعال تغییر میکند.
اتصال متغیرها به Tailwind
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
}اکنون کامپوننتها از رنگهای معنایی استفاده میکنند:
<div className="bg-background text-foreground">Content</div>ایجاد نوع Theme
export type Theme = 'light' | 'dark' | 'system';ایجاد Theme Provider
Provider وضعیت فعلی تم را مدیریت میکند.
'use client';
import { createContext, useContext, useEffect, useState } from 'react';
import type { Theme } from '@/lib/theme';
type ThemeContextType = {
theme: Theme;
setTheme: (theme: Theme) => void;
};
const ThemeContext = createContext<ThemeContextType | null>(null);
function getStoredTheme(): Theme {
if (typeof window === 'undefined') {
return 'system';
}
const stored = localStorage.getItem('theme');
if (stored === 'light' || stored === 'dark' || stored === 'system') {
return stored;
}
return 'system';
}
function applyTheme(theme: Theme) {
const root = document.documentElement;
root.classList.remove('light', 'dark');
if (theme === 'system') {
const dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
root.classList.add(dark ? 'dark' : 'light');
return;
}
root.classList.add(theme);
}
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setThemeState] = useState<Theme>(getStoredTheme);
useEffect(() => {
applyTheme(theme);
localStorage.setItem('theme', theme);
}, [theme]);
return (
<ThemeContext.Provider
value={{
theme,
setTheme: setThemeState,
}}
>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be inside ThemeProvider');
}
return context;
}اعمال تم روی HTML
تم فعال با اضافه کردن یک کلاس به عنصر ریشه کنترل میشود.
document.documentElement.classList.remove('light', 'dark');
document.documentElement.classList.add('dark');ذخیره ترجیح کاربر
انتخاب کاربر باید پس از بارگذاری مجدد صفحه نیز حفظ شود.
localStorage.setItem('theme', 'dark');خواندن مقدار:
const theme = localStorage.getItem('theme');مدیریت تم سیستم
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;مشکل Hydration
Next.js ابتدا HTML را در سرور رندر میکند.
سرور به موارد زیر دسترسی ندارد:
- window
- localStorage
- ترجیحات مرورگر
این موضوع میتواند باعث خطاهای عدم تطابق Hydration شود.
مثال:
سرور:
<button>
Sun
</button>کلاینت:
<button>
Moon
</button>حل مشکلات Hydration
رابط کاربری وابسته به کلاینت باید تا زمان mount شدن برنامه روی کلاینت منتظر بماند.
بهجای دنبال کردن وضعیت mount با useState و useEffect، میتوانیم با استفاده از useSyncExternalStore یک hook قابل استفاده مجدد ایجاد کنیم.
'use client';
import { useSyncExternalStore } from 'react';
const emptySubscribe = () => {
return () => {};
};
export function useMounted() {
return useSyncExternalStore(
emptySubscribe,
() => true,
() => false,
);
}این hook موارد زیر را برمیگرداند:
falseهنگام رندر سمت سرورtrueپس از Hydration در کلاینت
این کار از خطاهای عدم تطابق Hydration در React جلوگیری میکند.
مثال:
سرور:
<button>
Sun
</button>کلاینت:
<button>
Moon
</button>ساختار HTML یکسان است، اما محتوا متفاوت است. React این تفاوت را هنگام Hydration تشخیص میدهد.
استفاده از Mounted Hook
این hook را میتوان در کامپوننتهایی استفاده کرد که به قابلیتهای مخصوص مرورگر وابسته هستند.
'use client';
import { useMounted } from '@/hooks/use-mounted';
export function ThemeToggle() {
const mounted = useMounted();
if (!mounted) {
return null;
}
return <button>Dark Mode</button>;
}اکنون سرور یک نتیجه پایدار رندر میکند و کلاینت پس از Hydration کنترلهای تعاملی تم را رندر میکند.
ایجاد Theme Toggle
'use client';
import { Moon, Sun } from 'lucide-react';
import { useTheme } from './theme-provider';
import { useMounted } from '@/hooks/use-mounted';
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
const mounted = useMounted();
if (!mounted) {
return (
<button className="rounded-full border p-3" aria-label="Theme toggle" />
);
}
return (
<button
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
className="hover:bg-muted rounded-full border p-3 transition"
aria-label="Theme toggle"
>
{theme === 'dark' ? <Moon size={20} /> : <Sun size={20} />}
</button>
);
}برای سادگی، این toggle فقط بین حالتهای صریح روشن و تاریک جابهجا میشود. در یک برنامه production میتوان گزینه سومی برای ترجیح سیستم نیز ارائه کرد.
ساختار نهایی پروژه
components/
└── theme/
├── theme-provider.tsx
└── theme-toggle.tsx
hooks/
└── use-mounted.ts
lib/
└── theme.ts
app/
└── layout.tsx
globals.cssمسئولیتها
| بخش | مسئولیت |
|---|---|
| CSS Variables | ذخیره رنگهای تم |
| Theme Provider | مدیریت وضعیت تم |
| localStorage | ذخیره انتخاب کاربر |
| matchMedia | تشخیص تم سیستم |
| useMounted Hook | جلوگیری از عدم تطابق Hydration |
| Toggle Component | تعامل کاربر |
نتیجهگیری
ساخت یک سیستم تم بدون وابستگی به ما کمک میکند بفهمیم کتابخانههایی مانند @wrksz/themes در داخل چگونه کار میکنند.
برای پروژههای آموزشی و برنامههای کوچک، یک پیادهسازی سفارشی سبک است و کنترل کاملی در اختیار ما قرار میدهد.
بخش مهم، درک سازوکاری است که در پشت ابزارهایی که استفاده میکنیم وجود دارد.

