Imagine visiting a website that seamlessly adapts to your preferences—effortlessly switching between light, dark and system-based themes.
This article continues my series on SSR with React. In the basics article, we explored production-ready configurations, while in advanced techniques, we tackled challenges like hydration errors. Now, we’ll take it a step further by implementing robust theming support that integrates seamlessly with SSR.
Table of Contents
- Themes and SSR
-
Implementation
- Install Dependencies
- Add cookie to the Server Build
- Apply Themes on the Server
- Handle Themes on the Client
- Conclusion
Themes and SSR
The main issue is the Initial Flash of Incorrect Theme (FOIT).
Essentially, themes are just about changing CSS variables. In most cases, you’ll work with three themes:
- Light: The default set of CSS variables.
- Dark: Applied when the tag has the class dark.
- System: Automatically switches based on the user's system preference, using the (prefers-color-scheme: dark) media query to determine if the theme should be dark or light.
By default, the server will render the HTML with the light theme and send it to the browser. If a user prefers the dark theme, they will see a visible theme change on the first page load, which disrupts the user experience.
There are two main ways to solve this issue:
- Add a <script> tag in the HTML on server and set class dynamically on the client.</script>
- Use a cookie to store the user’s theme preference and set the class on the server.
The first solution is how the next-themes package works (Jan 2025). In this article, you’ll implement the cookie-based approach to ensure seamless theme handling in your SSR application.
Implementation
To implement themes, you’ll use two cookies:
- serverTheme - Used to apply the correct class to the tag.
- clientTheme - Used to handle hydration errors.
The client always sets both cookies, ensuring the server can correctly render the appropriate theme on the next request.
This guide builds upon concepts introduced in the previous article, Building Production-Ready SSR React Applications, which you can find linked at the bottom. For simplicity, shared constants and types are not created here, but you can find their implementation in the example repository.
Install Dependencies
Install the required packages for cookie handling:
pnpm add cookie js-cookie
Install types for js-cookie:
pnpm add -D @types/js-cookie
If you’re not using react-router in your app, you can use the cookie package as a devDependencies.
Add cookie to the Server Build
Update your tsup configuration file:
// ./tsup.config.ts import { defineConfig } from 'tsup' export default defineConfig({ entry: ['server'], outDir: 'dist/server', target: 'node22', format: ['cjs'], clean: true, minify: true, external: ['lightningcss', 'esbuild', 'vite'], noExternal: [ 'express', 'sirv', 'compression', 'cookie', // Include the cookie in the server build ], })
Apply Themes on the Server
Define Theme Constants
// ./server/constants.ts export const CLIENT_THEME_COOKIE_KEY = 'clientTheme' export const SERVER_THEME_COOKIE_KEY = 'serverTheme' export enum Theme { Light = 'light', Dark = 'dark', System = 'system' }
Apply Theme Class to Tag
Create a utility function to apply the correct theme class to the tag based on the serverTheme cookie:
// ./server/lib/applyServerTheme.ts import { parse } from 'cookie' import { Request } from 'express' import { SERVER_THEME_COOKIE_KEY, Theme } from '../constants' export function applyServerTheme(req: Request, html: string): string { const cookies = parse(req.headers.cookie || '') const theme = cookies?.[SERVER_THEME_COOKIE_KEY] if (theme === Theme.Dark) { return html.replace('', ` <h4> Retrieve the Client Theme Cookie </h4> <p>Create a utility function to retrieve the clientTheme cookie<br> </p> <pre class="brush:php;toolbar:false">// ./server/getClientTheme.ts import { parse } from 'cookie' import { Request } from 'express' import { CLIENT_THEME_COOKIE_KEY, Theme } from '../constants' export function getClientTheme(req: Request) { const cookies = parse(req.headers.cookie || '') return cookies?.[CLIENT_THEME_COOKIE_KEY] as Theme | undefined }
Update Server Configurations for Theming
Development Configuration:
// ./server/dev.ts import fs from 'fs' import path from 'path' import { Application } from 'express' import { HTML_KEY } from './constants' import { applyServerTheme } from './lib/applyServerTheme' import { getClientTheme } from './lib/getClientTheme' const HTML_PATH = path.resolve(process.cwd(), 'index.html') const ENTRY_SERVER_PATH = path.resolve(process.cwd(), 'src/entry-server.tsx') export async function setupDev(app: Application) { const vite = await ( await import('vite') ).createServer({ root: process.cwd(), server: { middlewareMode: true }, appType: 'custom', }) app.use(vite.middlewares) app.get('*', async (req, res, next) => { try { let html = fs.readFileSync(HTML_PATH, 'utf-8') html = await vite.transformIndexHtml(req.originalUrl, html) const { render } = await vite.ssrLoadModule(ENTRY_SERVER_PATH) // send Client Theme from cookie to render const appHtml = await render(getClientTheme(req)) // Apply Server theme on template html html = applyServerTheme(req, html) html = html.replace(HTML_KEY, appHtml) res.status(200).set({ 'Content-Type': 'text/html' }).end(html) } catch (e) { vite.ssrFixStacktrace(e as Error) console.error((e as Error).stack) next(e) } }) }
Production Configuration:
// ./server/prod.ts import fs from 'fs' import path from 'path' import compression from 'compression' import { Application } from 'express' import sirv from 'sirv' import { HTML_KEY } from './constants' import { applyServerTheme } from './lib/applyServerTheme' import { getClientTheme } from './lib/getClientTheme' const CLIENT_PATH = path.resolve(process.cwd(), 'dist/client') const HTML_PATH = path.resolve(process.cwd(), 'dist/client/index.html') const ENTRY_SERVER_PATH = path.resolve(process.cwd(), 'dist/ssr/entry-server.js') export async function setupProd(app: Application) { app.use(compression()) app.use(sirv(CLIENT_PATH, { extensions: [] })) app.get('*', async (req, res, next) => { try { let html = fs.readFileSync(HTML_PATH, 'utf-8') const { render } = await import(ENTRY_SERVER_PATH) // send Client Theme from cookie to render const appHtml = await render(getClientTheme(req)) // Apply Server theme on template html html = applyServerTheme(req, html) html = html.replace(HTML_KEY, appHtml) res.status(200).set({ 'Content-Type': 'text/html' }).end(html) } catch (e) { console.error((e as Error).stack) next(e) } }) }
Handle Themes on the Client
Define Constants
Duplicate constants for client use or move them to a shared folder
// ./src/constants.ts export const SSR = import.meta.env.SSR export const CLIENT_THEME_COOKIE_KEY = 'clientTheme' export const SERVER_THEME_COOKIE_KEY = 'serverTheme' export enum Theme { Light = 'light', Dark = 'dark', System = 'system', }
Create Theme Context
Set up a React context to manage theme state and provide theme management methods:
// ./src/theme/context.ts import { createContext, useContext } from 'react' import { Theme } from '../constants' export type ThemeContextState = { theme: Theme setTheme: (theme: Theme) => void } export const ThemeContext = createContext<themecontextstate>({ theme: Theme.System, setTheme: () => null, }) export const useThemeContext = () => useContext(ThemeContext) </themecontextstate>
Implement Theme Utilities
// ./src/theme/lib.ts import Cookies from 'js-cookie' import { CLIENT_THEME_COOKIE_KEY, SERVER_THEME_COOKIE_KEY, SSR, Theme } from '../constants' // Resolve the system theme using the `prefers-color-scheme` media query export function resolveSystemTheme() { if (SSR) return Theme.Light return window.matchMedia('(prefers-color-scheme: dark)').matches ? Theme.Dark : Theme.Light } // Update the theme cookies and set appropriate class to export function updateTheme(theme: Theme) { if (SSR) return const resolvedTheme = theme === Theme.System ? resolveSystemTheme() : theme Cookies.set(CLIENT_THEME_COOKIE_KEY, theme) Cookies.set(SERVER_THEME_COOKIE_KEY, resolvedTheme) window.document.documentElement.classList.toggle('dark', resolvedTheme === Theme.Dark) } // Get the default theme from cookies export function getDefaultTheme(): Theme { if (SSR) return Theme.System const theme = (Cookies.get(CLIENT_THEME_COOKIE_KEY) as Theme) || Theme.System updateTheme(theme) return theme }
Create a Theme Provider
// ./src/theme/Provider.tsx import { PropsWithChildren, useState } from 'react' import { Theme } from '../constants' import { ThemeContext } from './context' import { getDefaultTheme, updateTheme } from './lib' type Props = PropsWithChildren & { defaultTheme?: Theme // Handle theme for SSR } export function ThemeProvider({ children, defaultTheme }: Props) { const [theme, setTheme] = useState<theme>(defaultTheme || getDefaultTheme()) const handleSetTheme = (theme: Theme) => { setTheme(theme) updateTheme(theme) } return <themecontext value="{{" theme settheme: handlesettheme>{children}</themecontext> } </theme>
// ./src/theme/index.ts export { ThemeProvider } from './Provider' export { useThemeContext } from './context'
Use Theme Context in Components
// ./src/App.tsx import reactLogo from './assets/react.svg' import viteLogo from '/vite.svg' import Card from './Card' import { Theme } from './constants' import { ThemeProvider } from './theme' import './App.css' // Theme from Server Entry type AppProps = { theme?: Theme } function App({ theme }: AppProps) { return ( <themeprovider defaulttheme="{theme}"> <div> <a href="https://vite.dev" target="_blank" rel="noreferrer"> <img src="%7BviteLogo%7D" classname="logo" alt="Vite logo"> </a> <a href="https://react.dev" target="_blank" rel="noreferrer"> <img src="%7BreactLogo%7D" classname="logo react" alt="React logo"> </a> </div> <h1 id="Vite-React">Vite + React</h1> <card></card> <p classname="read-the-docs">Click on the Vite and React logos to learn more</p> </themeprovider> ) } export default App
Create a Card Component
// ./src/Card.tsx import { useState } from 'react' import { Theme } from './constants' import { useThemeContext } from './theme' function Card() { const { theme, setTheme } = useThemeContext() const [count, setCount] = useState(0) return ( <div classname="card"> <button onclick="{()"> setCount((count) => count + 1)}>count is {count}</button> <p> Edit <code>src/App.tsx</code> and save to test HMR </p> <div> Themes:{' '} <select value="{theme}" onchange="{(event)"> setTheme(event.target.value as Theme)}> <option value="{Theme.System}">System</option> <option value="{Theme.Light}">Light</option> <option value="{Theme.Dark}">Dark</option> </select> </div> </div> ) } export default Card
Resolve Hydration Error
Pass the theme to the server render method to ensure the server-generated HTML matches the client-side rendering:
import { renderToString } from 'react-dom/server' import App from './App' import { Theme } from './constants' export function render(theme: Theme) { return renderToString(<app theme="{theme}"></app>) }
Add Styles
:root { color: #242424; background-color: rgba(255, 255, 255, 0.87); } :root.dark { color: rgba(255, 255, 255, 0.87); background-color: #242424; }
Conclusion
In this article, we tackled the challenges of implementing seamless theming in SSR React applications. By using cookies and integrating both client-side and server-side logic, we created a robust system that supports light, dark and system-based themes without hydration errors or user experience disruptions.
Explore the Code
- Example: react-ssr-themes-example
- Landing with SSR: professional-landing
Related Articles
This is part of my series on SSR with React. Stay tuned for more articles!
- Building Production-Ready SSR React Applications
- Advanced React SSR Techniques with Streaming and Dynamic Data
- Setting Up Themes in SSR React Applications
Stay Connected
I’m always open to feedback, collaboration or discussing tech ideas — feel free to reach out!
- Portfolio: maxh1t.xyz
- Email: m4xh17@gmail.com
The above is the detailed content of Setting Up Themes in SSR React Applications. For more information, please follow other related articles on the PHP Chinese website!

The main difference between Python and JavaScript is the type system and application scenarios. 1. Python uses dynamic types, suitable for scientific computing and data analysis. 2. JavaScript adopts weak types and is widely used in front-end and full-stack development. The two have their own advantages in asynchronous programming and performance optimization, and should be decided according to project requirements when choosing.

Whether to choose Python or JavaScript depends on the project type: 1) Choose Python for data science and automation tasks; 2) Choose JavaScript for front-end and full-stack development. Python is favored for its powerful library in data processing and automation, while JavaScript is indispensable for its advantages in web interaction and full-stack development.

