Theming in Components with React and Bit

su
sunilsandhu2 months ago

Introduction

One of the big organisational problems facing engineering and design teams alike is decoupling the look and feel of an application from the underlying logic and system. This becomes a real challege for applications that have been created as one big monolith.

A common attempt to separate UI from system is to create a component library. However, what often happens is the component library gets created as another monolith of components. Attempts to decouple themes from systems is noble, but attempting to achieve this by combining monoliths is often not the best solution.

At Bit, we have a system in place for creating themes that is infinitely scaleable and composable. We are able to make system-wide UI changes in just a few minutes - this is incredibly powerful. By following the same design pattern we will be explaining in this post, you can very easily, and very quickly, update themes across a range of products and use-cases, in a way that is scaleable and composable.

Following this pattern makes it possible to do things like this:

A theme is a context

Themes create a uniform design language that can be used across all components, sites, applications, etc. This unifies an ecosystem and frees up design teams to tackle larger problems.

But whether you are thinking of a component with regard to a UI element, or a concrete application, theming is a context-specific problem.

In other words, we can build an application, but the look and feel of the application will vary dependent on who it is for, how we wish to present information, and so on. A form may be made up of a certain set of HTML element, such as <label>, <input>, and so on, and the form field validation may differ, dependent on the type of information being collected. But if we created a form 100 times, the logic would be the same, but the UI would likely differ depending on where it is being used.

When we decouple the user interface from component functionality, we can make wholesale changes to the UI in a scaleable, consistent, and efficient way. On top of that, we encourage ourselves to create components that are reusable - that form component can now be used in an infinite amount of other applications. Before, we would have created the same form logic over and over again. Now, we will have multiple themes, but only one form component.

How the Bit team solve the issue of theming

At Bit, we handle theming with contexts. For this, we make use of a ThemeProvider component and a ThemeContext component. This is the same design pattern that React recommends when dealing with features you want to be made available throughout the system.

This design pattern can be used for a range of use cases, such as handling localization, creating state, theming, and so on.

Themes provide a way to decouple UI from functionality. React provides an API for turning code into components. Bit turns components into reusable, composable Lego. This is a powerful combination of technologies.

Context is a best practice

Theoretically, we could handle theming with things such as prop drilling and global variables, but both of these would be bad practices (or anti-patterns). Context is the best way to approach context-specific problems.

How to create a theme context

The first step here is to create a theme. As we mentioned earlier, it makes sense for your first theme to be the base theme for your application/s. Here, we take our set of base theme design tokens and package them as a component.

When we work with design tokens, we create a single source of truth that can be used to create themes - starting with the first theme which is typically the base (default) theme.

We have covered design tokens in great detail in this post, so go check that our for a better understanding.

But to complete the loop, you should expect your BaseTheme to look something like this:

import { BaseThemeSchema } from './base-theme-schema';

/**
 * design tokens go here!
 * these are maintained by the designer.
 */
export const baseThemeDefaults: BaseThemeSchema = {
  backgroundColor: '#FFFFFF',
  onBackgroundColor: '#2B2B2B',
  onBackgroundLowColor: '#9598A1',
  onBackgroundMediumColor: '#707279',
  onBackgroundHighColor: '#2B2B2B',

  primaryColor: '#6C5CE7',
  onPrimaryColor: '#FFFFFF',

  borderMediumColor: '#EDEDED',
  borderMediumHoverColor: '#CECECE',
  borderMediumFocusColor: '#C6C6C6',
  borderMediumActiveColor: '#AFAFAF',

  borderHighColor: '#BABEC9',
  borderHighHoverColor: '#A3A6B0',
  borderHighFocusColor: '#9DA1A9',
  borderHighActiveColor: '#8C8F96',

  borderPrimaryColor: '#6C5CE7',
  borderPrimaryHoverColor: '#8376EB',
  borderPrimaryFocusColor: '#897DEC',
  borderPrimaryActiveColor: '#8F83ED',

  surfaceColor: '#FFFFFF',
  surfaceHoverColor: '#EDEBFC',
  surfaceActiveColor: '#DCD8F9',
  surfaceFocusColor: '#E2DEFA',

  onSurfaceColor: '#2B2B2B',
  onSurfaceMediumColor: '#707279',
  onSurfaceLowColor: '#9598A1',
  // ...more values
Copied

We now have a theme created. The next step is to consume the theme with a Theme Provider.

How to create a Theme Provider

Theme Providers provides a way to set a context that can be accessed and applied throughout an application. A good theme provider should be able to do the following:

  1. Generate a base theme, ideally from a set of design tokens;
  2. Apply that base theme;
  3. Provide an API (usually with props) for overriding the base theme.

If you like, you can go ahead and check out our open sourced ThemeProvider. This is the same theme provider we use to power theming across all of our products.

To complete the loop, the below code is typically what a theme provider looks like:

import { createTheme } from "@teambit/base-react.theme.theme-provider";

/* generate a theme with the values from our theme context */
const { useTheme, ThemeProvider } = createTheme<BaseThemeSchema>({
  theme: baseThemeDefaults,
});

/* create a theme schema to standardize future customized themes */
export type ThemeSchema = typeof defaultDesignTokenValues;
Copied

How to apply a theme to a React component

It's smooth sailing from here.

We can now take the component we wish to theme, and wrap it with our ThemeProvider component.

<Theme.ThemeProvider>
  <button>This is a button</button>
</Theme.ThemeProvider>
Copied

The baseTheme values will now be applied to the button.

How to override a theme

To override a theme, we first need to create another theme.

Here we can follow the same steps we mentioned earlier in the 'How to create a theme context' section. But instead of creating another Base theme, we would create another theme, using the same set of design tokens.

For example, if our Base theme was: {backgroundColor: #FFFFFF} our new theme might be {backgroundColor: #FF0000}.

Then, we can make use of the overrides prop in our ThemeProvider component to replace the base theme value for backgroundColor with the value in the new theme.

<Theme.ThemeProvider overrides={newTheme}>
  <button>This is a button</button>
</Theme.ThemeProvider>
Copied

Benefits of this approach

We now have an approach to theming that:

  1. promotes consistency;
  2. is infinitely scaleable;
  3. follows best practices;
  4. avoids React anti-patterns such as prop-drilling and global variable creation;
  5. gives us confidence in our system;
  6. saves time, money, and resource.

When we take this approach to theming, and we power it with tools such as Bit's Ripple CI, an update to a theme can propagate throughout all of the systems that make use of it. Design teams can make updates to design tokens, developers can switch one theme for another, and everything updates seemlessly. And with all this, developers no longer have to go through applications with a fine-tooth comb to look for UI discrepancies - anyone who has had to do this before knows how painful and time-consuming this process can be.

With UI decoupled from the system, teams can move fast and update themes by updating the value of one prop in one component. When we wanted to add a dark theme to The Bit Blog, we were able to take the same ThemeProvider and DarkTheme components used on the Bit Docs site, and add it to the blog within minutes.

Now that themes are distributed, fixing issues becomes easier. Instead of finding and fixing a line of CSS amongst thousands of lines of code that is coupled in a monolithic structure, dev teams can fix something that may now exist inside of 10 lines instead. Mental overhead becomes more palatable.

Conclusion

After reading this, I hope you now have confidence in a better way to handle theming of components with React, and have the building blocks to be able to go and implement this. While everything we have discussed can be created without Bit, building components in a Bit workspace actively promotes best practices. The ability to 'plug' in themes like Lego bricks is made easy by using Bit. Speaking as a developer, it's by far one of the best dev experiences I have had.