Dark Mode
There are a few different type of light/dark themes that can be available for a web application:
- Light Only
- Dark Only
- System Preference
- Light/Dark Mode Toggle
- Light/Dark/System Toggle
This page will guide you through all the approaches except for the "Light Only" theme since that is the default behavior. Check out the theme customization documentation for more theme behavior.
If the application should only use a dark theme, set the core.$color-scheme
variable to dark
when importing the react-md
styles which will generate all
styles with the dark theme variants.
@use "@react-md/core" with (
$color-scheme: dark
);
@include core.styles;
Enabling the dark mode will change all the theme colors to their dark-mode variants and exclude any light theme behavior from the generated CSS. The default theme colors are:
Name | Color |
---|---|
$dark-theme-background-color | #121212 |
$dark-theme-surface-color | #424242 |
$dark-theme-text-primary-color | rgba(255, 255, 255, 0.87) |
$dark-theme-text-secondary-color | rgba(255, 255, 255, 0.6) |
$dark-theme-text-hint-color | rgba(255, 255, 255, 0.38) |
$dark-theme-text-disabled-color | rgba(255, 255, 255, 0.38) |
$dark-surface-hover-background-color | rgba(255, 255, 255, 0.1) |
$dark-surface-focus-background-color | rgba(255, 255, 255, 0.12) |
$dark-surface-press-background-color | rgba(255, 255, 255, 0.24) |
$dark-surface-selected-background-color | rgba(255, 255, 255, 0.12) |
$dark-surface-ripple-background-color | rgba(255, 255, 255, 0.12) |
$icon-dark-theme-color | #B3B3B3 |
Dark Elevation Colors
When the dark mode is enabled, the surface background color will become lighter
as the z-index
/box-shadow
increases to enable more contrast between
temporary elements. Here is a list of components in react-md
and their elevation:
Name | Elevation |
---|---|
AppBar | 2 |
Card | 2 |
Sheet (inline) | 2 |
StickyTableHeader | 4 |
Chip | 8 |
Toast | 6 |
Menu | 8 |
Dialog | 16 |
Sheet (normal) | 16 |
Configuring Elevation Colors
The different elevation colors can be changed by modifying the core.$dark-elevation-colors map. The next demo shows all 25 elevation colors and allows the values to be modified to see how they behave.
System Mode
If the application should use the dark theme only if the user has set their
system preference to dark, set the core.$color-scheme to system
. The
generated styles will default to the light theme but add a media query to use
the dark theme when the @media (prefers-color-scheme: dark)
matches.
@use "@react-md/core" with (
$color-scheme: system
);
@include core.styles;
Light or Dark Mode
If the application allows the user to select the current color scheme, generate the styles as normal with the default color scheme and create a global class name with the alternative theme using the core.use-light-theme or core.use-dark-theme mixins.
Once the styles are generated, the app should be wrapped in the
LocalStorageColorSchemeProvider
or a custom
Cookie Storage Provider
and apply the light or dark theme class name to the root html as needed. The
following examples will use the LocalStorageColorSchemeProvider
to keep it simple.
Start by configuring the default core.$color-scheme and creating a class
name for the other color scheme. This example will default to a light
theme
and allow the user to configure it to be dark
.
@use "@react-md/core" with (
// If the $color-scheme is not set or set to `light`, the dark elevation
// styles are omitted by default to keep the bundle smaller. So when enabling
// a toggleable dark mode, force the styles to be created:
$disable-dark-elevation: false
);
@include core.styles;
.dark-theme {
@include core.dark-theme;
}
// if you want to default with a dark theme instead:
@use "@react-md/core" with (
$color-scheme: dark
);
@include core.styles;
.light-theme {
@include core.light-theme;
}
Wrap the app in the chosen ColorSchemeProvider
implementation:
import { LocalStorageColorSchemeProvider } from "@react-md/core/theme/LocalStorageColorSchemeProvider";
function App() {
return (
<LocalStorageColorSchemeProvider>
<RestOfTheApp />
<ApplyTheme />
</LocalStorageColorSchemeProvider>
);
}
Change the styles based on the color scheme:
import { useColorScheme } from "@react-md/core/theme/useColorScheme";
import { useHtmlClassName } from "@react-md/core/useHtmlClassName";
function ApplyTheme() {
const { currentColor, setColorScheme } = useColorScheme();
useHtmlClassName(currentColor === "dark" ? "dark-theme" : "");
// Whatever UI is desired for this
return (
<Button
onClick={() =>
setColorScheme((prev) => (prev === "light" ? "dark" : "light"))
}
>
Theme
</Button>
);
}
Light or Dark or System Mode
The style setup will be about the same as the previous examples. Start by defining the default core.$color-scheme and create additional classes for the other color schemes.
@use "@react-md/core" with (
$color-scheme: light
);
@include core.styles;
.dark-theme {
@include core.dark-theme;
}
@media (prefers-color-scheme: dark) {
.system-theme {
@include core.dark-theme;
}
}
Then apply the dark-theme
or system-theme
class name when needed:
import { useColorScheme } from "@react-md/core/theme/useColorScheme";
import { useHtmlClassName } from "@react-md/core/useHtmlClassName";
import { cnb } from "cnbuilder";
function ApplyTheme() {
const { colorScheme } = useColorScheme();
useHtmlClassName(cnb(colorScheme !== "light" && `${colorScheme}-theme`));
return null;
}
This Website's Implementation
If a real-world example is useful, here's this website's implementation with next.js:
"use client";
import { type ColorScheme } from "@react-md/core/theme/types";
import { ColorSchemeProvider } from "@react-md/core/theme/useColorScheme";
import { useColorSchemeProvider } from "@react-md/core/theme/useColorSchemeProvider";
import { type UseStateSetter } from "@react-md/core/types";
import {
type ReactElement,
type ReactNode,
useCallback,
useState,
} from "react";
import { COLOR_SCHEME_KEY } from "@/constants/cookies.js";
import { setCookie } from "@/utils/clientCookies.js";
export interface CookieColorSchemeProviderProps {
children: ReactNode;
defaultColorScheme: ColorScheme;
}
export function CookieColorSchemeProvider(
props: CookieColorSchemeProviderProps
): ReactElement {
const { children, defaultColorScheme } = props;
const [colorScheme, setColorScheme] = useState(defaultColorScheme);
const value = useColorSchemeProvider({
colorScheme,
setColorScheme: useCallback<UseStateSetter<ColorScheme>>((nextOrFn) => {
setColorScheme((prev) => {
const next = typeof nextOrFn === "function" ? nextOrFn(prev) : nextOrFn;
setCookie(COLOR_SCHEME_KEY, next);
return next;
});
}, []),
});
return <ColorSchemeProvider value={value}>{children}</ColorSchemeProvider>;
}
"use client";
import Cookies from "js-cookie";
export function setCookie(name: string, value: string): void {
const today = new Date();
const nextYear = today.getFullYear() + 1;
Cookies.set(name, value, {
secure: true,
expires: new Date(today.setFullYear(nextYear)),
// since Vercel is running on a different domain, this must be "none" instead
// of strict to access it from the server
sameSite: "none",
});
}
export function removeCookie(name: string): void {
Cookies.remove(name, {
secure: true,
sameSite: "none",
});
}
import { CoreProviders } from "@react-md/core/CoreProviders";
import { RootHtml } from "@react-md/core/RootHtml";
import { MenuConfigurationProvider } from "@react-md/core/menu/MenuConfigurationProvider";
import { NullSuspense } from "@react-md/core/suspense/NullSuspense";
import { isColorScheme } from "@react-md/core/theme/isColorScheme";
import { cnb } from "cnbuilder";
import { Roboto_Flex } from "next/font/google";
import { cookies } from "next/headers.js";
import { type ReactElement, type ReactNode } from "react";
import { CookieColorSchemeProvider } from "@/components/CookieColorSchemeProvider.jsx";
import { LoadThemeStyles } from "@/components/LoadThemeStyles.jsx";
import { COLOR_SCHEME_KEY } from "@/constants/cookies.js";
import { rmdConfig } from "@/constants/rmdConfig.jsx";
import "./layout.scss";
export { metadata } from "@/constants/metadata.js";
const roboto = Roboto_Flex({
subsets: ["latin"],
display: "swap",
variable: "--roboto",
});
export interface RootLayoutProps {
children: ReactNode;
}
export function RootLayout({ children }: RootLayoutProps): ReactElement {
const colorSchemeCookie = cookies().get(COLOR_SCHEME_KEY)?.value;
const defaultColorScheme = isColorScheme(colorSchemeCookie)
? colorScheme
: "system";
return (
<RootHtml className={cnb(roboto.variable, `${defaultColorScheme}-theme`)}>
<CoreProviders {...rmdConfig}>
<MenuConfigurationProvider renderAsSheet="phone">
<CookieColorSchemeProvider defaultColorScheme={defaultColorScheme}>
<NullSuspense>
<LoadThemeStyles />
</NullSuspense>
<MainLayout>{children}</MainLayout>
</CookieColorSchemeProvider>
</MenuConfigurationProvider>
</CoreProviders>
</RootHtml>
);
}
"use client";
import { useColorScheme } from "@react-md/core/theme/useColorScheme";
import { useHtmlClassName } from "@react-md/core/useHtmlClassName";
export function LoadThemeStyles(): null {
const { colorScheme } = useColorScheme();
useHtmlClassName(`${colorScheme}-theme`);
return null;
}