EnXan
EnXan

Reputation: 21

How can I set the ThemeSwitcher (Next-Themes) in NextJS 14 (Typescript) to system settings, but also change the mode onClick

I'm currently facing the issue that I want to create a theme switcher (with next-themes).Until know my code works so far that I have a switcher that switches between whitemode and darkmode.However, I would also like to take the system preference into account. That means if the user visites the website the first time, the switcher should be automatically enable the prefered theme. If the user then clicks the switcher by himself it should change the theme no matter the system preference.I would still describe myself as a beginner. So forgive me if I have forgotten to mention anything important. Thanks for your help! enter image description here enter image description here

ThemeToggler:

import { useState, useEffect } from "react";
import { useTheme } from "next-themes";
import { MoonIcon, SunIcon } from "@heroicons/react/16/solid";

const ThemeToggler = () => {
  const [mounted, setMounted] = useState(false);
  const [isActive, setIsActive] = useState(false);
  const { resolvedTheme, setTheme } = useTheme();

  const toggleTheme = () => {
    setTheme(resolvedTheme === "light" ? "dark" : "light");
    setIsActive(!isActive);
  };

  // useEffect only runs on the client, so now we can safely show the UI
  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) {
    return null;
  }

  return (
    <div
      onClick={toggleTheme}
      className={`
       relative w-16 h-8 flex items-center dark:bg-gray-900 bg-teal-500 cursor-pointer rounded-full p-1`}>
      <MoonIcon className="fill-white w-[15px] h-[15px]"></MoonIcon>
      <div
        id="toggleBtnTheme"
        className={` bg-white
        absolute  w-6 h-6 rounded-full shadow-customShadow-md ${
          isActive
            ? " transition-transform translate-x-0"
            : " transition-transform translate-x-8"
        }`}></div>
      <SunIcon className="fill-white ml-auto w-[15px] h-[15px]"></SunIcon>
    </div>
  );
};

export default ThemeToggler;

Theme Provider:

"use client";

import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { type ThemeProviderProps } from "next-themes/dist/types";

export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
  return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}

Header

"use client";
import React, { useEffect, useState, useCallback } from "react";
import { Bars3Icon } from "@heroicons/react/16/solid";
import Image from "next/image";
import Link from "next/link";
import DropdownMenu from "./LanguageDropdown";
import ThemeToggler from "./ThemeToggler";

// Typ für einen einzelnen Menüpunkt
interface MenuItem {
  href: string;
  label: string;
}

// Liste von Menüpunkten
const menuItems: MenuItem[] = [
  { href: "/", label: "Home" },
  { href: "/how-it-works", label: "How it Works" },
  { href: "/help", label: "Help" },
  { href: "/about-us", label: "About Us" },
];

