How to implement Dark Mode in Remix
Posted on 05/09/2023
18 min read
Introduction
Nowadays when we are working on a new app or web we usually design with dark mode as default or we think of having that as an out-of-the-box feature, but having dark mode is not only a cool feature:
Dark mode is also a valuable accessibility feature. Some types of visual impairment can make it painful to look at bright colours, or large blocks of white might wash over the black text. Some people with dyslexia or Irlen’s Syndrome can struggle to read black text on a white background.
Quote taken from Mobile A11y.
So, the problem we aim to address here is how to implement dark mode in Remix.
Styling
Since this post focuses on adding dark mode to Remix, we won't delve deep into CSS:
[data-theme='light'] {
color-scheme: light;
}
[data-theme='dark'] {
filter: invert(1) hue-rotate(180deg);
}
This example does not take into account real accessibility, for a best practices implementation you can check: Tailwind
Tailwind caveat: to make it work with this blog post you should change over to class
method of dark mode and set the name to data-theme
.
Another really good alternative is Daisyui
Approaches to implement the theme toggle
When it comes to implement a way to the user to toggle the theme to either mode, you have two options:
- Save the theme to the
LocalStorage
, override thedata-theme
attribute in the html tag and subscribe it to the changes to make the switch instantly. - Save the theme into a cookie and make the server do the change for you.
I will cover only the second option since we are using Remix, we want to take all the benefits of server-side rendering and try to ship the least possible amount of javascript to the client.
Getting started
Let's start by creating the cookie helper that will be used to store and get the theme preferences:
app/services/theme.server.js
import { createCookie } from '@remix-run/node'
export const themeCookie = createCookie('theme', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production'
})
export async function getThemeFromCookies(request: Request) {
const theme = await themeCookie.parse(request.headers.get('Cookie'))
return theme || 'system'
}
the getThemeFromCookies
function is a quick helper that will be useful to get the theme preferences in the next step.
Adding the theme to the html tag
Now we need to create a loader to bring up cookie value of the theme preference and set it to the html
tag:
app/root.jsx
import { useMemo } from 'react'
import { useLoaderData } from '@remix-run/react'
import { json} from '@remix-run/node'
import { getThemeFromCookies } from './services/theme.server'
export const loader = async ({ request }) => {
const theme = await getThemeFromCookies(request)
return json({ theme })
}
export default function App() {
const { theme } = useLoaderData()
const htmlProps = useMemo(() => {
return {
lang: 'en',
'data-theme': theme === 'system' ? undefined : theme,
}
}, [theme])
return (
<html {...htmlProps}>
(...)
</html>
)
}
We check if the theme cookie has a value, if it is not the case we just use the system preferences, otherwise we set the data-theme
attribute with the user preference
Adding endpoint to switch preferences
This can be done either in an action on the root
but I personally like to have them in a different resource route:
app/routes/prefereces.theme.js
import { redirectBack } from 'remix-utils'
import { themeCookie } from '../services/theme.server'
export const action = async ({ request }) => {
const form = await request.formData()
const theme = form.get('theme')
return redirectBack(request, {
fallback: '/',
headers: {
'Set-Cookie': await themeCookie.serialize(theme)
}
})
}
Here we take the theme
that the user want to set as preference from the request's form data and make a redirect back with the util function of sergiodxa's awesome library remix-utils
that sends the user back to where he/she was.
Create Toggle component
Last but not least we need to create a component that when being clicked it toggles the theme:
app/components/ToggleThemeButton.jsx
import { Form } from '@remix-run/react'
export default function ToggleThemeButton({ currentTheme }) {
const themeToToggleTo = currentTheme === 'dark' ? 'light' : 'dark'
return (
<Form action="/preferences/theme" method="POST">
<input type="hidden" name="theme" value={themeToToggleTo} />
<button>{currentTheme === 'dark' ? 'switch to light mode' : 'switch to dark mode'}</button>
</Form>
)
}
We add the opposite of the current theme value as a hidden input so that when the user clicks on the button, the page submits to the resource route we created previously with the theme correct theme value to change to.
And that's it, we have a fully server-side theme toggle for our Remix web app, a nice to have would be instead of a button, to have a dropdown to select between "System", "Light mode", and "Dark mode"