Python and JavaScript each have their own advantages, and the choice depends on project needs and personal preferences. 1. Python is easy to learn, with concise syntax, suitable for data science and back-end development, but has a slow execution speed. 2. JavaScript is everywhere in front-end development and has strong asynchronous programming capabilities. Node.js makes it suitable for full-stack development, but the syntax may be complex and error-prone.

JavaScriptisnotbuiltonCorC ;it'saninterpretedlanguagethatrunsonenginesoftenwritteninC .1)JavaScriptwasdesignedasalightweight,interpretedlanguageforwebbrowsers.2)EnginesevolvedfromsimpleinterpreterstoJITcompilers,typicallyinC ,improvingperformance.

JavaScript can be used for front-end and back-end development. The front-end enhances the user experience through DOM operations, and the back-end handles server tasks through Node.js. 1. Front-end example: Change the content of the web page text. 2. Backend example: Create a Node.js server.

Choosing Python or JavaScript should be based on career development, learning curve and ecosystem: 1) Career development: Python is suitable for data science and back-end development, while JavaScript is suitable for front-end and full-stack development. 2) Learning curve: Python syntax is concise and suitable for beginners; JavaScript syntax is flexible. 3) Ecosystem: Python has rich scientific computing libraries, and JavaScript has a powerful front-end framework.