// Header-Komponente
const Header: React.FC = () => {
  // Zustand für das Menü
  const [menuActive, setMenuActive] = useState(false);
  // Zustand für die Fensterbreite
  const [windowWidth, setWindowWidth] = useState(0);

  // Funktion zum Umschalten des Menüs
  const toggleNavbar = useCallback(() => {
    setMenuActive((prevMenuActive) => !prevMenuActive);
  }, []);

  useEffect(() => {
    const handleResize = () => {
      setWindowWidth(window.innerWidth);
    };

    // Füge den Event Listener nur im Client hinzu
    if (typeof window !== "undefined") {
      setWindowWidth(window.innerWidth);
      window.addEventListener("resize", handleResize);
      return () => {
        window.removeEventListener("resize", handleResize);
      };
    }
  }, []);

  useEffect(() => {
    if (windowWidth > 1200) {
      setMenuActive(false);
    }
  }, [windowWidth]);

  return (
    // Header-Element
    <header className="  absolute left-0 right-0 ml-auto mr-auto w-screen z-10">
      {/* Hintergrund für das aufklappbare Menü */}
      <div
        className={`-z-50 absolute h-screen w-screen bg-bg-color-dark/20 transition duration-300  ${
          menuActive ? "flex" : "hidden"
        }`}></div>
      <div className="flex justify-center ">
        <div className=" bg-nav-color-dark w-screen shadow-customShadow-md laptop:w-[80vw] text-white inline-flex justify-between py-1 mobile:py-3 px-5 laptop:rounded-full laptop:my-4  laptop:inline-flex">
          {/* Menüschalter */}
          <div className="flex gap-3 laptop:hidden max-mobile:pr[10%] z-10">
            <button
              id="nav-menu"
              className={`laptop:hover:text-custom-purple transition duration-200 ${
                menuActive ? "text-custom-purple" : ""
              }`}
              onClick={toggleNavbar}>
              <Bars3Icon className="w-[30px]" />
            </button>
          </div>
          <div className=" flex  z-10">
            {/* Logo mit Link zur Startseite */}
            <Link
              href="/"
              className="min-w-[50px] mobile:w-[100px] w-[100px] flex">
              <Image
                src="/static/images/logo_symbol_writing_weiß.svg"
                alt="UTP-Logo"
                width={100}
                height={100}
                className=""
              />
            </Link>
          </div>
          {/* Hauptmenü */}
          <div className="laptop:inline-flex bg-gradient-to-b from-nav-color-dark/70 to-nav-color-dark/20 border-[#ffff]/10 rounded-b-xl min-h-0 transition-[height] duration-300 laptop:bg-none laptop:h-[auto] laptop:backdrop-blur-none z-50 laptop:shadow-none hidden">
            <nav className="my-auto">
              <ul className="text-center laptop:flex gap-10 text-white laptop:w-auto">
                {/* Menüpunkte */}
                {menuItems.map((menuItem, index) => (
                  <MenuItem key={index} {...menuItem} />
                ))}
                {/* Dropdown-Menü */}
                <li className="relative z-50 py-3 laptop:py-0 hover:bg-custom-purple transition ease-in-out duration-150 laptop:hover:bg-transparent">
                  <DropdownMenu />
                </li>
              </ul>
            </nav>
          </div>
          {/* Zusätzliche Optionen wie Anmeldung und Themenschalter */}
          <div className=" z-40 flex items-center gap-3 tablet:inline-flex">
            <li className="py-2 list-none">
              <button className="bg-custom-purple transition ease-in-out duration-150 hover:bg-[#5F1EB4] px-2 py-2  mobile:px-3 mobile:py-2 rounded-xl text-xs mobile:text-base">
                <Link href="/auth/login">Login</Link>
              </button>
            </li>
            <div className="max-mobile:hidden">
              <ThemeToggler />
            </div>
          </div>
        </div>
      </div>

      {/* Aufklappbares Menü */}
      <div
        className={`absolute w-screen bg-gradient-to-b backdrop-blur-lg from-nav-color-dark/70 to-nav-color-dark/20 border-[#ffff]/10 rounded-b-xl min-h-0 transition-[height] duration-300 laptop:bg-none laptop:h-[auto] laptop:backdrop-blur-none z-50 laptop:shadow-none laptop:hidden ${
          menuActive
            ? "h-[240px] shadow-xl overflow-clip"
            : "h-[0px] shadow-none overflow-clip"
        }`}
        style={{ maxHeight: menuActive ? "block" : "hidden" }}>
        <nav className="my-auto">
          <ul className="text-center laptop:flex gap-4 text-white laptop:w-auto">
            {/* Dropdown-Menü */}
            <li className=" py-3 laptop:py-0 hover:bg-custom-purple transition ease-in-out duration-150 laptop:hover:bg-transparent">
              <DropdownMenu />
            </li>
            {/* Menüpunkte */}
            {menuItems.map((menuItem, index) => (
              <MenuItem key={index} {...menuItem} />
            ))}
          </ul>
        </nav>
      </div>
    </header>
  );
};

