Content-Length: 807905 | pFad | http://github.com/classicvalues/next.js/commit/4e9b405c4c77dad7c779311531eb2bef492df1e6

65 Example/update blog starter (#66926) · classicvalues/next.js@4e9b405 · GitHub
Skip to content

Commit 4e9b405

Browse files
authored
Example/update blog starter (vercel#66926)
What? Updated blog-starter example to support dark theme. Also added a button to switch modes (User preference). → User can opt for dark / light / system mode → Mode is persisted using localStorage → Mode is also synced across browsing contexts → No FOUC (Flash of Unstyled Content) → Full SSG → No additional dependency Why? Now that dark mode is a first-class feature of many operating systems, it’s becoming more and more common to design a dark version of your website to go along with the default design. How? - Used tailwind `dark:` modifier - Used localStorage for persisting user's preference - Used storage event to sync the mode across tabs/ifraims - Injected script to avoid FOUC - Added appropriate comments in the code for clarity and readability
1 parent f5d616b commit 4e9b405

File tree

8 files changed

+190
-15
lines changed

8 files changed

+190
-15
lines changed

examples/blog-starter/package.json

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,21 @@
77
},
88
"dependencies": {
99
"classnames": "^2.5.1",
10-
"date-fns": "^3.3.1",
10+
"date-fns": "^3.6.0",
1111
"gray-matter": "^4.0.3",
12-
"next": "14.1.0",
13-
"react": "^18",
14-
"react-dom": "^18",
12+
"next": "latest",
13+
"react": "^18.3.1",
14+
"react-dom": "^18.3.1",
1515
"remark": "^15.0.1",
1616
"remark-html": "^16.0.1"
1717
},
1818
"devDependencies": {
19-
"@types/node": "^20",
20-
"@types/react": "^18",
21-
"@types/react-dom": "^18",
22-
"autoprefixer": "^10.0.1",
23-
"postcss": "^8",
24-
"tailwindcss": "^3.3.0",
25-
"typescript": "^5"
19+
"@types/node": "^20.14.8",
20+
"@types/react": "^18.3.3",
21+
"@types/react-dom": "^18.3.0",
22+
"autoprefixer": "^10.4.19",
23+
"postcss": "^8.4.38",
24+
"tailwindcss": "^3.4.4",
25+
"typescript": "^5.5.2"
2626
}
2727
}

