Skip to content

Commit

Permalink
fix!: dark theme (via cookies)
Browse files Browse the repository at this point in the history
add actual preferred color theme switch support via cookies

⚠️ This commit introduce several issues directly linked to the App
Router's current limitations :

1. preferred-color-scheme isn't supported anymore
2. every route is now dynamically rendered by default as we use cookies
in the root layout

A better approach would be to load a client script as
"beforeInteractive" to (1) check if a "color-theme" preferrence has been
set in the local storage (2) if not, set it based on
preferred-color-scheme and (3) add or remove the 'dark' class on
document.documentElement accordingly. Then, the DarkModToggle element
could work exactly as it is, but using the local storage instead
of a cookie.

Unfortunately, this wouldn't be a better approach at the moment as the
Next.js Script component isn't supported by the App Router.

see vercel/next.js#51242
and https://nextjs.org/docs/app/api-reference/functions/cookies
and https://nextjs.org/docs/pages/api-reference/components/script#beforeinteractive

inspired by https://michaelangelo.io/blog/darkmode-rsc
  • Loading branch information
nweldev committed Sep 4, 2023
1 parent 062ef94 commit bb35abe
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 32 deletions.
20 changes: 11 additions & 9 deletions src/app/[lang]/_header/DarkModeToggle.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
'use client';

import classNames from 'classnames';
import { ComponentProps, FC, useContext, useEffect, useState } from 'react';
import { ComponentProps, useState } from 'react';
import Cookies from 'js-cookie';
import { useRouter } from 'next/navigation';

export interface DarkModeToggleProps extends ComponentProps<'button'> {}

type ThemeColor = 'dark' | 'light';

export function DarkModeToggle({ className }: DarkModeToggleProps) {
const preferedTheme: ThemeColor = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
const [theme, setTheme] = useState(preferedTheme);
export default function DarkModeToggle({ className }: DarkModeToggleProps) {
const router = useRouter();
const theme = (Cookies.get('color-theme') as ThemeColor | undefined) || 'light';

const toggleTheme = () => {
if (theme === 'dark') {
setTheme('light');
document.documentElement.classList.remove('dark');
Cookies.set('color-theme', 'light');
} else {
setTheme('dark');
document.documentElement.classList.add('dark');
Cookies.set('color-theme', 'dark');
}
window.location.reload();
router.refresh();
};

return (
Expand All @@ -42,7 +44,7 @@ export function DarkModeToggle({ className }: DarkModeToggleProps) {
</svg>
<svg
id="theme-toggle-light-icon"
className={classNames('w-5 h-5', { hidden: theme === 'light'})}
className={classNames('w-5 h-5', { hidden: theme === 'light' })}
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
Expand Down
43 changes: 23 additions & 20 deletions src/app/[lang]/_header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
import { DarkModeToggle } from './DarkModeToggle';
import LocaleSwitcher from './LocaleSwitcher';
import dynamic from 'next/dynamic';

export function Header({ author } : { author : string }) {
const DarkModeToggle = dynamic(() => import('./DarkModeToggle'), {
ssr: false,
});

export function Header({ author }: { author: string }) {
return (
<div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm md:flex">
<div className="flex h-12 w-full item-start justify-center md:h-auto md:w-auto">
<LocaleSwitcher />
</div>
<div className="">
<DarkModeToggle />
</div>
<div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black md:static md:h-auto md:w-auto md:bg-none">
<a
className="pointer-events-none flex place-items-center gap-2 p-8 md:pointer-events-auto md:p-0"
href="https://nwel.dev"
target="_blank"
rel="noopener noreferrer"
>
{author}
</a>
</div>
<div className="flex h-12 w-full item-start justify-center md:h-auto md:w-auto">
<LocaleSwitcher />
</div>
)
}
<div className="">
<DarkModeToggle />
</div>
<div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black md:static md:h-auto md:w-auto md:bg-none">
<a
className="pointer-events-none flex place-items-center gap-2 p-8 md:pointer-events-auto md:p-0"
href="https://nwel.dev"
target="_blank"
rel="noopener noreferrer"
>
{author}
</a>
</div>
</div>
);
}
12 changes: 9 additions & 3 deletions src/app/[lang]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import './globals.css';
import { cookies } from 'next/headers';
import type { Metadata } from 'next';
import classNames from 'classnames';
import { Inter } from 'next/font/google';
import { Locale } from '@/i18n-config';

const inter = Inter({ subsets: ['latin'] });

Expand All @@ -14,10 +17,13 @@ export const metadata: Metadata = {
},
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
export default function RootLayout({ children, params: { lang } }: { children: React.ReactNode; params: { lang: Locale } }) {
const prefersDark = cookies().get('color-theme')?.value === 'dark';
return (
<html lang="en">
<body className={inter.className}>{children}</body>
<html lang={lang} className={classNames({ dark: prefersDark })}>
<body className={inter.className} suppressHydrationWarning={true}>
{children}
</body>
</html>
);
}

0 comments on commit bb35abe

Please sign in to comment.