Skip to content

From 'dark' to 'light' mode

Allowing users to switch between a ‘light’ and ‘dark’ mode on your website is something most people expect from a modern web application. It also helps improve accessibility for the site, allowing users to settle on a theme that suits them best.

The only problem is that implementing a light and dark mode is surprisingly difficult. Luckily, the Epic Stack has most of the code for this already included.

All we need to do is implement it 🚀.

Trigger ‘light’ and ‘dark’ mode with code

Section titled “Trigger ‘light’ and ‘dark’ mode with code”

Open app/root.tsx and look carefully at the App function being exported:

app/root.tsx
4 collapsed lines
import { type LinksFunction } from '@remix-run/node'
import Document from '~/components/shared-layout/Document.tsx'
import rootLinkElements from '~/utils/providers/rootLinkElements'
import { useNonce } from '~/utils/nonce-provider.ts'
export const links: LinksFunction = () => {
return rootLinkElements
}
export { headers, meta } from './__root.client.tsx'
export { action, loader } from './__root.server.tsx'
export default function App() {
const nonce = useNonce()
return (
<Document nonce={nonce}>
<div className="flex h-screen flex-col justify-between">
<div className="flex-1">
<main className="grid h-full place-items-center">
<h1 className="text-mega">Welcome to Epic News!</h1>
</main>
</div>
</div>
</Document>
)
}

Notice the Document component that wraps everything being returned from the App function?

app/root.tsx
import { type LinksFunction } from '@remix-run/node'
import Document from '~/components/shared-layout/Document.tsx'
import rootLinkElements from '~/utils/providers/rootLinkElements'
import { useNonce } from '~/utils/nonce-provider.ts'
5 collapsed lines
export const links: LinksFunction = () => {
return rootLinkElements
}
export { headers, meta } from './__root.client.tsx'
export { action, loader } from './__root.server.tsx'
export default function App() {
const nonce = useNonce()
return (
<Document nonce={nonce}>
<div className="flex h-screen flex-col justify-between">
<div className="flex-1">
<main className="grid h-full place-items-center">
<h1 className="text-mega">Welcome to Epic News!</h1>
</main>
</div>
</div>
</Document>
)
}

Right-click the opening Document tag, and select ‘Go to Source Definition’ from the option menu:

Selecting Go to source definition

This will open the file where the Document component code is held:

app/components/shared-layout/Document.tsx
interface DocumentProps {
children: React.ReactNode
nonce: string
theme?: Theme
env?: Record<string, string>
allowIndexing?: boolean
}
export default function Document({
children,
nonce,
theme = 'dark',
env = {},
allowIndexing = true,
}: DocumentProps) {
return (
<html lang="en" className={`${theme} h-full overflow-x-hidden`}>
<head>
<ClientHintCheck nonce={nonce} />
<Meta />
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
{allowIndexing ? null : (
<meta name="robots" content="noindex, nofollow" />
)}
<Links />
</head>
<body className="bg-background text-foreground">
{children}
<script
nonce={nonce}
dangerouslySetInnerHTML={{
__html: `window.ENV = ${JSON.stringify(env)}`,
}}
/>
<ScrollRestoration nonce={nonce} />
<Scripts nonce={nonce} />
</body>
</html>
)
}

Manually updating from ‘light’ to ‘dark’ themes

Section titled “Manually updating from ‘light’ to ‘dark’ themes”

Head back over to app/components/shared-layout/Document.tsx if you are not in it already.

Find the line that reads theme = 'dark', around line 16.

Change this to theme = 'light' and save the file:

app/components/shared-layout/Document.tsx
export default function Document({
children,
nonce,
theme = 'dark',
theme = 'light',
env = {},
}: DocumentProps)

You should see the website in the browser looks completely different:

Project in Light Mode

Why? Tailwind classes and string interpolation

Section titled “Why? Tailwind classes and string interpolation”

Take a closer look at the opening html component being returned from the Document function:

export default function Document({
children,
nonce,
theme = 'light',
env = {},
allowIndexing = true,
}: DocumentProps) {
return (
<html lang="en" className={`${theme} h-full overflow-x-hidden`}>
21 collapsed lines
<head>
<ClientHintCheck nonce={nonce} />
<Meta />
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
{allowIndexing ? null : (
<meta name="robots" content="noindex, nofollow" />
)}
<Links />
</head>
<body className="bg-background text-foreground">
{children}
<script
nonce={nonce}
dangerouslySetInnerHTML={{
__html: `window.ENV = ${JSON.stringify(env)}`,
}}
/>
<ScrollRestoration nonce={nonce} />
<Scripts nonce={nonce} />
</body>
</html>
)
}

Can you see how the value of the theme variable has been slotted into the className property (or ‘prop’)?

This is called interpolation, and is a way of inserting the value of a variable into a string in JavaScript.

In this tutorial, we’ve learned:

  • How to switch between ‘light’ and ‘dark’ themes in the Epic Stack.
  • How to manually update the theme from ‘dark’ to ‘light’ in the Document component.
  • How to use string interpolation to insert the value of a variable into a string.
  • How to use TypeScript interfaces to define the shape of an object.
  • How to use React props to pass data into a component.

What we need now is a way for users to control the value of theme dynamically from the front-end, so they can switch easily between both.

In our next tutorial, we add a button that users can click to toggle between the two states.