الآن سنبني نظام Dark / Light Mode احترافي 100% باستخدام
⚡ Bootstrap 5.3 الرسمي (وليس CSS عشوائي).
Bootstrap 5.3 يقدم دعم رسمي عبر:
data-bs-theme="light"
data-bs-theme="dark"JavaScriptونحن سنبني نظام احترافي فيه:
- ✅ تبديل يدوي
- ✅ حفظ في localStorage
- ✅ اكتشاف وضع النظام تلقائيًا
- ✅ يعمل في كل التطبيق
- ✅ قابل للتوسع لاحقًا
🎯 الفكرة المعمارية الصحيحة
نحتاج:
ThemeProvider (Context)
↓
يغير data-bs-theme على <html>
↓
>Bootstrap يغير الألوان تلقائيًاJavaScript⚠️ لا نستخدم Redux لهذا الغرض.
🏗 الخطوة 1: إنشاء ThemeContext احترافي
📁 context/ThemeContext.jsx
import { createContext, useEffect, useState } from "react";
/*
ننشئ Context جديد اسمه ThemeContext
ما هو Context؟
هو طريقة في React لمشاركة بيانات (state)
بين مكونات كثيرة بدون الحاجة لتمرير props في كل مستوى.
هنا سنستخدمه لمشاركة:
- قيمة الثيم الحالية (light أو dark)
- دالة تغيير الثيم
*/
export const ThemeContext = createContext();
/*
ThemeProvider هو مكون (Component)
مهمته أن يلف التطبيق بالكامل
ويعطي كل المكونات القدرة على الوصول إلى الثيم.
children = أي شيء موجود داخل <ThemeProvider>
*/
export const ThemeProvider = ({ children }) => {
/*
هذه الدالة تحدد ما هو الثيم الابتدائي عند تشغيل الموقع لأول مرة.
الفكرة:
1️⃣ إذا كان المستخدم اختار ثيم سابقًا → نستخدمه من localStorage
2️⃣ إذا لم يختر → نقرأ إعدادات النظام (الوضع الليلي في الجهاز)
*/
const getInitialTheme = () => {
// نحاول قراءة الثيم المخزن سابقًا
const savedTheme = localStorage.getItem("theme");
// إذا وجدنا ثيم محفوظ نرجعه فورًا
if (savedTheme) {
return savedTheme;
}
/*
إذا لم يوجد ثيم محفوظ
نتحقق من إعدادات النظام
هل المستخدم يستخدم dark mode على جهازه؟
*/
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
// إذا كان النظام dark نرجع dark
// وإلا نرجع light
return prefersDark ? "dark" : "light";
};
/*
useState هنا يخزن الثيم الحالي في React
القيمة الابتدائية هي ناتج getInitialTheme
(لاحظ أننا مررنا الدالة نفسها وليس getInitialTheme()
حتى يتم تنفيذها مرة واحدة فقط)
*/
const [theme, setTheme] = useState(getInitialTheme);
/*
useEffect يعمل كل مرة يتغير فيها theme
مهمته:
1️⃣ وضع attribute على <html> اسمه data-bs-theme
2️⃣ حفظ الثيم في localStorage
*/
useEffect(() => {
/*
هذا السطر هو الأهم 👇
نحن نضع على عنصر html:
<html data-bs-theme="dark">
Bootstrap 5.3 يعتمد على هذا
ليغير الألوان تلقائيًا
*/
document.documentElement.setAttribute("data-bs-theme", theme);
// نحفظ الثيم حتى يبقى بعد refresh
localStorage.setItem("theme", theme);
}, [theme]);
// هذا يعني: نفذ هذا الكود فقط عندما تتغير قيمة theme
/*
هذه دالة بسيطة لتبديل الثيم
إذا كان light يصبح dark
وإذا كان dark يصبح light
*/
const toggleTheme = () => {
setTheme((prevTheme) =>
prevTheme === "light" ? "dark" : "light"
);
};
/*
هنا نعيد Provider
أي مكون داخل ThemeProvider
يستطيع استخدام:
const { theme, toggleTheme } = useContext(ThemeContext)
*/
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};JavaScript🧠 الآن نشرح الفكرة بالكامل بطريقة مبسطة جدًا
🔹 ما الذي يحدث عند تشغيل الموقع؟
1️⃣ React ينشئ ThemeProvider
2️⃣ يتم تشغيل getInitialTheme
3️⃣ يبحث في localStorage
4️⃣ إذا وجد قيمة يستخدمها
5️⃣ إذا لم يجد → يقرأ وضع النظام
6️⃣ يتم وضع data-bs-theme على <html>
7️⃣ Bootstrap يغير الألوان تلقائيًا
🔹 ماذا يحدث عند الضغط على زر تغيير الثيم؟
1️⃣ يتم استدعاء toggleTheme
2️⃣ يتم تغيير state
3️⃣ React يعيد render
4️⃣ useEffect يعمل
5️⃣ يتم تغيير data-bs-theme
6️⃣ Bootstrap يغير الألوان فورًا
🎯 لماذا هذه الطريقة احترافية؟
✔ لا نستخدم Redux (غير ضروري)
✔ لا نعيد تحميل الصفحة
✔ نحفظ التفضيل
✔ نحترم إعدادات النظام
✔ Bootstrap يعمل تلقائيًا
🏆 الشكل النهائي للتدفق
User clicks button
↓
toggleTheme()
↓
setTheme()
↓
useEffect()
↓
<html data-bs-theme="dark">
↓
Bootstrap changes colorsJavaScript🏗 الخطوة 2: لف التطبيق بالـ ThemeProvider
في main.jsx:
<Provider store={store}>
<ThemeProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</ThemeProvider>
</Provider>JavaScript🏗 الخطوة 3: زر تغيير الوضع في Navbar
import { useContext } from "react";
import { ThemeContext } from "../context/ThemeContext";
const ThemeToggle = () => {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button
className="btn btn-outline-secondary"
onClick={toggleTheme}
>
{theme === "light" ? "🌙 Dark" : "☀ Light"}
</button>
);
};export default ThemeToggle;JavaScript🎯 ماذا يحدث الآن؟
1️⃣ عند فتح الموقع لأول مرة
→ يقرأ وضع النظام
2️⃣ عند الضغط على الزر
→ يتغير data-bs-theme
3️⃣ Bootstrap يغير كل الألوان تلقائيًا
4️⃣ يتم حفظ الاختيار في localStorage
5️⃣ عند refresh
→ يبقى الوضع محفوظًا
🔥 نظام احترافي كامل.
🎨 تخصيص ألوانك الخاصة (مهم جدًا)
Bootstrap يسمح لك بتخصيص الألوان لكل وضع.
📁 theme.css
/* الوضع الفاتح */
[data-bs-theme="light"] {
--bs-primary: #0d6efd;
--bs-body-bg: #ffffff;
--bs-body-color: #212529;
}/* الوضع الداكن */
[data-bs-theme="dark"] {
--bs-primary: #bb86fc;
--bs-body-bg: #121212;
--bs-body-color: #f1f1f1;
--bs-card-bg: #1e1e1e;
}JavaScriptثم استورد الملف في main.jsx:
import "./theme.css";JavaScript🔥 الآن يمكنك تخصيص أي متغير من Bootstrap.
🧠 لماذا هذه الطريقة احترافية؟
| ميزة | لماذا مهمة |
|---|---|
| تعتمد على Bootstrap الرسمي | لا hacks |
| Context وليس Redux | بسيط |
| حفظ في localStorage | تجربة أفضل |
| auto detect system | UX حديث |
| لا re-render ثقيل | خفيف |
🏆 الآن لديك نظام Theme احترافي Production-ready
🎯 السطر المرعب 👇
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)"
).matches;JavaScriptسنفهمه من 5 أجزاء.
🧠 أولًا: ما هو window ؟
window هو الكائن الرئيسي في المتصفح.
يمثل:
- الصفحة الحالية
- المتصفح
- كل الخصائص المرتبطة به
مثال:
window.innerWidth
window.location
window.localStorageJavaScript🧠 ثانيًا: ما هي matchMedia() ؟
matchMedia() هي دالة في المتصفح.
وظيفتها:
تسأل المتصفح سؤالًا يشبه سؤال CSS
وتنتظر هل الجواب صحيح أم لا.
🎯 مثال بسيط
window.matchMedia("(max-width: 600px)")JavaScriptهذا يعني:
هل عرض الشاشة أقل من 600px؟
🧠 ماذا تُرجع matchMedia ؟
ترجع كائن اسمه:
MediaQueryListJavaScriptيحتوي على:
{
matches: true or false,
media: "(max-width: 600px)",
addEventListener: function,
removeEventListener: function
}JavaScript🧠 ثالثًا: ما معنى هذا النص داخل الأقواس؟
"(prefers-color-scheme: dark)"JavaScriptهذا ليس JavaScript.
هذا:
Media Query
مثل التي نستخدمها في CSS
🎯 نفس الشيء في CSS
@media (prefers-color-scheme: dark) {
body {
background: black;
}
}JavaScriptيعني:
إذا كان نظام المستخدم في الوضع الداكن
طبق هذه القواعد.
🧠 إذًا ماذا يفعل هذا السطر كاملًا؟
window.matchMedia("(prefers-color-scheme: dark)")JavaScriptيسأل المتصفح:
هل المستخدم يستخدم الوضع الداكن في نظامه؟
🧠 رابعًا: ما هو .matches ؟
.matches هو خاصية (property) داخل الكائن الذي يرجع من matchMedia.
قيمته:
trueإذا الشرط تحققfalseإذا لم يتحقق
🎯 مثال عملي
لو جهاز المستخدم مضبوط على Dark Mode:
window.matchMedia("(prefers-color-scheme: dark)").matchesJavaScriptترجع:
trueJavaScriptولو الجهاز Light:
falseJavaScript🧠 إذًا هذا السطر:
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)"
).matches;JavaScriptيعني:
خزّن في المتغير prefersDark
هل النظام الحالي Dark أم لا
🎯 مثال كامل مبسط
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;if (prefersDark) {
console.log("النظام داكن");
} else {
console.log("النظام فاتح");
}JavaScript🧠 خامسًا: كيف يعرف المتصفح هذا؟
المتصفح يقرأ:
- إعدادات النظام (Windows / Mac / Android / iOS)
- إذا كان المستخدم مفعل Dark Mode
ثم يعكسها في Media Query.
🎯 هل يمكن استخدام أشياء أخرى؟
نعم.
مثلاً:
window.matchMedia("(min-width: 768px)").matchesJavaScriptأو:
window.matchMedia("(orientation: portrait)").matchesJavaScriptأو:
window.matchMedia("(prefers-reduced-motion: reduce)").matchesJavaScript🏆 لماذا نستخدم هذا في ThemeProvider؟
حتى لو لم يختر المستخدم ثيم يدويًا
نحترم إعدادات جهازه.
يعني:
- لو جهازه Dark → موقعك يبدأ Dark
- لو جهازه Light → موقعك يبدأ Light
🔥 هذا UX احترافي جدًا.
🧠 صورة ذهنية بسيطة
جهاز المستخدم
↓
الوضع داكن؟
↓
matchMedia يسأل
↓
true أو false
↓
نحدد الثيم الابتدائيJavaScript🎯 خلاصة سطر “”
| الجزء | ماذا يفعل |
|---|---|
| window | المتصفح |
| matchMedia | يسأل سؤال Media Query |
| “(prefers-color-scheme: dark)” | هل النظام Dark؟ |
| .matches | يرجع true أو false |
🚀 المستوى الأعلى القادم
يمكننا تطويره ليصبح:
- 🔥 ثلاث أوضاع (light / dark / system)
- 🔥 تبديل أيقونة متحركة
- 🔥 transition ناعم بين الأوضاع
- 🔥 تخصيص Navbar مختلف لكل وضع
🚀 المستوى الأعلى القادم يمكننا تطويره ليصبح: 🔥 ثلاث أوضاع (light / dark / system) 🔥 تبديل أيقونة متحركة 🔥 transition ناعم بين الأوضاع 🔥 تخصيص Navbar مختلف لكل وضع بالتفصيل وشرح لكل خطوة
ممتاز 🔥
الآن سنبني نظام Theme احترافي جدًا بمستوى مواقع Production مثل GitHub 👌
سنضيف:
- ✅ 3 أوضاع:
light/dark/system - ✅ اكتشاف وضع النظام تلقائيًا
- ✅ أيقونة متحركة
- ✅ Transition ناعم بين الأوضاع
- ✅ Navbar مخصص لكل وضع
- ✅ حفظ الاختيار في localStorage
- ✅ متوافق 100% مع Bootstrap 5.3
🧠 أولاً: الفكرة المعمارية
ThemeProvider (Context)
↓
theme: "light" | "dark" | "system"
↓
resolveTheme → (light أو dark)
↓
document.documentElement.setAttribute("data-bs-theme")
↓
Bootstrap يغير الألوان
🏗 الخطوة 1: ThemeContext احترافي بثلاث أوضاع
📁 context/ThemeContext.jsx
import { createContext, useEffect, useState } from "react";export const ThemeContext = createContext();export const ThemeProvider = ({ children }) => { // 1️⃣ قراءة الثيم من localStorage أو افتراضي system
const getStoredTheme = () => {
return localStorage.getItem("theme") || "system";
}; const [theme, setTheme] = useState(getStoredTheme); // 2️⃣ تحديد الثيم الفعلي
const getSystemTheme = () => {
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}; const resolveTheme = theme === "system" ? getSystemTheme() : theme; // 3️⃣ تطبيق الثيم
useEffect(() => {
document.documentElement.setAttribute("data-bs-theme", resolveTheme);
localStorage.setItem("theme", theme);
}, [theme, resolveTheme]); // 4️⃣ الاستماع لتغير وضع النظام
useEffect(() => {
const media = window.matchMedia("(prefers-color-scheme: dark)"); const handleChange = () => {
if (theme === "system") {
document.documentElement.setAttribute(
"data-bs-theme",
getSystemTheme()
);
}
}; media.addEventListener("change", handleChange); return () => media.removeEventListener("change", handleChange);
}, [theme]); return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
};
🧠 ماذا يحدث هنا؟
| الجزء | ماذا يفعل |
|---|---|
| theme | القيمة المختارة |
| resolveTheme | الثيم الحقيقي المطبق |
| system | يعتمد على إعدادات الجهاز |
| useEffect 1 | يطبق الثيم ويحفظه |
| useEffect 2 | يراقب تغير نظام المستخدم |
🔥 الآن لدينا 3 أوضاع حقيقية.
🏗 الخطوة 2: لف التطبيق
<Provider store={store}>
<ThemeProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</ThemeProvider>
</Provider>
🏗 الخطوة 3: زر اختيار احترافي (Dropdown)
import { useContext } from "react";
import { ThemeContext } from "../context/ThemeContext";const ThemeSwitcher = () => { const { theme, setTheme } = useContext(ThemeContext); return (
<div className="dropdown">
<button
className="btn btn-outline-secondary dropdown-toggle"
data-bs-toggle="dropdown"
>
{theme === "light" && "☀ Light"}
{theme === "dark" && "🌙 Dark"}
{theme === "system" && "🖥 System"}
</button> <ul className="dropdown-menu">
<li>
<button className="dropdown-item" onClick={() => setTheme("light")}>
☀ Light
</button>
</li>
<li>
<button className="dropdown-item" onClick={() => setTheme("dark")}>
🌙 Dark
</button>
</li>
<li>
<button className="dropdown-item" onClick={() => setTheme("system")}>
🖥 System
</button>
</li>
</ul>
</div>
);
};export default ThemeSwitcher;
🔥 هذا مستوى احترافي.
🏗 الخطوة 4: Transition ناعم
📁 theme.css
* {
transition: background-color 0.3s ease,
color 0.3s ease,
border-color 0.3s ease;
}
🔥 الآن الانتقال بين الأوضاع ناعم.
🏗 الخطوة 5: تخصيص Navbar حسب الوضع
Bootstrap يغير الألوان تلقائيًا
لكن لو أردنا تخصيص إضافي:
/* Light Navbar */
[data-bs-theme="light"] .navbar {
background-color: #ffffff !important;
}/* Dark Navbar */
[data-bs-theme="dark"] .navbar {
background-color: #1a1a1a !important;
}
🏗 تخصيص ألوان إضافية
[data-bs-theme="dark"] {
--bs-primary: #bb86fc;
--bs-body-bg: #121212;
--bs-card-bg: #1e1e1e;
}[data-bs-theme="light"] {
--bs-primary: #0d6efd;
}
🧠 كيف يعمل النظام بالكامل؟
1️⃣ المستخدم يختار وضع
2️⃣ theme يتغير
3️⃣ resolveTheme يحسب الثيم الفعلي
4️⃣ data-bs-theme يتغير
5️⃣ Bootstrap يعيد تلوين كل العناصر
6️⃣ localStorage يحفظ الاختيار
7️⃣ refresh → يبقى الوضع
🔥 بدون Redux
🔥 بدون إعادة تحميل
🔥 بدون تعقيد