skip to Main Content

I’m developing my first simple Next.js+Tailwind app. It features a fixed header and a sidebar. The sidebar contains a button to toggle between dark and light mode. I’m using next-theme and with Tailwind this was rather straightforward so far.

Now, one of the main content pages contains a directed graph. I currently use Cytoscape for this, which is overall a great library. My main problem right now is that Cytoscape does not support Tailwind-like styling. Creating a Cytoscape graph takes an input as follows:

const stylesheet = [
    {
    selector: 'node',
    style: {
        'backgroundColor': "#33FF66", // this color should change in dark mode
    },
    {
    selector: "edge",
    style: {
        'curve-style': 'bezier',
        'line-color': '#333333', // this color should change in dark mode
    },
    },
  ];

In other words, I would like my graph colors to change when toggling dark mode. My current consideration is to have to style sheets (e.g., stylesheetLight and stylesheetDark) and then switch between both sheets when the toggle button is pressed. However, I have no idea how I can listen to the toggle button residing in the sidebar in the page containing the graph. My RootLayout looks as follows:

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  const [sidebarOpen, setSidebarOpen] = useState(false);
  return (
    <html lang="en">
    <body className="${inter.className} bg-slate-50 dark:bg-slate-800">
        <div className="grid min-h-screen grid-rows-header">
        <div>
            <Header onMenuButtonClick={() => setSidebarOpen((prev) => !prev)} />
        </div>
        <div className="flex min-h-screen">
            <div className="">
            <Sidebar open={sidebarOpen} setOpen={setSidebarOpen} />
            </div>
            <div className="mt-[50px] flex-1">
                <main>{children}</main>
            </div>
        </div>
        </div>
    </body>
    </html>
  );
}

How can I accomplish this? Do I have access to the toggle button in the page with the graph to add event listener?

Or is this even the correct approach anyway?

UPDATE 1: Following the answers of @ayex @Ganesh, I’ve modified my RootLayout as follows

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  const [sidebarOpen, setSidebarOpen] = useState(false);
  return (
    <html lang="en">
      <body className="${inter.className} bg-slate-50 dark:bg-slate-800">
      <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
          <div className="grid min-h-screen grid-rows-header">
          <div>
            <Header onMenuButtonClick={() => setSidebarOpen((prev) => !prev)} />
          </div>
          <div className="flex min-h-screen">
            <div className="">
              <Sidebar open={sidebarOpen} setOpen={setSidebarOpen} />
            </div>
              <div className="mt-[50px] flex-1">
                <main>{children}</main>
              </div>
          </div>
        </div>
        </ThemeProvider>
      </body>
    </html>
  );
}

The change is that I moved the ThemeProvider from the sidebar component out into the RootLayout. This seems to help since I can now "see" the change of the theme in my graph page, which in its core looks as follows:

export default function GraphPage() {
  const { theme, setTheme } = useTheme()
  const [topicGraph, setTopicGraph] = useState<any[] | null>(null);
  const [stylesheet, setStylesheet] = useState<any | null>(null)

  // GOOD: Prints every time I toggle the dark mode
  console.log("Theme:", theme)
  
  // BAD: this causes "Too Many Re-renders" error
  //if (theme === "light") {
  //  setStylesheet(stylesheetLight);
  //} else {
  //  setStylesheet(stylesheetDark);
  //}

  useEffect(() => {
    getGraph().then((jsonResponse) => {

      // THIS WORKS!
      if (theme === "light") {
        setStylesheet(stylesheetLight);
      } else {
        setStylesheet(stylesheetDark);
      }

      //...Call API to fetch graph data...
      setTopicGraph(graph);
    });
  }, [theme]);

  return (
    <div>
      <CytoscapeComponent
        elements={topicGraph}
        style={{ width: 'w-full', height: 'calc(100vh - 50px)' }}
        stylesheet={stylesheet}
        layout={defaultLayout}
        wheelSensitivity={0.15}
        cy={(cy) => {
          setListeners(cy);
        }}
      />      
    </div>
  )
}

As commented in the code snippet above, the console.log statement now always prints the current mode (dark or light). However, if I now use setStylesheet there, I get a "too many re-renders" error. How can I avoid this?

UPDATE 2: I got it to work by moving setStylesheet into the useEffect block — I edit the code snippet above. It’s important that theme is a dependency.

3

Answers


  1. if you are using next-themes then it is simple, the useTheme hook gives you the state of the theme and depending on that you can manage the style applied on the graph like you mentioned. bye the way, i do not think you have used next-themes yet because as i can see it on your Root Layout it is not configured. here is how you might use it, customize on your way.

    RootLayout.tsx

    export default function RootLayout({
      children,
    }: Readonly<{
      children: React.ReactNode;
    }>) {
      const [sidebarOpen, setSidebarOpen] = useState(false);
      return (
        <html lang="en">
        <body className="${inter.className} bg-slate-50 dark:bg-slate-800">
    <ThemeContextProvider>
            <div className="grid min-h-screen grid-rows-header">
            <div>
                <Header onMenuButtonClick={() => setSidebarOpen((prev) => !prev)} />
            </div>
            <div className="flex min-h-screen">
                <div className="">
                <Sidebar open={sidebarOpen} setOpen={setSidebarOpen} />
                </div>
                <div className="mt-[50px] flex-1">
                    <main>{children}</main>
                </div>
            </div>
            </div>
    </ThemeContextProvider>
        </body>
        </html>
      );
    }
    

    context/theme-context.tsx

    "use client";
    
    import { ThemeProvider } from "next-themes";
    
    type ThemeContextProviderProps = {
      children: React.ReactNode;
    };
    
    export default function ThemeContextProvider({
      children,
    }: ThemeContextProviderProps) {
      return <ThemeProvider attribute="class">{children}</ThemeProvider>;
    }
    
    

    Sidebar.tsx

    "use client";
    import { useTheme } from "next-themes";
    import React from "react";
    import { BsMoon, BsSun } from "react-icons/bs";
    
    export default function Sidebar() {
      const { theme, setTheme } = useTheme();
      
      const toggleTheme = () => {
        if (theme === "light") {
          setTheme("dark");
        } else {
          setTheme("light");
        }
      };
    
      return (
        <button
          className="fixed bottom-5 right-5 bg-white w-[3rem] h-[3rem]
        bg-opacity-80 backdrop-blur-lg border border-white 
        border-opacity-40 shadow-2xl rounded-full flex 
        justify-center items-center hover:scale-105 active:scale-105 
        hover:backdrop-blur-none hover:bg-opacity-100 transition-all dark:bg-gray-950/80
        dark:backdrop-blur-lg dark:hover:bg-gray-950 dark:border-dark/40"
          onClick={toggleTheme}
        >
          {theme === "light" ? <BsSun /> : <BsMoon />}
        </button>
      );
    }
    
    

    so on your Sidebar.tsx you can do something like theme==='light'?stylesheetLight:stylesheetDark
    you can read more about it here
    and if you use shadcn-ui: which is a tailwindcss based component library,they have there own docs on how to set a dark mode and use it with next-themes here

    Login or Signup to reply.
  2. context/theme-context.tsx
    
    return <ThemeProvider attribute="class">{children}</ThemeProvider>;
    
    
    replace above line by below line
    
    return <ThemeProvider attribute="class" defaultTheme="dark">{children}</ThemeProvider>; 
    
    you forgot to add defaultTheme="dark" that's why your code is not working...
    
    Login or Signup to reply.
  3. @christian show me the code where you call the Graph component

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search