search
HomeWeb Front-endJS TutorialSetting Up Themes in SSR React Applications

Setting Up Themes in SSR React Applications

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:

  1. serverTheme - Used to apply the correct class to the tag.
  2. 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!

Statement
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Python vs. JavaScript: A Comparative Analysis for DevelopersPython vs. JavaScript: A Comparative Analysis for DevelopersMay 09, 2025 am 12:22 AM

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.

Python vs. JavaScript: Choosing the Right Tool for the JobPython vs. JavaScript: Choosing the Right Tool for the JobMay 08, 2025 am 12:10 AM

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: Understanding the Strengths of EachPython and JavaScript: Understanding the Strengths of EachMay 06, 2025 am 12:15 AM

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.

JavaScript's Core: Is It Built on C or C  ?JavaScript's Core: Is It Built on C or C ?May 05, 2025 am 12:07 AM

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

JavaScript Applications: From Front-End to Back-EndJavaScript Applications: From Front-End to Back-EndMay 04, 2025 am 12:12 AM

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.

Python vs. JavaScript: Which Language Should You Learn?Python vs. JavaScript: Which Language Should You Learn?May 03, 2025 am 12:10 AM

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.

JavaScript Frameworks: Powering Modern Web DevelopmentJavaScript Frameworks: Powering Modern Web DevelopmentMay 02, 2025 am 12:04 AM

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.

The Relationship Between JavaScript, C  , and BrowsersThe Relationship Between JavaScript, C , and BrowsersMay 01, 2025 am 12:06 AM

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

See all articles

Hot AI Tools

Undresser.AI Undress

Undresser.AI Undress

AI-powered app for creating realistic nude photos

AI Clothes Remover

AI Clothes Remover

Online AI tool for removing clothes from photos.

Undress AI Tool

Undress AI Tool

Undress images for free

Clothoff.io

Clothoff.io

AI clothes remover

Video Face Swap

Video Face Swap

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

Hot Tools

Safe Exam Browser

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

ZendStudio 13.5.1 Mac

Powerful PHP integrated development environment

SecLists

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

PhpStorm Mac version

The latest (2018.2.1) professional PHP integrated development tool

MinGW - Minimalist GNU for Windows

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.