Implemented theme switcher as an exercise

This commit is contained in:
2026-01-23 16:38:04 +01:00
parent 497741fe1b
commit d07ec05ecc
5 changed files with 74 additions and 20 deletions

View File

@@ -1,5 +1,7 @@
@import "tailwindcss"; @import "tailwindcss";
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
:root { :root {
--background: #ffffff; --background: #ffffff;
--foreground: #171717; --foreground: #171717;
@@ -23,4 +25,5 @@ body {
background: var(--background); background: var(--background);
color: var(--foreground); color: var(--foreground);
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
transition: background-color 0.3s ease, color 0.3s ease;
} }

View File

@@ -1,5 +1,7 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { ThemeProvider } from "next-themes";
import { Geist, Geist_Mono } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
import { ThemeToggle } from "../components/ThemeToggle";
import "./globals.css"; import "./globals.css";
const geistSans = Geist({ const geistSans = Geist({
@@ -23,11 +25,14 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html lang="en"> <html lang="en" suppressHydrationWarning>
<body <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
className={`${geistSans.variable} ${geistMono.variable} antialiased`} <ThemeProvider defaultTheme="system" enableSystem>
> <nav className="bg-gray-100 dark:bg-gray-800 p-4">
<ThemeToggle />
</nav>
{children} {children}
</ThemeProvider>
</body> </body>
</html> </html>
); );

View File

@@ -0,0 +1,34 @@
"use client";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setTimeout(() => setMounted(true), 10);
}, []);
if (!mounted) {
return null;
}
return (
<div className="flex gap-4 justify-end">
<button
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
className="px-4 py-2 rounded-lg bg-gray-200 dark:bg-gray-800 text-gray-900 dark:text-gray-100"
>
{theme === "dark" ? "🌙" : "☀️"}
</button>
<button
onClick={() => setTheme("system")}
className="px-4 py-2 rounded-lg bg-gray-200 dark:bg-gray-800 text-gray-900 dark:text-gray-100"
>
🖥
</button>
</div>
);
}

11
package-lock.json generated
View File

@@ -9,6 +9,7 @@
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"next": "16.1.4", "next": "16.1.4",
"next-themes": "^0.4.6",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3" "react-dom": "19.2.3"
}, },
@@ -5002,6 +5003,16 @@
} }
} }
}, },
"node_modules/next-themes": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
"integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/next/node_modules/postcss": { "node_modules/next/node_modules/postcss": {
"version": "8.4.31", "version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",

View File

@@ -10,6 +10,7 @@
}, },
"dependencies": { "dependencies": {
"next": "16.1.4", "next": "16.1.4",
"next-themes": "^0.4.6",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3" "react-dom": "19.2.3"
}, },