Work

Products

Services

About Us

Careers

Blog

Resources

Building a Scalable Theme System in React
Image

Sahil Dhawan

Sep 23, 2025

Overview

A practical guide to creating scalable and maintainable theming solutions in React applications using configuration-driven design and Context API.

Building a Scalable Theme System in React

Theming is one of those things that seems simple on the surface, yet often grows into a tangled mess as projects scale. What starts as a few color variables can quickly balloon into deeply coupled styles, CSS overrides, tool-specific hacks, and inconsistent UI tokens.

At its core, though, a theme is just a configuration object. When treated that way, theming becomes simpler to build, easier to manage, and more powerful to scale.

What Not to Do

Before diving into the right way, here’s an example of a brittle theming setup:

/* theme.css */
.light-button {
  background-color: white;
  color: black;
  border-radius: 8px;
  font-family: Arial;
}

.dark-button {
  background-color: black;
  color: white;
  border-radius: 8px;
  font-family: Arial;
}
// Button.tsx
import "./theme.css";

const Button = ({ theme }) => {
  return <button className={`${theme}-button`}>Tightly Coupled Button</button>;
};

This approach tightly couples your components to CSS class names and theme logic. It’s harder to extend or scale, and porting this to another design system or UI library becomes messy fast.

The Right Way to Approach Theming

When designing a theming system, the primary goal is to create a scalable, maintainable, and flexible solution that adapts as your application grows. Instead of hard-coding styles or relying on brittle CSS class names, the best practice is to treat your theme as a centralized configuration object. This object defines all your design tokens — colors, typography, spacing, borders, and more — in one place.

By abstracting your theme details into a structured config, you gain several benefits:

  • Consistency: All components reference the same source of truth, ensuring uniform styles throughout the app.
  • Flexibility: You can switch themes dynamically without changing component logic or CSS.
  • Scalability: Adding new theme variations or design tokens becomes easy without refactoring.
  • Portability: Components become self-contained and theming-agnostic, suitable for reuse or integration with different UI libraries.
  • Developer Experience: Reduces cognitive load by centralizing style management, making it easier for teams to onboard and contribute.

A key technique to achieve this is leveraging React’s Context API to provide theme data throughout your component tree. This avoids prop drilling and keeps your state management lightweight and focused.

With that in mind, the next step is to define your theme as an explicit configuration object, which serves as the foundation for your theming system.

Define a Theme Object

// theme.ts
export interface Theme {
  name: string;
  colors: {
    background: string;
    text: string;
    primary: string;
  };
  typography: {
    fontSize: number;
    fontFamily: string;
  };
  border: {
    radius: number;
  };
  spacing: {
    padding: number;
  };
}
// themes.ts
export const themes: [Theme] = [
  {
    name: "light",
    colors: {
      background: "#ffffff",
      text: "#121212",
      primary: "#007bff",
    },
    typography: {
      fontSize: 16,
      fontFamily: "Inter, sans-serif",
    },
    border: {
      radius: 8,
    },
    spacing: {
      padding: 12,
    },
  },
  {
    name: "dark",
    colors: {
      background: "#121212",
      text: "#ffffff",
      primary: "#00bfff",
    },
    typography: {
      fontSize: 16,
      fontFamily: "Inter, sans-serif",
    },
    border: {
      radius: 8,
    },
    spacing: {
      padding: 12,
    },
  },
];

Set Up Context

// theme-context.tsx
import { createContext, useContext, type ReactNode } from "react";
import { Theme } from "theme";

interface ThemeContextProps {
  theme: Theme;
}

interface ThemeProviderProps {
  children: ReactNode;
  currentTheme: string;
  themes: Theme[];
}

const ThemeContext = createContext<ThemeContextProps | undefined>(undefined);

export const ThemeProvider = ({ children, currentTheme, themes }: ThemeProviderProps) => {
  const theme = themes.find((t) => t.name === currentTheme);

  if (!theme) {
    throw new Error(`Theme "${currentTheme}" not found.`);
  }

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

export const useTheme = (): Theme => {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error("useTheme must be used within a ThemeProvider");
  }
  return context.theme;
};

Instead of using a global state library like Redux or Zustand for theming, which is often overkill and introduces unnecessary complexity, React’s built-in Context is a simple and scalable solution. It avoids prop drilling while remaining lightweight and directly tied to the component tree.

Use the Theme in Components

// Button.tsx
import { useTheme } from "./theme-context";

const Button = () => {
  const theme = useTheme();

  return (
    <button
      style={{
        backgroundColor: theme.colors.primary,
        color: theme.colors.text,
        borderRadius: `${theme.border.radius}px`,
        fontFamily: theme.typography.fontFamily,
        padding: `${theme.spacing.padding}px`,
      }}
    >
      Themed Button
    </button>
  );
};

This inline styling approach keeps components self-contained and portable across applications and teams.

Why This Works

This architecture solves several problems:

  • Avoids tightly coupled CSS overrides and class names
  • Prevents over-engineering by skipping global state libraries
  • Keeps styling colocated with logic
  • Enables easy onboarding for new devs
  • Makes your themes composable and testable

Integrating with UI Libraries

This setup is library-agnostic and can be injected into UI libraries like MUI or Mantine. You can:

  • Pass theme tokens directly into their theme providers
  • Map your config to their design systems
  • Avoid rewriting theme logic just to match external tools

Once you define your theme structure well, it becomes your single source of truth. Everything else, shadows, animations, variants, can be mapped from it.

Scaling Across Projects

This setup can be extracted into a separate package and:

  • Reused across multiple applications
  • Versioned and published to a private registry
  • Integrated easily in a monorepo workflow
  • Updated in one place, reflected everywhere

By treating theming as a reusable service, you reduce duplicated effort and enforce consistency across products.

Final Thoughts

A good theming setup should:

  • Be decoupled from tools and CSS class names
  • Support multiple design directions (playful, corporate, minimal)
  • Be flexible enough for use in any UI library or project

When approached as just a configuration object, theming becomes one of the easiest parts of your front-end architecture to scale and share.

You’ll spend less time wrestling with overrides and more time crafting pixel-perfect UI.

Happy theming!

We Build Digital Products That Move Your Business Forward

locale flag

en

Office Locations

India

India

502/A, 1st Main road, Jayanagar 8th Block, Bengaluru - 560070

France

France

66 Rue du Président Edouard Herriot, 69002 Lyon

United States

United States

151, Railroad Avenue, Suite 1F, Greenwich, CT 06830

© 2025 Surya Digitech Private Limited. All Rights Reserved.