I’m building an application using some of the most used technologies in the frontend scene: Typescript, ReactJS, Redux, Styled Components, ExpressJS and Webpack.
Time ago, I heard about an API that allows webapps to detect the OS dark mode, so you could adapt your styles to the user preferences, and well, I’m very curious, so you guess what happened :).
What do I want to achieve
So, before start coding, you’ll need to know what are my goals and what this post is about. I want my application to be able to switch to dark mode in runtime, so I need different sets of variables, and I need my components to generate their colors when they are rendered.
This will allow the user to switch its device Dark mode option without refreshing the page.
So, stuff that I need:
- A color palette: A list of variables where I will store all my hex colors and use some “decontextualized names”, such as
- A theme template: A list of “contextualized variables” where I will assign those colors to my application components, such as
- A theme selector: Of course, I need a way to select a theme and store it somewhere, so all my components can access to it and know what color to use. (spoiler alert: did I heard
- A color resolver: A function that, given the
selectedTheme, it will return the color value. In case the color doesn’t exist in selected theme, or no theme is selected, it will return some default values.
Creating a theme-engine
So, now that I have my ideas more or less clear, it’s time to translate it to code! Based on the statements I already mentioned, I would need these files to create my theme system:
themes/ // Contextualized color palettes
colors.ts // Decontextualized color palette
index.ts // Theme selector and color resolver
index.tsx // App's mount point
wrapper.tsx // Where I use my styled components
The color palette
theme/colors.ts is probably the simplest file. It’s just a list of variables containing hex values like these ones:
I think you can imagine the rest of the file :)
The theme template
You may think I could use
theme/colors.ts directly in my components. Well, I’ll tell you that if I use
GREY_3 in two different components, and I want to change one of them, probably I will modify
GREY_3 value and change the two components, wich is bad.
So you may think I could use variables such as
HEADER_BACKGROUND and assign them the hex values, like
const HEADER_BACKGROUND = '#f374ad'. And you are right, but this approach probably will lead me to have very similar colors and don’t realize about that, so my UI won’t look as much coherent as it could.
That’s why I consider I need a theme template :). Of course, this is a matter of taste, so feel free to do it as it best fits your needs.
Since there are colors that won’t change, such as branded colors (my own brand color, linkedin, instagram, etc), I decided to create a
theme/themes/common.ts file where I will expose those common colors, and reuse them in my themes. So, it’ll look like this:
Yes. That simple. It’s just a subset of my color palette, so I don’t have to specify them manually on each theme.
Then, the real theme templates (like
theme/themes/light.ts) will result in something like this:
The theme selector and color resolver
The last piece of my theme engine is
theme/index.ts that is the only file that will be used in the application, and it will provide 2 vital functions for this system to work.
Using the theme engine
Now that I have created my engine, I need two things:
- Store user preference in some place
- Read user preference in my components
Storing selected theme in React Context
So, for the first point, I’m using React context to store the selected theme, and make it accessible to my styled components. And, as I’m using
styled-components I can use the context they are providing for this purpose.
So, I’ll go to my app’s root point, and I will wrapp everything with this context, like follows:
You’ll notice that
theme prop in
ThemeProvider is an object. This is something
styled-component decided, and I think it can be useful if you want to apply different themes to different parts of your application. I’m just using one single theme at time, so that’s something anoying for me in certain moments, but it’s only cosmetics!
If you want the user to be able to change, you’ll need to provide the user the ability to change the
localStorage to set
theme=<selectedTheme>. I haven’t coded that functionality yet, but it shouldn’t be difficult.
We are about to finish! The only thing pending is to make our components able to read the context and apply the propper color. This is quite simple, since
styled-components is injecting the
theme property in our components (if we use the
ThemeContext they are providing)
So, the only think we need to do is to check that property in our components:
Now, I just need to use my styled components as usual. If the OS mode changes, or the
localStorage property changes, the application detect it on any state change (such as navigating), and will update the entire application theme to the new one!
I have to say that I’m very happy with the result. Probably there are different approaches with different advantages, but this is the one I think best fits my needs right now.
While developing this, I’ve found several pros and cons that you may want to know:
- Transparent to the user: In this example, I don’t provide the user a tool for selecting a theme. The app will match user’s OS’ selected theme, so the user doesn’t have to do anything. Just like magic. Despite this, the theme selector supports user preferences :)
- Easy to maintain: Since the theme templates are abstracted, you can easily change a component’s color from one single and located file, so updating your buttons color for a particular theme should take you less than 10s.
- Scalable: The example has been made with 2 themes, but you could add infinite themes to this system. In deed, you could even create variants, using object destructuring, to override only certain values of your main theme.
- Automated: Thanks to the color resolver, you don’t have to take care about conditionals or stuff like that. Also, thanks to styled-component’s
ThemeProvider, you don’t need to manually inject the selected theme to each styled component.
- Performance: Maybe setting a new context value on each navigation or state change is not the most performant thing in the world. If you are a performance-drama guy, or your application requires a top-level performance, probably, you should polish this approach.
- Color selection error: Since
getColorfunction accept any string as first parameter, you could misspell the color name you want to select. I solved this by adding
Typesto all the system, so now the function has autocomplete feature thanks to TS. But if you are not using it, you will face this problem.
So, I hope you ejoyed reading this, and also hope you get something interesting to your skills.