Next.js starter your AI actually understands. Ship internal tools in days not weeks. Pre-order $199 $499 → [Get it now]

How to create a theme provider in React

Implementing a consistent design system across a React application requires a centralized way to manage styles, colors, and layout tokens.
With over 25 years of experience in software development and as the creator of CoreUI, I’ve built numerous high-performance theme engines for enterprise dashboards.
The most efficient and modern solution is to use the React Context API combined with a custom provider to distribute theme state without prop drilling.
This approach ensures your UI components remain synchronized, facilitates easy dark mode switching, and improves overall maintainability.

Wrap your application in a custom context provider that manages the theme state and provides it to all child components via a dedicated hook.

import { createContext, useContext, useState, useEffect } from 'react'

const ThemeContext = createContext(undefined)

export const useTheme = () => {
  const context = useContext(ThemeContext)
  if (!context) {
    throw new Error('useTheme must be used within a ThemeProvider')
  }
  return context
}

export const ThemeProvider = ({ children }) => {
  const [mode, setMode] = useState(() => {
    return localStorage.getItem('app-theme') || 'light'
  })

  useEffect(() => {
    localStorage.setItem('app-theme', mode)
    document.documentElement.setAttribute('data-theme', mode)
  }, [mode])

  const toggleTheme = () => {
    setMode((prev) => (prev === 'light' ? 'dark' : 'light'))
  }

  return (
    <ThemeContext.Provider value={{ mode, toggleTheme, isDark: mode === 'dark' }}>
      {children}
    </ThemeContext.Provider>
  )
}

This code creates a ThemeContext and a ThemeProvider component that manages the current theme mode. The provider reads the initial preference from localStorage, syncs changes back to storage, and sets a data-theme attribute on the root HTML element so CSS variables can respond to theme switches automatically.

1. Defining Your Theme Configuration

Before building the provider, you need a clear definition of your theme tokens. This structure allows you to maintain consistency across the entire application and makes it easier to add new themes in the future.

// themes.js
export const lightTheme = {
  background: '#ffffff',
  color: '#333333',
  primary: '#321fdb',
  border: '#d8dbe0'
}

export const darkTheme = {
  background: '#181924',
  color: '#ffffff',
  primary: '#4638c2',
  border: '#2c2c34'
}

export const getThemeConfig = (mode) => {
  return mode === 'dark' ? darkTheme : lightTheme
}

In this section, we define objects for both light and dark modes. These tokens represent the core colors used in your application. By centralizing these values, you avoid hardcoding hex codes inside individual components, which is a key principle we follow in CoreUI React components. Using a helper function like getThemeConfig simplifies the logic of retrieving the active set of styles.

2. Creating the React Context and Custom Hook

The next step is to create the context that will hold our theme data and a custom hook to consume it. Custom hooks provide a cleaner API for your components and allow for better abstraction.

// ThemeContext.js
import { createContext, useContext } from 'react'

const ThemeContext = createContext(undefined)

export const useTheme = () => {
  const context = useContext(ThemeContext)
  if (!context) {
    throw new Error('useTheme must be used within a ThemeProvider')
  }
  return context
}

export { ThemeContext }

We initialize ThemeContext with undefined to help catch errors where the hook might be used outside its provider. The useTheme hook is a wrapper around useContext. It includes a safety check to ensure the context is available. This pattern is essential for large-scale applications where component nesting can become complex.

3. Implementing the Theme Provider Component

The ThemeProvider is the core component that manages the state and provides the data to the rest of the application. It handles the logic for switching themes and persisting the user preference.

// ThemeProvider.js
import { useState, useEffect } from 'react'
import { ThemeContext } from './ThemeContext'

export const ThemeProvider = ({ children }) => {
  const [mode, setMode] = useState(() => {
    return localStorage.getItem('app-theme') || 'light'
  })

  useEffect(() => {
    localStorage.setItem('app-theme', mode)
    document.documentElement.setAttribute('data-theme', mode)
  }, [mode])

  const toggleTheme = () => {
    setMode((prevMode) => (prevMode === 'light' ? 'dark' : 'light'))
  }

  const value = {
    mode,
    toggleTheme,
    isDark: mode === 'dark'
  }

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  )
}

This implementation uses an initializer function in useState to read the initial theme from localStorage. The useEffect hook synchronizes the theme state with localStorage and updates a data-theme attribute on the root HTML element. This allows you to use CSS variables that automatically update when the theme changes.

4. Building a Theme Toggle Component

With the provider in place, you need a way for users to switch themes. A simple toggle button is the most common UI pattern for this feature.

// ThemeToggle.js
import { CButton } from '@coreui/react'
import { useTheme } from './ThemeContext'

const ThemeToggle = () => {
  const { mode, toggleTheme } = useTheme()
  const label = mode === 'light' ? 'Dark Mode' : 'Light Mode'

  return (
    <CButton 
      color='primary' 
      onClick={toggleTheme}
      variant='outline'
    >
      Switch to {label}
    </CButton>
  )
}

In this example, we use the CButton component from CoreUI to create a polished toggle. The button text dynamically updates based on the current mode.

5. Applying Global Styles with CSS Variables

To make the theme provider truly effective, you should link the React state to CSS variables. This allows even non-React elements or legacy CSS to respect the active theme.

/* index.css */
:root[data-theme='light'] {
  --bg-color: #ffffff;
  --text-color: #333333;
  --primary-color: #321fdb;
}

:root[data-theme='dark'] {
  --bg-color: #181924;
  --text-color: #ffffff;
  --primary-color: #4638c2;
}

body {
  background-color: var(--bg-color);
  color: var(--text-color);
  transition: background-color 0.3s ease;
}

By using the data-theme attribute we set in our provider, we can define scoped CSS variables. This is the same strategy we use in the CoreUI React Dashboard Template to handle dark mode transitions smoothly. It keeps the styling logic in CSS where it belongs, while React handles the state orchestration.

6. Consuming Theme Data in UI Components

Finally, you can use the useTheme hook in any component within the provider’s tree to access the current theme and apply conditional logic or styles.

// ThemedCard.js
import { CCard, CCardBody, CCardHeader } from '@coreui/react'
import { useTheme } from './ThemeContext'

const ThemedCard = ({ title, children }) => {
  const { isDark } = useTheme()

  return (
    <CCard className={isDark ? 'bg-dark text-white' : ''}>
      <CCardHeader>{title}</CCardHeader>
      <CCardBody>
        {children}
      </CCardBody>
    </CCard>
  )
}

This component demonstrates how to use the isDark boolean from our context to conditionally apply classes to a CoreUI Card component. This pattern is highly reusable and keeps your components clean.

Best Practice Note:

Always memoize the context value using useMemo if your provider performs heavy calculations or contains many state variables. This prevents unnecessary re-renders of all consuming components when the provider’s parent re-renders. In CoreUI templates, we also recommend checking the user’s system preference using window.matchMedia('(prefers-color-scheme: dark)') to provide a better initial experience by matching the OS theme automatically.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.

Answers by CoreUI Core Team