The power of the JavaScript framework lies in simplifying development, improving user experience and application performance. When choosing a framework, consider: 1. Project size and complexity, 2. Team experience, 3. Ecosystem and community support.

Introduction I know you may find it strange, what exactly does JavaScript, C and browser have to do? They seem to be unrelated, but in fact, they play a very important role in modern web development. Today we will discuss the close connection between these three. Through this article, you will learn how JavaScript runs in the browser, the role of C in the browser engine, and how they work together to drive rendering and interaction of web pages. We all know the relationship between JavaScript and browser. JavaScript is the core language of front-end development. It runs directly in the browser, making web pages vivid and interesting. Have you ever wondered why JavaScr


Hot AI Tools

Undresser.AI Undress
AI-powered app for creating realistic nude photos

AI Clothes Remover
Online AI tool for removing clothes from photos.

Undress AI Tool
Undress images for free

Clothoff.io
AI clothes remover

Video Face Swap
Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Article

Hot Tools

Safe Exam Browser
Safe Exam Browser is a secure browser environment for taking online exams securely. This software turns any computer into a secure workstation. It controls access to any utility and prevents students from using unauthorized resources.

ZendStudio 13.5.1 Mac
Powerful PHP integrated development environment

SecLists
SecLists is the ultimate security tester's companion. It is a collection of various types of lists that are frequently used during security assessments, all in one place. SecLists helps make security testing more efficient and productive by conveniently providing all the lists a security tester might need. List types include usernames, passwords, URLs, fuzzing payloads, sensitive data patterns, web shells, and more. The tester can simply pull this repository onto a new test machine and he will have access to every type of list he needs.

PhpStorm Mac version
The latest (2018.2.1) professional PHP integrated development tool

MinGW - Minimalist GNU for Windows
This project is in the process of being migrated to osdn.net/projects/mingw, you can continue to follow us there. MinGW: A native Windows port of the GNU Compiler Collection (GCC), freely distributable import libraries and header files for building native Windows applications; includes extensions to the MSVC runtime to support C99 functionality. All MinGW software can run on 64-bit Windows platforms.
