In this article, we’ll discuss different approaches to establishing a consistent UI theme in a React application and the benefits/trade-offs of each approach.
Many large-scale applications we use today tend have a certain theme associated with them. Theme, in this context, can be explained as the branding or design of an application that adheres to certain design principles. This involves consistent use of typography, color, patterns and layout to establish a coherent design structure.
To illustrate this further, here’s an example of a sample app built with KendoReact components and Kendo’s Default theme.
By taking a glance at the above sample app, you’ll immediately notice there’s a level of consistency with the layout and styles for all the UI elements being presented. In other words, there’s a consistent theme in the app.
We can see that:
Establishing a design theme is important because, as a developer working among a team of developers and designers, we want a reusable and structured manner to ensure we build UI elements (e.g., buttons, links, text, etc.) that follow a consistent theme. Furthermore, understanding how to implement a theme helps us further understand how to customize and modify themes available to us from frameworks like KendoReact.
We’ll come back to discussing the KendoReact library and Kendo UI themes at the end of this article. Before that, we’ll discuss some simple ways one can implement a consistent theme within a React app.
Building and using components in our app is the first step for us to introduce theming. This is because components are self-contained elements where we can group markup (HTML), styles (CSS) and JavaScript. We can build components for each of the distinct UI elements we want to show in our app.
ui-components/
Button/
Button.jsx
Card/
Card.jsx
Link/
Link.jsx
Menu/
Menu.jsx
These components can be reused anywhere and still maintain the same structural appearance.
const Button = ({ onClick, children }) => {
return (
<button className="button" onClick={onClick}>
{children}
</button>
);
};
const ParentComponent = () => {
// we can use the <Button /> component to render the same expected <button /> element
return <Button>Click Me!</Button>;
};
However, there is one aspect of these components that we would want to be modifiable/extensible and that is the theme! To facilitate this, we can create a theme
object that contains the specific style properties we would want as part of our
theme.
const theme = {
light: {
foreground: "#000000",
background: "#eeeeee",
primary: "#0092e3",
font: "Nunito",
},
dark: {
foreground: "#ffffff",
background: "#222222",
primary: "#0017e3",
font: "Nunito",
},
};
To have our components use these styles, we’ll need to have this theme
object available in all our components. We would most likely want to avoid prop-drilling this theme context/data down to all our child components from the parent
component since this would make things hard to maintain as our application scales.
We could use some sort of shared store (like Redux) where the theme state data is kept and then pushed to components.
However, one of the more appropriate ways of passing theme data to all our components can be done by leveraging React’s Context API.
Context in React provides a way to pass data through a component tree without the need to prop-drill (i.e., pass props down manually at every level).
We first create the Context object with the createContext()
function. As we create the Context object, we can also specify the default context value in the createContext()
function.
import React, { useState, useContext, createContext } from "react";
import { theme } from './theme'
const ThemeContext = createContext({ themes.light });
We can then wrap the parent <App>
component with the Context provider to have all child components subscribe to Context changes.
import React, { createContext } from "react"
import { Button } from './ui-components'
import { theme } from './theme'
const ThemeContext = createContext({ themes.light });
function App() {
return (
<ThemeContext.Provider value={themes.dark}>
{/* a child component to <App /> */}
<Button />
</ThemeContext.Provider>
);
}
We can then leverage the useContext()
hook to retrieve the current context value in our child components.
import { useContext } from "react";
function Button() {
const theme = useContext(ThemeContext);
// we have access to theme styles in our component
}
With our component now able to access the styles from the theme
object, we can implement these styles in our component markup in a few different ways.
Since React components allow us to build the markup of the component within a JavaScript setting, we can use the style
attribute to add dynamically computed styles at render time.
import { useContext } from "react";
function Button() {
const theme = useContext(ThemeContext);
return (
<button
style={{ background: theme.light.background, color: theme.light.primary }}
>
I am styled by theme context!
</button>
);
}
This approach is easy to implement and easy to get started with. However, it’s not recommended for a variety of reasons.
:hover
, :focus
and :active
since we
would have to rely on state changes to apply these features. Furthermore, we can’t apply media-queries since dynamic inline styles with JS is using a plain JavaScript object.The styled-components (i.e., CSS-in-JS) library can help us to leverage template literals to write actual CSS to style React components. It’s sort of like implementing inline styles with JS but on steroids.
When the styled-components/
library is installed, we can define our component styling as follows.
import { useContext } from "react";
import styled, { css } from "styled-components";
// styled button component that receives theme as a prop
const StyledButton = styled.button`
${(props) =>
css`
background: ${props.theme.light.background};
color: ${props.theme.light.primary};
`}
`;
import { useContext } from "react";
function Button() {
const theme = useContext(ThemeContext);
return (
<StyledButton theme={theme}>I am styled by theme context!</StyledButton>
);
}
Note the
styled-components/
library has a more concrete capability for introducing theming support which you could read about here. I’ve only shared a simple example here.
The advantage of using styled-components
over defining dynamic inline styles is that we avoid a lot of the
issues we see with inline styles in JavaScript. Our styles get compiled as actual CSS and we can use CSS features (like @media-queries
) and selectors (like :hover
, :focus
, :active
).
However, some potential concerns with using styled-components
are:
CSS variables (i.e., custom CSS properties) can help us achieve conditional styling all within the context of CSS! As the name suggests, CSS variables allow us to store CSS values in properties that can be reused in a CSS document.
Instead of defining our theme styles in a theme
object, we can create theme CSS properties that encapsulate the CSS variables for our theme. We can define these properties in CSS classes.
.light-theme {
--foreground: "#000000";
--background: "#eeeeee";
--primary: "#0092e3";
--font: "Nunito";
}
.dark-theme {
--foreground: "#ffffff";
--background: "#222222";
--primary: "#0017e3";
--font: "Nunito";
}
We can then apply one of these classes in the parent markup of our root <App />
component:
import React from "react";
import { Button } from "./ui-components";
import "./styles";
function App() {
return (
<div id="app" className="light-theme">
<Button />
</div>
);
}
If we ever wanted to allow a user to replace or toggle a defined theme, we can introduce that capability as well.
import React, { useState } from "react";
import { Button } from "./ui-components";
import "./styles";
function App() {
const [theme, updateTheme] = useState("light-theme");
const toggleTheme = () => {
if (theme === "light-theme") {
updateTheme("dark-theme");
} else {
updateTheme("light-theme");
}
};
return (
<div id="app" className={theme}>
{/* <Button /> onClick() handler triggers the toggleTheme() function */}
<Button onClick={() => toggleTheme()} />
</div>
);
}
In our child components, we no longer have to define our theme capability within the context of our JavaScript code. Instead, we can have CSS classes be defined that derive values for its properties from our theme CSS variables. We can access the values
of our CSS variables by using the var()
keyword.
Here’s an example of defining a .button
class where the values of the background
and color
properties are the values of the CSS variables defined in our root CSS file.
.button {
background: var(--background);
color: var(--primary);
}
import React from "react";
import "./styles";
function Button() {
return (
<button className="button">I am styled by theme CSS variables!</button>
);
}
There are some helpful advantages to using CSS variables over a CSS-in-JS approach.
By avoiding the use of CSS within JS, we can also leverage different ways to help structure the CSS of our entire app.
As an example, we can avoid issues with CSS specificity by following a methodology to have each of our CSS classes named in a very particular manner—block__element--modifier
. This is often recognized as the BEM methodology.
.button {
background: var(--background);
color: var(--primary);
}
.button--disabled {
background: var(--disabled);
color: var(--disabled);
}
.button--error {
background: var(--error);
color: var(--error);
}
Or, we can use a CSS preprocessor like SCSS (or SASS) which can help extend our CSS with “superpowers.”
.button {
background: var(--background);
color: var(--primary);
&.disabled {
background: var(--disabled);
color: var(--disabled);
}
&.error {
background: var(--error);
color: var(--error);
}
}
A preprocessor like SCSS/SASS provides useful features like nested rules, mixins, functions, etc. However, using a preprocessor like SCSS/SASS introduces another compile step since SCSS files need to be compiled to CSS.
Lastly, we could leverage CSS Modules. CSS Modules are CSS files where all class names are scoped locally by default. It’s a process in a build step (with the help of a tool like webpack) that changes class names and selectors to be scoped. It’s designed to fix the concern of the global scope issue in CSS.
With CSS modules, it’s a guarantee that all the styles for a single component:
import React from "react";
import ButtonStyles from "./ButtonStyles.css";
function Button() {
return (
<button className={ButtonStyles.Button}>
I am styled with a locally scoped ButtonStyle!
</button>
);
}
As mentioned above, to make CSS modules work, we need to use use a build process like webpack that has loaders such css-loader
and style-loader
.
We talked about some helpful ways we can introduce a consistent theme in our React app. However, writing, creating and producing robust CSS code for large applications take a lot of work. Presentation, appearance, responsiveness, accessibility and structure are a lot of things that need to be kept in mind when building frontend code.
This is where UI/CSS frameworks often come in. UI/CSS Frameworks are packages containing pre-written, standardized and often well-tested template and CSS code. They help speed up development, oftentimes provide a grid layout and help enforce good web design.
A great example of a robust UI/CSS framework is Progress’s own KendoReact library. KendoReact provides numerous out-of-the-box React components like:
KendoReact provides three themes that can be used for the entire component suite.
@progress/kendo-theme-default
npm package.@progress/kendo-theme-bootstrap
npm package.@progress/kendo-theme-material
npm package.(The Progress team is also adding a Fluent theme, supported in Figma Kits and ThemeBuilder—read more here.)
Like one of these themes but want to make some slight modifications? KendoReact supports this! We can customize theme variables directly in our application.
Here’s where the CSS variables for Kendo’s Default theme live:
https://github.com/telerik/kendo-themes/blob/develop/packages/default/scss/_variables.scss
Using this as a starting point, we can customize theme variables directly in our React app like the following:
// App.scss
// root SCSS file of our app
// this is where we override the $primary CSS variable
$primary: #ff69b4;
// this is where we import the SCSS for the default theme
@import "~@progress/kendo-theme-default/dist/all.scss";
Note that the above only works with the existence of a build process (e.g., with webpack) in our React app. For more information, refer to the KendoReact documentation’s Customizing Themes article.
By concluding this article, we should now have an understanding of the benefits of establishing a consistent theme in an application. We should also now recognize the different ways to introduce a theme in our React app and the benefits/trade-offs between each approach.
Lastly, KendoReact is a robust React/UI framework that provides more than 100+ components out of the box and allows us to choose between three already pre-established themes (Default, Material and Bootstrap). We also have the capability to fully customize any theme if we choose to do so. Consider using KendoReact if you plan on introducing a new design framework into your React app!
Hassan is a senior frontend engineer and has helped build large production applications at-scale at organizations like Doordash, Instacart and Shopify. Hassan is also a published author and course instructor where he’s helped thousands of students learn in-depth frontend engineering skills like React, Vue, TypeScript, and GraphQL.