// Einzelner Menüpunkt
const MenuItem: React.FC<MenuItem> = ({ href, label }) => {
  return (
    <li className="py-3 laptop:py-0 hover:bg-custom-purple transition ease-in-out duration-150 laptop:hover:bg-transparent laptop:text-lg laptop:hover:text-custom-purple">
      <Link href={href}>{label}</Link>
    </li>
  );
};

export default Header;

globals.css

@tailwind base;
@tailwind components;
@tailwind utilities;


:root {
  --foreground-rgb: 0, 0, 0;
  --background-rgb: 255, 255, 255;
}

@media (prefers-color-scheme: dark) {
  :root {
    --foreground-rgb: 255, 255, 255;
    --background-rgb: 8, 8, 8;
  }
}

body {
  color: rgb(var(--foreground-rgb));
  background: rgb(var(--background-rgb));
}

@layer utilities {
  .text-balance {
    text-wrap: balance;
  }
}

Layout.tsx

import type { Metadata } from "next";
import { Roboto } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "./components/theme-provider";
import Header from "./components/Header";

const roboto = Roboto({
  weight: ["400", "700"],
  style: ["normal", "italic"],
  subsets: ["latin"],
  display: "swap",
});

export const metadata: Metadata = {
  title: "Unlock the Power",
  description: "The number one PC-Configurator",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body className={`${roboto.className}  `}>
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange>
          <Header />
          {children}
          {/* <Footer /> */}
        </ThemeProvider>
      </body>
    </html>
  );
}

Switch: Dakmode, system: light enter image description here Switch: Darkmode, system: dark enter image description here Switch: Lightmode, system: dark enter image description here

Upvotes: 1

Views: 5141

Answers (1)

Youssouf Oumar
Youssouf Oumar

Reputation: 46191

You could do what you are looking for by simply removing the isActive state from ThemeToggler, and use resolvedTheme to set the ste of the button, like so:

import { useState, useEffect } from "react";
import { useTheme } from "next-themes";
import { MoonIcon, SunIcon } from "@heroicons/react/16/solid";

const ThemeToggler = () => {
  const [mounted, setMounted] = useState(false);
  const { resolvedTheme, setTheme } = useTheme();

  const toggleTheme = () => {
    setTheme(resolvedTheme === "light" ? "dark" : "light");
  };

  // useEffect only runs on the client, so now we can safely show the UI
  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) {
    return null;
  }

  return (
    <div
      onClick={toggleTheme}
      className={`
       relative w-16 h-8 flex items-center  cursor-pointer rounded-full p-1 ${
         resolvedTheme === "dark" ? "bg-teal-500" : "bg-gray-900"
       }`}
    >
      <MoonIcon className="fill-white w-[15px] h-[15px]"></MoonIcon>
      <div
        id="toggleBtnTheme"
        className={` bg-white
        absolute  w-6 h-6 rounded-full shadow-customShadow-md ${
          resolvedTheme === "dark"
            ? " transition-transform translate-x-0"
            : " transition-transform translate-x-8"
        }`}
      ></div>
      <SunIcon className="fill-white ml-auto w-[15px] h-[15px]"></SunIcon>
    </div>
  );
};

export default ThemeToggler;

Also, you can update your CSS as well, since Next.js Theme set the data type in all scenarios:

@tailwind base;
@tailwind components;
@tailwind utilities;

[data-theme="light"] {
  --foreground-rgb: 0, 0, 0;
  --background-rgb: 255, 255, 255;
}

[data-theme="dark"] {
  --foreground-rgb: 255, 255, 255;
  --background-rgb: 8, 8, 8;
}

body {
  color: rgb(var(--foreground-rgb));
  background: rgb(var(--background-rgb));
}

@layer utilities {
  .text-balance {
    text-wrap: balance;
  }
}

Upvotes: 1

Related Questions