Theme Switcher

Mantle

Mantle is ngrok’s UI library and design system that powers its front-end.

Overview

Mantle is a carefully designed system of React components and utilities that establishes a unified design language and consistent user experience across ngrok’s web applications. Built with flexibility, extensibility, and developer ergonomics in mind, Mantle prioritizes accessibility, performance, and long-term maintainability. Developed in TypeScript, it offers strong typing, rich IDE support, and increased confidence through compile-time safety. Mantle empowers ngrok’s developers to craft interfaces that align seamlessly with the company’s brand, design principles, and engineering standards. By progressively enhancing standard DOM elements, it not only improves usability and accessibility, but also fills functional gaps—providing a robust foundation for building cohesive, modern UIs throughout the platform.

All of Mantle’s components are styled using Tailwind. and we compose around the following unstyled primitive component libraries:

Mantle uses Phosphor Icons as the primary icon library, providing a versatile and consistent set of icons. In addition, custom-designed icons tailored to ngrok’s needs are available through the @ngrok/mantle/icons module.

Status

Mantle is a work in progress that’s currently adding components. It intends to replace new and existing ngrok user interfaces.

Mantle is available in its alpha state on NPM. It is open source and available on GitHub.

Setup

Installation

Start by installing @ngrok/mantle and all of the required peerDependencies:

Mantle supports react and react-dom versions 18 and 19.

mantle and dependencies installation

pnpm add -E @ngrok/mantle @phosphor-icons/react date-fns

You will also need to install the following devDependencies:

Mantle only supports tailwindcss version 3 at this time. We are in the process of upgrading to version 4.

mantle devDependencies installation

pnpm add -DE tailwindcss@3.4.1 postcss autoprefixer

Tailwind Configuration

Then, add the tailwind preset and mantle content to your tailwind configuration:

tailwind.config.ts
import { createRequire } from "node:module";
import { mantlePreset, resolveMantleContentGlob } from "@ngrok/mantle/tailwind-preset";
import type { Config } from "tailwindcss";

const require = createRequire(import.meta.url);

export default {
	presets: [mantlePreset],
	content: [resolveMantleContentGlob(require), "./app/**/*.tsx"], // 👈 don't forget to swap out app content glob here!
	// ... the rest of your tailwind config!
} satisfies Config;

Application Scaffolding

I want to use mantle in my application…

In your react-router app’s src/root.tsx file, import the mantle.css file to apply the mantle styles.

We will also add the Theme Provider, Toaster, and Tooltip Provider to your app to enable theme selection, toasts, and tooltips.

It is critical to include the MantleThemeHeadContent in the head of your app to prevent a flash of unstyled content (FOUC). This component will inject the necessary script to prevent the FOUC.

app/root.tsx

import {
	MantleThemeHeadContent,
	ThemeProvider,
	useInitialHtmlThemeProps,
} from "@ngrok/mantle/theme-provider";
import { Toaster } from "@ngrok/mantle/toast";
import { TooltipProvider } from "@ngrok/mantle/tooltip";
import {
	isRouteErrorResponse,
	Links,
	Meta,
	Outlet,
	Scripts,
	ScrollRestoration,
} from "react-router";

import type { Route } from "./+types/root";
import "@ngrok/mantle/mantle.css"; // 👈 add this import to include mantle styles!

export function Layout({ children }: { children: React.ReactNode }) {
	const initialHtmlThemeProps = useInitialHtmlThemeProps({
		className: "h-full",
	});

	return (
		<html {...initialHtmlThemeProps} lang="en-US" dir="ltr">
			<head>
				<meta charSet="utf-8" />
				{/* 👇 The MantleThemeHeadContent should be rendered at the top of your <head>
						to prevent a flash of unstyled content (FOUC)! */}
				<MantleThemeHeadContent />
				<meta name="viewport" content="width=device-width, initial-scale=1" />
				<Meta />
				<Links />
			</head>
			<body>
				<ThemeProvider>
					<TooltipProvider>
						<Toaster />
						{children}
					</TooltipProvider>
				</ThemeProvider>
				<ScrollRestoration />
				<Scripts />
			</body>
		</html>
	);
}

export default function App() {
	return <Outlet />;
}

export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
	let message = "Oops!";
	let details = "An unexpected error occurred.";
	let stack: string | undefined;

	if (isRouteErrorResponse(error)) {
		message = error.status === 404 ? "404" : "Error";
		details =
			error.status === 404
				? "The requested page could not be found."
				: error.statusText || details;
	} else if (import.meta.env.DEV && error && error instanceof Error) {
		details = error.message;
		stack = error.stack;
	}

	return (
		<main className="pt-16 p-4 container mx-auto">
			<h1>{message}</h1>
			<p>{details}</p>
			{stack && (
				<pre className="w-full p-4 overflow-x-auto">
					<code>{stack}</code>
				</pre>
			)}
		</main>
	);
}

You are now ready to use mantle components in your application! For example, you can use the Button!