Theme Provider
ThemeProvider is a React Context Provider that provides the current theme to the application and a function to change it.
To use the ThemeProvider
, wrap your application's entry point. This should be done as high in the component tree as possible.
You should also add the PreventWrongThemeFlash
component to the head of your application to prevent a Flash of Unstyled Content (FOUC) when the app first loads.
root.tsx
import { PreventWrongThemeFlash, ThemeProvider } from "@ngrok/mantle";
export default function App() {
return (
<html className="h-full" lang="en-US" dir="ltr">
<head>
// 👇 add this as high in the <head> as possible!
<PreventWrongThemeFlash />
<meta charSet="utf-8" />
<meta name="author" content="ngrok" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body className="h-full min-h-full overflow-y-scroll bg-body">
// 👇 wrap your app entry in the ThemeProvider
<ThemeProvider>
<Outlet />
</ThemeProvider>
</body>
</html>
);
}
Sometimes you cannot use the PreventWrongThemeFlash
component because your webserver is not able to render React components. In this case, you can use the copy the following script and add it to your application's <head>
:
index.html
<script>
(function() {
const themes = ["system","light","dark","light-high-contrast","dark-high-contrast"];
const isTheme = (value) => typeof value === "string" && themes.includes(value);
const fallbackTheme = "system" ?? "system";
const maybeStoredTheme = window.localStorage.getItem("mantle-ui-theme");
const hasStoredTheme = isTheme(maybeStoredTheme);
if (!hasStoredTheme) {
window.localStorage.setItem("mantle-ui-theme", fallbackTheme);
}
const themePreference = hasStoredTheme ? maybeStoredTheme : fallbackTheme;
const prefersDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches;
const prefersHighContrast = window.matchMedia("(prefers-contrast: more)").matches;
let initialTheme = themePreference;
if (initialTheme === "system") {
if (prefersHighContrast) {
initialTheme = prefersDarkMode ? "dark-high-contrast" : "light-high-contrast";
} else {
initialTheme = prefersDarkMode ? "dark" : "light";
}
}
const htmlElement = document.documentElement;
htmlElement.classList.remove(...themes);
htmlElement.classList.add(initialTheme);
htmlElement.dataset.appliedTheme = initialTheme;
htmlElement.dataset.theme = themePreference;
})();
</script>
Then, in your application, you can use the useTheme
hook to get and change the current theme:
app.tsx
import {
isTheme,
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
theme,
useTheme,
} from "@ngrok/mantle";
function App() {
const [currentTheme, setTheme] = useTheme();
return (
<>
<Select
value={currentTheme}
onValueChange={(value) => {
const maybeNewTheme = isTheme(value) ? value : undefined;
if (maybeNewTheme) {
setTheme(maybeNewTheme);
}
}}
>
<div className="ml-auto">
<span className="sr-only">Theme Switcher</span>
<SelectTrigger className="w-min">
<Sun className="mr-1 h-6 w-6" />
</SelectTrigger>
</div>
<SelectContent>
<SelectGroup>
<SelectLabel>Choose a theme</SelectLabel>
<SelectItem value={theme("system")}>System</SelectItem>
<SelectItem value={theme("light")}>Light</SelectItem>
<SelectItem value={theme("dark")}>Dark</SelectItem>
<SelectItem value={theme("light-high-contrast")}>Light High Contrast</SelectItem>
<SelectItem value={theme("dark-high-contrast")}>Dark High Contrast</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
{/* The rest of your app... */}
</>
);
}