Theming with React and Sass

Jason McAffee
4 min readJul 21, 2019

--

In this post we’ll explore using features in sass and react that can help us encapsulate, organize, and simplify theme logic and styling within our web applications.

By using this approach we aim to:

  • Encapsulate theme styling within component css classes.
  • Ensure maximum flexibility in how a component can be styled for a given theme.
  • Minimize JavaScript usage. (JS is optional. We could avoid js and instead use checkbox inputs and the :checked pseudo-class aka the “checkbox-hack”)

Structuring the HTML

First let’s put together a simple outline of how our html will be structured.

We need to structure our html so that the element with the theme class assigned to it is an ancestor of any themed content descendants. i.e. The themed content element lives somewhere inside an element with the theme class.

<div class="theme-name">
<div class="themed-content">...</div>
</div>

Styling Using the CSS Descendant Combinator

Next we can target our nested element using the descendant combinator. The descendant combinator allows us to select and style by combining selectors, where the first selector selects the ancestor, and the second selector selects the descendant.

Example CSS

A descendent element (an element nested at any level/depth inside of an ancestor) with the class name “themed-content” can be selected/styled whenever an ancestor element has a class named “dark-theme” or “light-theme” assigned to it.

This gives us conditional styling of an element based on a parent/ancestor’s class name.

//common styling for all themes
.themed-content {
padding: 20px;
}
//applied when .themed-content is a descendant of .dark-theme
.dark-theme .themed-content {
background-color: #007dd1;
}
//applied when .themed-content is a descendant of .light-theme
.light-theme .themed-content {
background-color: #00c8ff;
}

Output

When an ancestor element has “dark-theme” assigned to it, we get the dark blue background we defined for “themed-content”:

<div class="dark-theme">
<div class="themed-content">
Example themed content.
</div>
</div>

When an ancestor element has “light-theme” assigned to it, we get the light blue background we defined for “themed-content”:

<div class="light-theme">
<div class="themed-content">
Example themed content.
</div>
</div>

Simplifying the CSS using SASS

The parent selector in sass, denoted by the “&” character, allows us to reference the parent/ancestor selector from within a declaration block. It is typically referenced in conjunction with another selector or pseudo element.

For example, we can generate this css:

.dark-theme .themed-content {
background-color: #007dd1;
}
.light-theme .themed-content {
background-color: #00c8ff;
}

Using this scss:

.dark-theme {
& .themed-content {
background-color: #007dd1;
}
}
.light-theme {
& .themed-content {
background-color: #00c8ff;
}
}

Organizing by Descendant

It’d be a bit nicer if we could keep our generated css the same, but organize all styling for .themed-content inside of one declaration block. We can accomplish this by placing the “&” after the ancestor selector, from within .themed-content’s declaration block:

.themed-content {  .dark-theme & {
background-color: #007dd1;
}
.light-theme & {
background-color: #00c8ff;
}
}

Simplifying by using a Mixin

We can simplify our syntax, and ensure we always use appropriate theme names, by defining a mixin for each of our themes:

@mixin darkTheme(){
.dark-theme & {
@content;
}
}
@mixin lightTheme(){
.light-theme & {
@content;
}
}
.themed-content{ @include darkTheme(){
background-color: #007dd1;
}

@include lightTheme(){
background-color: #00c8ff;
}
}

React

Now all we have left to do is to dynamically assign a class name based on the theme selected by the user. For simplicity’s sake, we will use react hooks to dynamically set the className of the Theme component whenever the user selects a theme option from the ThemeSelector component.

App Component

Let’s wire up our theme hook and compose our various components in a root component named App:

//config
const themeNames = { dark: `dark-theme`, light: `light-theme` };
function App(){
const [themeName, setThemeName] = useState(themeNames.dark);

return <Theme themeName={themeName}>
<ThemeSelector setThemeName={setThemeName} themeNames={themeNames}/>
<ThemedContent/>
</Theme>;
}

Theme Component

We’ll want a container component that has a dynamic className property that changes every time a new option is selected from the selector in ThemeSelector:

function Theme({ children, themeName }){
return <div className={themeName}>{children}</div>;
}

ThemeSelector Component

Next we define a select element with an option for each entry found in the themeNames object. The entry’s key is used as the display text, and entry’s value is used as the option value that is passed as e.target.value on change, which is then passed to the setThemeName function.

function ThemeSelector({ setThemeName, themeNames }){
return <div>
<label>Theme: </label>
<select onChange={(e)=> setThemeName(e.target.value) }>
{ Object.entries(themeNames).map( ([key, value]) => <option value={value}>{key}</option>) }
</select>
</div>;
}

ThemedContent Component

Now all we need to do is define our components. Here’s an example component that has theme styling. This is how we would define themed components like buttons, lists, sections, etc.

//example component
function ThemedContent(){
return <div className="themed-content">Example themed content</div>;
}

Codepen Demo

Other Combinators

We could also use combinators other than descendant, like child, adjacent sibling, and general sibling selectors, which would allow us to structure our html in different ways, if needed. In general, the descendant combinator provides the most flexibility, as it targets any descendants within the targeted ancestor, regardless of how deeply they are nested.

--

--