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.
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