{"id":17463300,"url":"https://github.com/pixeledcode/next-dark-mode","last_synced_at":"2026-04-29T18:34:26.618Z","repository":{"id":212784926,"uuid":"732309425","full_name":"PixeledCode/next-dark-mode","owner":"PixeledCode","description":"Pattern documentation for implementing proper dark mode in Next app router","archived":false,"fork":false,"pushed_at":"2023-12-17T13:15:03.000Z","size":82,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-28T07:11:20.002Z","etag":null,"topics":["cookie","css-variables","dark-theme","nextjs"],"latest_commit_sha":null,"homepage":"https://next-dark-mode-px.vercel.app","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/PixeledCode.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-12-16T08:44:22.000Z","updated_at":"2024-11-03T03:30:35.000Z","dependencies_parsed_at":"2023-12-16T09:55:47.408Z","dependency_job_id":"6054345d-21f7-43f0-a870-bf22f7fc30a2","html_url":"https://github.com/PixeledCode/next-dark-mode","commit_stats":null,"previous_names":["pixeledcode/next-dark-mode"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/PixeledCode/next-dark-mode","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PixeledCode%2Fnext-dark-mode","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PixeledCode%2Fnext-dark-mode/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PixeledCode%2Fnext-dark-mode/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PixeledCode%2Fnext-dark-mode/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/PixeledCode","download_url":"https://codeload.github.com/PixeledCode/next-dark-mode/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PixeledCode%2Fnext-dark-mode/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32439284,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-29T18:12:22.909Z","status":"ssl_error","status_checked_at":"2026-04-29T18:11:33.322Z","response_time":110,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cookie","css-variables","dark-theme","nextjs"],"created_at":"2024-10-18T10:09:18.531Z","updated_at":"2026-04-29T18:34:26.604Z","avatar_url":"https://github.com/PixeledCode.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Next 13/14 Dark Mode\n\nThis repository is a separate implementation of the [Dark Mode in Joy of React](https://github.com/joy-of-react/next-13-dark-mode/tree/main). This differs by relying on CSS Variables and uses `prefers-color-scheme` API to detect the user's preference for first time visitors.\n\n[Live Site](https://next-dark-mode-px.vercel.app/)\n\n## Getting Started\n\nThis is made for Next.js `/app` router. But the methodology should work with any React framework that generates `html` on the server.\n\nLet's start by creating a simple toggle without user preference detection.\n\n1. Creating a component for toggling theme. [/components/theme-toggle.tsx](/app/components/theme-toggle.tsx)\n\n```tsx\n'use client'\n\nimport React from 'react'\nimport Cookie from 'js-cookie'\n\nexport const ThemeToggle = ({ initialTheme }: { initialTheme: string }) =\u003e {\n\tconst [theme, setTheme] = React.useState(initialTheme)\n\n\tfunction handleClick() {\n\t\t// get next theme\n\t\tconst newTheme = theme === 'light' ? 'dark' : 'light'\n\t\t// set theme for local state\n\t\tsetTheme(newTheme)\n\n\t\t// set cookies for next visit\n\t\tCookie.set('theme-color', newTheme, {\n\t\t\texpires: 1000,\n\t\t})\n\n\t\t// set theme for html\n\t\tdocument.documentElement.setAttribute('data-theme-color', newTheme)\n\t}\n\n\treturn (\n\t\t\u003cbutton onClick={handleClick}\u003e\n\t\t\tSwitch to {theme === 'light' ? 'dark' : 'light'} mode\n\t\t\u003c/button\u003e\n\t)\n}\n```\n\n\u003e We are using [js-cookie](https://www.npmjs.com/package/js-cookie) to handle cookies. You can use any library or write your own.\n\n2. In `/app/layout.tsx`, we will look for the cookie and set the theme for the visitors.\n\n```tsx\nimport { cookies } from 'next/headers'\nimport { ThemeToggle } from './components/theme-toggle'\nimport './globals.css'\n\nexport default function RootLayout({\n\tchildren,\n}: {\n\tchildren: React.ReactNode\n}) {\n\tconst savedTheme = cookies().get('theme-color')\n\t// if no cookie, set theme to light\n\tconst theme = savedTheme?.value || 'light'\n\n\treturn (\n\t\t\u003chtml lang=\"en\" data-theme-color={theme}\u003e\n\t\t\t\u003cbody\u003e\n\t\t\t\t\u003cheader className=\"header\"\u003e\n\t\t\t\t\t\u003cThemeToggle initialTheme={theme} /\u003e\n\t\t\t\t\u003c/header\u003e\n\t\t\t\t{children}\n\t\t\t\u003c/body\u003e\n\t\t\u003c/html\u003e\n\t)\n}\n```\n\n## Adding User Preference Detection\n\nNow that we have a working toggle, let's add user preference detection. We will use `prefers-color-scheme` API to detect the user's preference for visitors.\n\n1. In `/app/layout.tsx`, we will inject a script in the head that will run on the client.\n\n```tsx\nconst ThemeScript = () =\u003e {\n\tconst codeToRunOnClient = `\n\t\t(function() {\n\t\t\tif(document.documentElement.getAttribute('data-theme-color') === 'system' \u0026\u0026 window.matchMedia) {\n\t\t\t\tconst theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';\n\t\t\t\tdocument.documentElement.setAttribute('data-theme-color', theme)\n\n\t\t\t\twindow.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event =\u003e {\n\t\t\t\t\tconst newColorScheme = event.matches ? \"dark\" : \"light\";\n\t\t\t\t\tdocument.documentElement.setAttribute('data-theme-color', newColorScheme)\n\t\t\t});\n\t\t\t}\n\t\t})()\n\t`\n\n\treturn \u003cscript dangerouslySetInnerHTML={{ __html: codeToRunOnClient }} /\u003e\n}\n```\n\n\u003e We are using IIFE to avoid polluting the global namespace.\n\n\u003e Injecting the script is intentional, we are XSSing ourselves.\n\n2. Now we will add the script to the head and also change default theme to `system`. `ThemeScript` will detect that the theme is `system` and will set the theme based on user preference.\n\n```tsx\n...\n\nexport default function RootLayout({\n\tchildren,\n}: {\n\tchildren: React.ReactNode\n}) {\n\tconst savedTheme = cookies().get('theme-color')\n\tconst theme = savedTheme?.value || 'system'\n\n\treturn (\n\t\t\u003chtml lang=\"en\" data-theme-color={theme}\u003e\n\t\t\t\u003chead\u003e\n\t\t\t\t\u003cThemeScript /\u003e\n\t\t\t\u003c/head\u003e\n\t\t\t\u003cbody className={inter.className}\u003e\n\t\t\t\t\u003cheader className=\"header\"\u003e\n\t\t\t\t\t\u003cThemeToggle initialTheme={theme} /\u003e\n\t\t\t\t\u003c/header\u003e\n\t\t\t\t{children}\n\t\t\t\u003c/body\u003e\n\t\t\u003c/html\u003e\n\t)\n}\n```\n\n3. This works but we need to sync the toggle with the user preference.\n\n```tsx\n// /components/theme-toggle.tsx\n\nReact.useEffect(() =\u003e {\n\tif (initialTheme === 'system') {\n\t\tsetTheme(\n\t\t\tdocument.documentElement.getAttribute('data-theme-color') || 'light'\n\t\t)\n\t}\n}, [initialTheme])\n```\n\n4. Also change the button to reflect the user preference. Everthing else will remain the same.\n\n```tsx\nif (theme === 'system') {\n\treturn \u003cbutton className={styles.toggle}\u003eLoading\u003c/button\u003e\n}\n\nreturn (\n\t\u003cbutton onClick={handleClick} className={styles.toggle}\u003e\n\t\tSwitch to {theme === 'light' ? 'dark' : 'light'} mode\n\t\u003c/button\u003e\n)\n```\n\n## CSS Variables\n\nWe are using CSS Variables to set the theme. It's upto you on how to manage the variables. One way is to create a `/tokens.css` file and import it in `/globals.css`.\n\n```css\n/* /tokens.css */\n:root,\n[data-theme-color='light'] {\n\t--foreground-rgb: 0, 0, 0;\n\t--background-start-rgb: 214, 219, 220;\n\t--background-end-rgb: 255, 255, 255;\n\n\t--callout-rgb: 238, 240, 241;\n\t--callout-border-rgb: 172, 175, 176;\n\t--card-rgb: 180, 185, 188;\n\t--card-border-rgb: 131, 134, 135;\n}\n\n[data-theme-color='dark'] {\n\tcolor-scheme: dark;\n\t--foreground-rgb: 255, 255, 255;\n\t--background-start-rgb: 0, 0, 0;\n\t--background-end-rgb: 0, 0, 0;\n\n\t--callout-rgb: 20, 20, 20;\n\t--callout-border-rgb: 108, 108, 108;\n\t--card-rgb: 100, 100, 100;\n\t--card-border-rgb: 200, 200, 200;\n}\n```\n\n```css\n/* /globals.css */\n@import './tokens.css';\n\n...\n```\n\n### CSS based on theme\n\nSince we are using [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes) to set the theme, we can use CSS to change the styles based on theme. We cannot rely on `@media (prefers-color-scheme: dark)` anymore.\n\n```css\n[data-theme-color='dark'] {\n\t.logo {\n\t\tfilter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);\n\t}\n}\n```\n\n## Tailwind\n\n\u003e Work in progress\n\nGeneral idea is to use classes to set the theme instead of `data-theme-color`. Then in `tailwind.config.js`, set the dark mode property to class:\n\n```js\nmodule.exports = {\n\tdarkMode: 'class',\n}\n```\n\nUsing dark mode classes is possible then:\n\n```html\n\u003cdiv className=\"bg-white dark:bg-black\"\u003e\u003c/div\u003e\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpixeledcode%2Fnext-dark-mode","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpixeledcode%2Fnext-dark-mode","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpixeledcode%2Fnext-dark-mode/lists"}