Telerik blogs

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.

Coffee Warehouse dashboard Sample app built with KendoReact and Kendo 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.

Sample KendoReact app with diagrams to illustrate theme - primary calls to action have ff6358 orange color, all text shares the same Roboto font, background is consistent fff white color, secondary background is fafafa gray color

We can see that:

  • Primary call to actions (i.e., buttons) have an orange color (#FF6358).
  • Secondary call to actions (i.e., buttons) have a gray color (#F3F3F3).
  • The primary background is a consistent plain white color (#FFF).
  • A secondary background is applied in certain areas with a consistent grey color (#FAFAFA).
  • All text shares the same “Roboto” font.

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.

Components

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

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.

Dynamic Inline Styles

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.

  • It’s difficult to apply simple CSS selectors like :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.
  • Markup (i.e., HTML structure) of a component can be hard to read if a component has a large number of style properties defined inline.
  • There are also performance implications:
    • With inline styles, the browser spends more time in scripting because it has to map all the style rules passed into the component to actual CSS rules.
    • Without using an external CSS stylesheet file, the browser can’t cache the styles being used in our component. Caching can often help with browser page performance.

Styled-components

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 is still no longer a separate document but instead part of our JavaScript files.
  • Styles on their own can’t be shared easily with other projects since they’re colocated within React components.

CSS Variables

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.

  • We no longer have to fully rely on JavaScript to conditionally dictate the styles and theme of our components.
  • We don’t have to leverage and use the Context API or some other capability to ensure our components have access to theme-style data.
  • If we implement functionality to allow a user to change the application theme, and when this occurs, we won’t have multiple components in our component tree re-render to reflect the change (which would always occur with a CSS-in-JS approach). This could lead to a performance improvement for large applications with many components.

By avoiding the use of CSS within JS, we can also leverage different ways to help structure the CSS of our entire app.

BEM

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);
}

SASS

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.

CSS Modules

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:

  • Live in one place.
  • And only apply to that 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.

Theming in KendoReact

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.

  • The Default theme available through the @progress/kendo-theme-default npm package.
  • The Bootstrap theme available through the @progress/kendo-theme-bootstrap npm package.
  • The Material theme available through the @progress/kendo-theme-material npm package.

(The Progress team is also adding a Fluent theme, supported in Figma Kits and ThemeBuilderread 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.

Wrap-up

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!


About the Author

Hassan Djirdeh

Hassan is currently a senior frontend engineer at Doordash. Prior to Doordash, Hassan worked at Instacart and Shopify, where he helped build large production applications at-scale. Hassan is also a published author and course instructor and has helped thousands of students learn in-depth fronted engineering tools like React, Vue, TypeScript and GraphQL. Hassan’s non-work interests range widely and, when not in front of a computer screen, you can find him at the gym, going for walks or running through the six.

Related Posts

Comments

Comments are disabled in preview mode.