React(ive) Styling on Steroids
My approach to implementing not just themes in React, but also to allowing users to customize any css property of any component they'd like
August 14, 2021

Feel free to check out the demo on which this tutorial is based, Custom Themes, and its associated GitHub repo.
- To see changes made to your custom theme via settings page, make sure to select "userPreference" as your viewing choice.
- Heads up, this demo is running on a free Heroku dyno, so give it a bit to load and refresh after possible timeout.
There are many standard ways to add themes to a website, but when your vision is unique, your approach must be as well.
The user story I've fantasized about since I wrote my first style rule looks like this:
As a user, I can make my own theme.
Choosing between preset viewing experiences is cool, but what's even cooler is being able to look at the components of a website and say hey...
I don't want all the colors to go from light to dark, but I do want the borders of my comments to be this shade of blue and the backgrounds of the form fields to be this shade of gray.
I don't want all the fonts to be larger, but I do want this text to be this size, that text to be that weight, and those words to remain the same size but have more spacing.
I don't care about fonts and colors at all, but I'd like to see my photo albums layed out vertically instead of horizontally while on desktop.
This level of flexibility might sound extreme and/or extremely unnecessary, but I became passionate about implementing the feature when designing my personal website. I wanted to be able to meet with experienced designers for styling guidance and not have to go back to the source code to change things. Someone I respect could review my portfolio and say 'I think the font-size should be bigger on the project titles,' or provide any of the feedback described in the bullets above, and in real time we could see the change from the settings UI immediately.
Without further adieu... here's how you can do it, too!
<Icon />
Let's work backwards and imagine that I'm the image component that's rendered every time a forecast is displayed in a weather app.
My name is Icon, but you know I can be a sun or rain icon and not a facebook or twitter icon because I live in src/components/weather/WeatherIcon.js. Those social media icons might be instances of a different component by the same name of Icon, but my path
in conjunction with my name
uniquely identifies me.
The developer decided that my width and height will always be 50px and my opacity would always be 80%, but what qualifies me as one of the customizableComponents
that share my path
(i.e. come from the same file) is my border and background-color. The values of those css properties depend on Icon_props["border"]
and Icon_props["background-color"]
, which are provided to me at runtime when I'm first rendered or re-rendered following a theme change.
styleSeed
and StyleContext
Fortunately for me, there is a piece of state called StyleContext
that stores all the styles within the application that can be altered on demand.
StyleContext
is initialized by calling createContext(styleSeed)
. The argument styleSeed
contains definitions for my border and background-color props. And because StyleContext.Provider
wraps the entire application and passes useContext(StyleContext)
as a value
, those css property values in styleSeed
are accessible to any component in any file, including me (I'm an <Icon />, in case you forgot).
Here's just me in styleSeed
after a user specified what they would like my border to look like should they select the userPreference theme as the currentTheme
:
{
name: 'Icon',
border: {
default: '1px solid black',
userPreference: '3px dotted blue',
minimalTheme: '0px solid black',
superTheme: '8px solid crimson'
},
"background-color": {
default: 'lightblue',
userPreference: null,
minimalTheme: 'transparent',
superTheme: 'orange'
},
}
And here's me in the same state in the list of customizableComponents
that come from the my path
:
{
path: 'src/components/weather/WeatherIcon.js',
customizableComponents: [
{
name: 'Icon',
border: {
default: '1px solid black',
userPreference: '3px dotted blue',
minimalTheme: '0px solid black',
superTheme: '8px solid crimson'
},
"background-color": {
default: 'lightblue',
userPreference: null,
minimalTheme: 'transparent',
superTheme: 'orange'
},
},
{
name: 'Label',
"padding-right": {
default: '0.5rem',
userPreference: '1rem',
minimalTheme: '0.1rem',
superTheme: '3rem'
},
color: {
default: 'white',
userPreference: 'white',
minimalTheme: 'black',
},
}
]
}
And here's me and my sibling <Label /> in an extremely slimed down demo version of styleSeed
const styleSeed = [
{
path: 'src/components/social/SocialIcon.js',
customizableComponents: [
{
name: 'Icon',
width: {
default: '25px',
userPreference: '2rem',
minimalTheme: '15px',
superTheme: '50px',
},
height: {
default: '25x',
userPreference: '3.5rem',
minimalTheme: '15px',
superTheme: '50px',
},
},
],
},
{
path: 'src/components/weather/WeatherIcon.js',
customizableComponents: [
{
name: 'Icon',
border: {
default: '1px solid black',
userPreference: '3px dotted blue',
minimalTheme: '0px solid black',
superTheme: '8px solid crimson',
},
'background-color': {
default: 'lightblue',
userPreference: null,
minimalTheme: 'transparent',
superTheme: 'orange',
},
},
{
name: 'Label',
'padding-right': {
default: '0.5rem',
userPreference: '1rem',
minimalTheme: '0.1rem',
superTheme: '3rem',
},
color: {
default: 'white',
userPreference: 'white',
minimalTheme: 'black',
},
},
],
},
];
Note: There is much more to write here about how I passed props to styled-components in order to implement this strategy, and also how I automated the creation of a form that uses ccs-validator to check user input for proper values. However, the system I began to describe in this article needs improvement in terms of requiring less manual copy/pasting to scale, so I'll leave things here for now and update this post after refactoring.
Till then, here is a glimpse of how the above styleSeed can be harvested to change top-level style properties based on a user's current theme choice.
import { StyleContext } from 'components/providers/ThemeProvider';
import { useContext } from 'react';
import styled from 'styled-components';
import { getStyledCommands } from 'utils/theme-helper';
const relativePath = 'src/components/layout/GlobalStyle';
const styledComponentNames = ['GlobalContainer'];
const GlobalContainer = styled.div`
font-size: ${(props) => props.GlobalContainer_props['font-size']};
font-family: ${(props) => props.GlobalContainer_props['font-family']};
color: ${(props) => props.GlobalContainer_props['color']};
background: ${(props) => props.GlobalContainer_props['background']};
padding: ${(props) => props.GlobalContainer_props['padding']};
min-height: 100vh;
display: grid;
grid-template-rows: auto 1fr auto;
max-width: 100vw !important;
overflow-x: hidden;
`;
const GlobalContainer_props = {};
function GlobalStyle(props) {
const { styles, themes } = useContext(StyleContext);
const styledCommands = getStyledCommands(
styles,
themes.currentTheme,
styledComponentNames,
relativePath
);
for (let i = 0; i < styledCommands.length; i++) {
try {
// eslint-disable-next-line no-eval
eval(styledCommands[i]);
} catch (error) {
console.error(error.message);
}
}
return (
<GlobalContainer GlobalContainer_props={GlobalContainer_props}>
{props.children}
</GlobalContainer>
);
}
export default GlobalStyle;
Also, here is the reducer that updates the user's custom theme based on form data.
const updateStyleStateFromCustomForm = (formData, styles) => {
const updatedStyle = [...styles];
updatedStyle.map((file) => {
file.customizableComponents.map((comp) => {
Object.keys(comp)
.filter((e) => e !== 'name')
.map((property) => {
const newProp = formData[file.path][comp.name][
property
].includes('__proto__')
? 'nice try'
: formData[file.path][comp.name][property];
comp[property].custom = newProp;
return property;
});
return comp;
});
return file;
});
return updatedStyle;
};