Theming with React, Less and CSS variables
Tags: react, less, css, javascript
Published: 2021-04-25 15:32:43


The styles written in less are compiled to generate a CSS file, so Less variables get converted to their values in the output. To switch a theme dynamically in the browser we'll need to change the color values on the fly, this is where CSS variables come in.

[CSS variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties) can be declared and used in less files, we can change the variable value or swap variable definition in the browser and it's as easy as changing an element's class name.

Let's set up a basic react page to understand theming. The task can be broken down in to
- Theme Context and Wrapper
- Header 
- Card with some text/images
- Theme toggle button
- light and dark theme variables
- CSS for the above components

Create a theme [context](https://reactjs.org/docs/context.html) and a wrapper component to make them available to the app.
```js
const LIGHT_THEME = 'light-theme';
const DARK_THEME = 'dark-theme';
const ThemeContext = React.createContext();

// wrapper to make theme and changeTheme available 
// down the tree
function ThemeWrapper({ children }) {
  const [theme, setTheme] = React.useState(LIGHT_THEME);
  
  const applyTheme = (newTheme) => {
    // TODO: apply new theme on app
    setTheme(newTheme);
  }
  
  return (
    <ThemeContext.Provider value={{ theme, applyTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}
```

The card component
```js
function Card() {
  const { theme } = React.useContext(ThemeContext);
  return (
    <div className="card"> Applied theme: {theme} </div>
  );
}
```

Theme toggle button
```js
function ToggleTheme() {
  const { theme, applyTheme } = React.useContext(ThemeContext);
  
  const altTheme = theme === LIGHT_THEME ? DARK_THEME : LIGHT_THEME;
  
  const toggle = () => {
    applyTheme(altTheme);
  }
  
  return (
    <div className="toggle-theme"> 
      <button onClick={toggle}>Go {altTheme}</button> 
    </div>
  )
}
```

The parent App wraps children with ThemeWrapper
```js
function App() {
  return (
    <div id="app" className="light-theme">
      <div className="header"> Theme Sandbox </div>
      <ThemeWrapper>
        <div>
         <ToggleTheme />
         <Card />
        </div>
      </ThemeWrapper>
    </div>
  );
}

// mount to html
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
```

HTML just needs a root element
```html
<div id="root"></div>
```

Now let's define some essential colors for our two themes. I mixed a few palettes from [colorhunt](https://colorhunt.co) to get these.

We'll define two colors each for font, background, and border - a primary and a secondary. The themes will be defined as classes and to apply a theme we just need to apply the corresponding class.

```css
.light-theme {
  --primary: #02475e; 
  --secondary: #194350;
  --primaryBackground: #f9f3f3;
  --secondaryBackground: #d8e3e7;
  --primaryBorder: #000;
  --secondaryBorder: #333;
}
.dark-theme {
  --primary: #f9f3f3;
  --secondary:#dddddd;
  --primaryBackground: #151515;
  --secondaryBackground: #301b3f;
  --primaryBorder: #3c415c;
  --secondaryBorder: #b4a5a5;
}
```

Write styles for the rest of the items using the above variables
```css
#app {
  color: var(--primary);
  background-color: var(--primaryBackground);
  width: 100%;
  height: 100%;
  position:absolute;
}

.header {
  text-align: center;
  font-size: 1.5em;
  margin: 10px 0px 20px 0px;
}

.toggle-theme {
  position: absolute;
  right: 10px;
  top: 5px;
}

.card {
  color: var(--secondary);
  background-color: var(--secondaryBackground);
  border: 1px solid var(--secondaryBorder);
  width: 300px;
  height: 300px;
  margin: auto;
  padding: 5px;
}
```

In the app component, I have specified "light-theme" as the class, so the variables defined by our light theme would be available to the components below. Changing the theme would just mean switching the class assigned to the App component. Let's add that action to ThemeWrapper
```js
const applyTheme = (newTheme) => { 
  document.getElementById('app').className = newTheme;
  setTheme(newTheme);
}
```

The output,
{% codepen https://codepen.io/kailash-sankar/pen/bGgzeQw default-tab=result %}


Now that it's working, time to prettify the page a bit. Change the button to a switch, add some icons, font and tweak the styles to get:
{% codepen https://codepen.io/kailash-sankar/pen/qBRgYVW default-tab=result %}

Theme Context is not required to do theming if the requirement is only to change the CSS variables. But a context is useful to have the theme selection available everywhere, there might be external components where you have to pass in the theme or for taking actions based on a theme(styled-components).

That's all folks :)