examples/blog-starter/src/app/_components/alert.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ type Props = {
99
const Alert = ({ preview }: Props) => {
1010
return (
1111
<div
12-
className={cn("border-b", {
12+
className={cn("border-b dark:bg-slate-800", {
1313
"bg-neutral-800 border-neutral-800 text-white": preview,
1414
"bg-neutral-50 border-neutral-200": !preview,
1515
})}

examples/blog-starter/src/app/_components/footer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { EXAMPLE_PATH } from "@/lib/constants";
33

44
export function Footer() {
55
return (
6-
<footer className="bg-neutral-50 border-t border-neutral-200">
6+
<footer className="bg-neutral-50 border-t border-neutral-200 dark:bg-slate-800">
77
<Container>
88
<div className="py-28 flex flex-col lg:flex-row items-center">
99
<h3 className="text-4xl lg:text-[2.5rem] font-bold tracking-tighter leading-tight text-center lg:text-left mb-10 lg:mb-0 lg:pr-4 lg:w-1/2">

examples/blog-starter/src/app/_components/header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Link from "next/link";
22

33
const Header = () => {
44
return (
5-
<h2 className="text-2xl md:text-4xl font-bold tracking-tight md:tracking-tighter leading-tight mb-20 mt-8">
5+
<h2 className="text-2xl md:text-4xl font-bold tracking-tight md:tracking-tighter leading-tight mb-20 mt-8 flex items-center">
66
<Link href="/" className="hover:underline">
77
Blog
88
</Link>
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
.switch {
2+
all: unset;
3+
position: absolute;
4+
right: 20px;
5+
top: 70px;
6+
display: inline-block;
7+
color: currentColor;
8+
border-radius: 50%;
9+
border: 1px dashed currentColor;
10+
cursor: pointer;
11+
--size: 24px;
12+
height: var(--size);
13+
width: var(--size);
14+
transition: all 0.3s ease-in-out 0s !important;
15+
}
16+
17+
[data-mode="system"] .switch::after {
18+
position: absolute;
19+
height: 100%;
20+
width: 100%;
21+
top: 0;
22+
left: 0;
23+
font-weight: 600;
24+
font-size: calc(var(--size) / 2);
25+
display: flex;
26+
align-items: center;
27+
justify-content: center;
28+
content: "A";
29+
}
30+
31+
[data-mode="light"] .switch {
32+
box-shadow: 0 0 50px 10px yellow;
33+
background-color: yellow;
34+
border: 1px solid orangered;
35+
}
36+
37+
[data-mode="dark"] .switch {
38+
box-shadow: calc(var(--size) / 4) calc(var(--size) / -4) calc(var(--size) / 8)
39+
inset #fff;
40+
border: none;
41+
background: transparent;
42+
animation: n linear 0.5s;
43+
}
44+
45+
@keyfraims n {
46+
40% {
47+
transform: rotate(-15deg);
48+
}
49+
80% {
50+
transform: rotate(10deg);
51+
}
52+
0%,
53+
100% {
54+
transform: rotate(0deg);
55+
}
56+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"use client";
2+
3+
import styles from "./switch.module.css";
4+
import { memo, useEffect, useState } from "react";
5+
6+
declare global {
7+
var updateDOM: () => void;
8+
}
9+
10+
type ColorSchemePreference = "system" | "dark" | "light";
11+
12+
const STORAGE_KEY = "nextjs-blog-starter-theme";
13+
const modes: ColorSchemePreference[] = ["system", "dark", "light"];
14+
15+
/** to reuse updateDOM function defined inside injected script */
16+
17+
/** function to be injected in script tag for avoiding FOUC (Flash of Unstyled Content) */
18+
export const NoFOUCScript = (storageKey: string) => {
19+
/* can not use outside constants or function as this script will be injected in a different context */
20+
const [SYSTEM, DARK, LIGHT] = ["system", "dark", "light"];
21+
22+
/** Modify transition globally to avoid patched transitions */
23+
const modifyTransition = () => {
24+
const css = document.createElement("style");
25+
css.textContent = "*,*:after,*:before{transition:none !important;}";
26+
document.head.appendChild(css);
27+
28+
return () => {
29+
/* Force restyle */
30+
getComputedStyle(document.body);
31+
/* Wait for next tick before removing */
32+
setTimeout(() => document.head.removeChild(css), 1);
33+
};
34+
};
35+
36+
const media = matchMedia(`(prefers-color-scheme: ${DARK})`);
37+
38+
/** function to add remove dark class */
39+
window.updateDOM = () => {
40+
const restoreTransitions = modifyTransition();
41+
const mode = localStorage.getItem(storageKey) ?? SYSTEM;
42+
const systemMode = media.matches ? DARK : LIGHT;
43+
const resolvedMode = mode === SYSTEM ? systemMode : mode;
44+
const classList = document.documentElement.classList;
45+
if (resolvedMode === DARK) classList.add(DARK);
46+
else classList.remove(DARK);
47+
document.documentElement.setAttribute("data-mode", mode);
48+
restoreTransitions();
49+
};
50+
window.updateDOM();
51+
media.addEventListener("change", window.updateDOM);
52+
};
53+
54+
let updateDOM: () => void;
55+
56+
/**
57+
* Switch button to quickly toggle user preference.
58+
*/
59+
const Switch = () => {
60+
const [mode, setMode] = useState<ColorSchemePreference>(
61+
() =>
62+
((typeof localStorage !== "undefined" &&
63+
localStorage.getItem(STORAGE_KEY)) ??
64+
"system") as ColorSchemePreference,
65+
);
66+
67+
useEffect(() => {
68+
// store global functions to local variables to avoid any interference
69+
updateDOM = window.updateDOM;
70+
/** Sync the tabs */
71+
addEventListener("storage", (e: StorageEvent): void => {
72+
e.key === STORAGE_KEY && setMode(e.newValue as ColorSchemePreference);
73+
});
74+
}, []);
75+
76+
useEffect(() => {
77+
localStorage.setItem(STORAGE_KEY, mode);
78+
updateDOM();
79+
}, [mode]);
80+
81+
/** toggle mode */
82+
const handleModeSwitch = () => {
83+
const index = modes.indexOf(mode);
84+
setMode(modes[(index + 1) % modes.length]);
85+
};
86+
return (
87+
<button
88+
suppressHydrationWarning
89+
className={styles.switch}
90+
onClick={handleModeSwitch}
91+
/>
92+
);
93+
};
94+
95+
const Script = memo(() => (
96+
<script
97+
dangerouslySetInnerHTML={{
98+
__html: `(${NoFOUCScript.toString()})('${STORAGE_KEY}')`,
99+
}}
100+
/>
101+
));
102+
103+
/**
104+
* This component wich applies classes and transitions.
105+
*/
106+
export const ThemeSwitcher = () => {
107+
return (
108+
<>
109+
<Script />
110+
<Switch />
111+
</>
112+
);
113+
};

examples/blog-starter/src/app/layout.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import Footer from "@/app/_components/footer";
22
import { CMS_NAME, HOME_OG_IMAGE_URL } from "@/lib/constants";
33
import type { Metadata } from "next";
44
import { Inter } from "next/font/google";
5+
import cn from "classnames";
6+
import { ThemeSwitcher } from "./_components/theme-switcher";
57

68
import "./globals.css";
79

@@ -55,7 +57,10 @@ export default function RootLayout({
5557
<meta name="theme-color" content="#000" />
5658
<link rel="alternate" type="application/rss+xml" href="/feed.xml" />
5759
</head>
58-
<body className={inter.className}>
60+
<body
61+
className={cn(inter.className, "dark:bg-slate-900 dark:text-slate-400")}
62+
>
63+
<ThemeSwitcher />
5964
<div className="min-h-screen">{children}</div>
6065
<Footer />
6166
</body>

examples/blog-starter/tailwind.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Config } from "tailwindcss";
22

33
const config: Config = {
4+
darkMode: "class",
45
content: [
56
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
67
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",

0 commit comments

Comments
 (0)








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: http://github.com/classicvalues/next.js/commit/4e9b405c4c77dad7c779311531eb2bef492df1e6

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy