导航菜单
首页 >  Improved dark mode default styling with the color  > Setting up Dark Theme with Remix and Stitches

Setting up Dark Theme with Remix and Stitches

Want the solution and not a story? Check out the example repo on GitHub.

I have been working on a side project called Cryptocons.io for the past few months. It’s not even close to presentable, but I typically use side projects to try new frameworks and tools. This time, I wanted to try a combination of 3 tools I have been gushing over lately: Remix, Stitches, and Radix.

Setting up a Remix project was really easy. I remarked about this on Twitter and how excited I am for how Remix prompts a different component/project structure approach.

Initial Remix Reaction

So far it feels like Remix blurs the line between pages, components, and state in how the structure of the project’s folder architecture dictates the actual UI. Typically I set up a project by creating a page layout and passing components in willy-nilly. I then handle each component child’s data and state via some form of Context Provider.

Remix feels conceptually similar but where files live and how they are named seem to play a much more significant role. I like this about it so far (time will tell long-term). It inherently encourages me to be consistent and intentional about my project's structure. Not solving every problem in the context of a React hook has already caused me to learn quite a lot about web development and how web pages are fundamentally created.

Setting Up Stitches and Remix

I ran into some snags for setting up Stitches and Remix at first. The setup was a breeze: install both packages, run the dev server, and import a Stitches styled component into a page route. However, the nuance of SSR, having the site load without an initial flash of unstyled content (also known as FART), and tracking a user’s theme state were a little trickier. This is especially true if I wanted to detect prefers-color-scheme dynamically.

Failed Attempt #1

At first, I set up the site using Stitches SSR documentation. My styles were rendered correctly, but this approach caused some odd hydration errors in the console and didn’t fix my FART. Interestingly, my styles rendered fine without doing this part at all.

Meh Attempt #2

I was about to start following the CSS-in-JS instructions on Remix’s site, and I imagine it would have worked just fine, but I wanted to eliminate the need for a “throwaway” render.

Anthony on Twitter suggested loading the CSS via a loader function in the root.tsx server file. This eliminated the hydration warning, but I got a FART on my screen during refreshes.

TSX1import {2 Links,3 LiveReload,4 Meta,5 Outlet,6 Scripts,7 ScrollRestoration,8 LoaderFunction,9 useLoaderData,10} from 'remix'1112import { getCssText } from 'path/to/stitches.config'1314export const loader: LoaderFunction = async () => {15 return new Response(getCssText(), {16headers: { 'Content-Type': 'text/css; ' },17 })18}1920export default function App() {21 const styles = useLoaderData()2223 return (2425 26272829{styles}30 31 32333435{process.env.NODE_ENV === 'development' && }36 3738 )39}40root.tsxWinning Attempt #3

Jenna Smith from Modulz hopped in with a simple gist that worked like a charm. I have to imagine altering the head tag via regex isn’t the ideal solution, but it works. Speculating from the tweet, Stitches may be hot on the trail of a static CSS mechanism, which will make all this a whole lot easier.

TSX1import type { EntryContext } from 'remix'2import ReactDOMServer from 'react-dom/server'3import { RemixServer } from 'remix'4import { getCssText } from '../stitches.config'56export default function handleRequest(7 request: Request,8 responseStatusCode: number,9 responseHeaders: Headers,10 remixContext: EntryContext11) {12 const markup = ReactDOMServer.renderToString(1314 ).replace(//, `${getCssText()}`)1516 return new Response('' + markup, {17status: responseStatusCode,18headers: {19 ...Object.fromEntries(responseHeaders),20 'Content-Type': 'text/html',21},22 })23}24entry.server.tsxIf you want one theme without a color scheme and no FART, your work is done 🎉. Altering the head element from the server loads all the necessary styles ahead of paint.Stitches and Remix Color Scheme Theme Setup

Most of my experience with web development so far is front-of-the-frontend and has sidestepped server considerations. I typically use Gatsby or Next with CSS-in-JS. Theme considerations and state in those cases are taken care of via Javascript on the client-side by a supplied ThemeProvider or custom ThemeContext that addresses the FART.

This was a unique opportunity to learn how to lean on the browser’s Sec-CH-Prefers-Color-Scheme client headers. Remix embraces the server, time to get on board.

1. Create the theming color aliases

For our example, let’s set up the minimal theme functionality for a demo. This means setting color aliases for text color, anchor color, and our body’s background color in both light and dark themes.

TSX1import { createStitches } from '@stitches/react'23export const { styled, createTheme, globalCss, getCssText, theme } =4 createStitches({5theme: {6 colors: {7text: '#191919',8bgBody: '#f8f9fa',9anchor: 'DarkGoldenRod',10 },11},12 })1314export const darkTheme = createTheme('dark', {15 colors: {16text: '#f8f9fa',17bgBody: '#191919',18anchor: 'BlanchedAlmond',19 },20})2122export const globalStyles = globalCss({23 body: {24color: '$text',25backgroundColor: '$bgBody',26 },2728 a: {29color: '$anchor',30 },31})32stitches.config.ts2. Tracking Color Scheme State

We want our theming functionality to have pretty simple logic:

If the user has set a theme manually for the site (determined by a theme cookie we will create), we set the selected themeIf the user hasn’t set a theme manually and has a user prefers-color-mode preference, we grab it from the request header (so we can process it server-side) and set the theme accordinglyIf neither of these is true, we set to our default theme: lightTS1import { createCookie } from 'remix'23// Create a cookie to track color scheme state4export let colorSchemeCookie = createCookie('color-scheme')56// Helper function to get the value of the color scheme cookie7export const getColorSchemeToken = async (request: Request) =>8 await colorSchemeCookie.parse(request.headers.get('Cookie'))910export const getColorScheme = async (request: Request) => {11 // Manually selected theme12 const userSelectedColorScheme = await getColorSchemeToken(request)13 // System preferred color scheme header14 const systemPreferredColorScheme = request.headers.get(15'Sec-CH-Prefers-Color-Scheme'16 )1718 // Return the manually selected theme19 // or system preferred theme or default theme20 return userSelectedColorScheme ?? systemPreferredColorScheme ?? 'light'21}22cookies.ts3. Setting the correct themeCSS1:root,2.t-cIBkYY {3 --colors-text: #191919;4 --colors-bgBody: #f8f9fa;5 --colors-anchor: DarkGoldenRod;6}78.dark {9 --colors-text: #f8f9fa;10 --colors-bgBody: #191919;11 --colors-anchor: BlanchedAlmond;12}13styles.css

I borrowed an approach I picked up from Radix Design System GitHub. They have a DarkMode button component that essentially adds or removes the darkTheme class name to the body element.

I love how simple this is. The default theme’s :root css color alias variables are overridden via specificity by using the className of the dark theme. In our case, we are aligning the names of the themes to the browser’s color-scheme strings but really the class="light" has no effect.

Stitches generates a unique class hash for the default theme (which is light in our case) and we specify class="dark" for our darkTheme class to override it.

TSX1import {2 LiveReload,3 LoaderFunction,4 Outlet,5 useLoaderData,6 HeadersFunction,7} from 'remix'8import { getColorScheme } from './cookies'9import { globalStyles } from '../stitches.config'1011export const headers: HeadersFunction = () => ({12 'Accept-CH': 'Sec-CH-Prefers-Color-Scheme',13})1415export const loader: LoaderFunction = async ({ request }) => ({16 colorScheme: await getColorScheme(request),17})1819export default function App() {20 const { colorScheme } = useLoaderData()21 globalStyles()2223 return (2425 262728 29 3031{process.env.NODE_ENV === 'development' && }32 3334 )35}36root.tsxSet up the theme switcher

The last piece of the puzzle is to put a Button on a route that changes the cookie’s theme-token value. This is the code snippet I used in my index.tsx file but I imagine there are lots of ways you could do this.

TSX1import { Button } from '~/components/Button'2import { ActionFunction, Form, redirect, Link } from 'remix'3import { colorSchemeCookie, getColorScheme } from '../cookies'45export const action: ActionFunction = async ({ request }) => {6 const currentColorScheme = await getColorScheme(request)7 const newColorScheme = currentColorScheme === 'light' ? 'dark' : 'light'89 return redirect(request.url, {10headers: {11 'Set-Cookie': await colorSchemeCookie.serialize(newColorScheme),12},13 })14}1516export default function Index() {17 return (1819 20Change Theme21 2223 )24}25index.tsx

If you have any suggestions for ways to improve this documentation or the GitHub example repo, lemme have ‘em.

ResourcesHello darkness, my old friend. Amazing read on prefers-color-scheme functionality and the history of dark-mode.Improved dark mode default styling: The color-scheme CSS property and the corresponding meta tag

相关推荐: