Theme Switcher

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 MantleThemeHeadContent component to the head of your application to prevent a Flash of Unstyled Content (FOUC) when the app first loads as well as preload all of our custom fonts.

root.tsx

import { MantleThemeHeadContent, ThemeProvider } from "@ngrok/mantle/theme-provider";

export default function App() {
	return (
		<html className="h-full" lang="en-US" dir="ltr">
			<head>
				// 👇 add this as high in the <head> as possible!
				<MantleThemeHeadContent />
				<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 MantleThemeHeadContent 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";
	let maybeStoredTheme = null;
	try {
		maybeStoredTheme = "localStorage" in window ? window.localStorage.getItem("mantle-ui-theme") : null;
	} catch (_) {}
	const hasStoredTheme = isTheme(maybeStoredTheme);
	if (!hasStoredTheme && "localStorage" in window) {
		try {
			window.localStorage.setItem("mantle-ui-theme", fallbackTheme);
		} catch (_) {}
	}
	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>

You will also need to ensure that you add the PreloadFonts component to your app as well.

index.html

<head>
	<link rel="preconnect" href="https://assets.ngrok.com"/>
	<link rel="preload" href="https://assets.ngrok.com/fonts/euclid-square/EuclidSquare-Regular-WebS.woff" as="font" type="font/woff" crossorigin="anonymous"/>
	<link rel="preload" href="https://assets.ngrok.com/fonts/euclid-square/EuclidSquare-RegularItalic-WebS.woff" as="font" type="font/woff" crossorigin="anonymous"/>
	<link rel="preload" href="https://assets.ngrok.com/fonts/euclid-square/EuclidSquare-Medium-WebS.woff" as="font" type="font/woff" crossorigin="anonymous"/>
	<link rel="preload" href="https://assets.ngrok.com/fonts/euclid-square/EuclidSquare-Semibold-WebS.woff" as="font" type="font/woff" crossorigin="anonymous"/>
	<link rel="preload" href="https://assets.ngrok.com/fonts/euclid-square/EuclidSquare-MediumItalic-WebS.woff" as="font" type="font/woff" crossorigin="anonymous"/>
	<link rel="preload" href="https://assets.ngrok.com/fonts/ibm-plex-mono/IBMPlexMono-Text.woff" as="font" type="font/woff" crossorigin="anonymous"/>
	<link rel="preload" href="https://assets.ngrok.com/fonts/ibm-plex-mono/IBMPlexMono-TextItalic.woff" as="font" type="font/woff" crossorigin="anonymous"/>
	<link rel="preload" href="https://assets.ngrok.com/fonts/ibm-plex-mono/IBMPlexMono-SemiBold.woff" as="font" type="font/woff" crossorigin="anonymous"/>
	<link rel="preload" href="https://assets.ngrok.com/fonts/ibm-plex-mono/IBMPlexMono-SemiBoldItalic.woff" as="font" type="font/woff" crossorigin="anonymous"/>
</head>

Then, in your application, you can use the useTheme hook to get and change the current theme:

app.tsx

import {
	Select,
	SelectContent,
	SelectGroup,
	SelectItem,
	SelectLabel,
	SelectTrigger,
} from "@ngrok/mantle/select";
import { isTheme, theme, useTheme } from "@ngrok/mantle/theme-provider";

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... */}
		</>
	);
}

API Reference

ThemeProvider

The ThemeProvider accepts the following props in addition to the PropsWithChildren.

PropTypeDefaultDescription
children
ReactNode

The React components to be wrapped by the theme provider context.

defaultTheme
  • system
  • light
  • dark
  • light-high-contrast
  • dark-high-contrast
system

The default theme to use when no theme is stored in localStorage. The system theme will automatically resolve to the user's preferred color scheme.

storageKey
stringmantle-ui-theme

The key used to store the theme preference in localStorage.

MantleThemeHeadContent

The MantleThemeHeadContent component prevents Flash of Unstyled Content (FOUC) and preloads fonts. It accepts the following props:

PropTypeDefaultDescription
defaultTheme
  • system
  • light
  • dark
  • light-high-contrast
  • dark-high-contrast
system

The default theme to use in the FOUC prevention script. Should match the defaultTheme prop of your ThemeProvider.

storageKey
stringmantle-ui-theme

The localStorage key to check for theme preference. Should match the storageKey prop of your ThemeProvider.

includeNunitoSans
booleanfalse

Whether to include preload links for the Nunito Sans font family.