Sebastian Pieczynski's website and blog
Published on: yyyy.mm.dd
Published on: yyyy.mm.dd
Created with ❤️ by Sebastian Pieczyński © 2023-2024.
Published on: 11/2/2023
There was a developer and there was a black IDE. IDE was empty and only the cursor was pulsing slowly. After a time a developer started to write code. The cursor was moving and the IDE was filled with code. The developer was happy it was good flow, pixels were drawn on the screen thanks to the words unspoken but written. Sooner rather than later something went wrong.. variables were all over the place with names set as they saw fit not as intended by their creator, paddings and margins were not uniform and changing color required editing each place separately. It was a mess and developer was sad.
The purge was needed.
...
If that short story reminds you of a thing or two then as you can clearly see you are not alone. Working with designers and having defined the details of how things are supposed to work helps a lot. If it's a solo project that has no defined structure yet then, well sometimes it starts living it's own life.
Thankfully I was already using TailwindCSS (it's awesome, really!) and there is documentation showing how to use CSS variables with Tailwind classes: see how to use variables with Tailwind and especially how to define colors to be fully usable the same way as "native" Tailwind classes: see how to define colors . Using these two methods I was able to keep things configurable with variables as I wanted and also use only Tailwind to get there. Win - win!
Try it now:
Inspect
tab and filter for root
element then in the CSS section paste this code for the element
:For light mode:
For dark mode:
Now the page background and few other elements are light green.
The system is a fancy name here for a set of rules and tokens, the ones I chose work as follows:
Every variable tries to follow this patters theme-[light/dark]-[property]-[value]-[modifier/discriminator]
.
For example: --theme-light-background-primary
so it's theme variable for light mode that modifies the background property and it is it's primary version.
This requires us to be very specific about the use of the variable and has added benefit of being verbose. I do prefer that to some short names no one will remember in three weeks or two. It makes code more readable and most importantly anyone reading this code will figure out what it does after checking other classes following the same pattern.
To create design system using TailwindCSS and CSS variables we need to define the variable in the :root
element and then we can use it in the tailwind.config.ts
. This is the only "drawback" as the variables are defined in one file and used mostly in another but they are sometimes needed when we want to create global styles utilizing them.
Example of few CSS variables used:
And then we can use these variables to create classes in Tailwind.
You probably saw that I was not completely honest with you. I have broken the pattern for the variable names here:
These short values are working more like globals / universal values or partials ex. --theme-mute-factor
is used in tandem with colors to create muted color variants. If at any point I decide they are muted too little or too much I only need to change value in this one place.
Examples:
By defining only the values for hsl(...)
CSS function we can now use the Tailwind's <alpha-value>
placeholder for the alpha channel and use the same opacity syntax as for every other Tailwind defined color:
If we defined the CSS variable including hsl
function we would have lost the ability to control the alpha channel and the code above would not work.
Configuring the "dark mode" in Tailwind is done with only one line of code in the tailwind.config.ts
file. Using it with our design is also straightforward but I will show you some cool ways to make it easier to implement into the website or app.
First we need to setup how dark mode should be handled by Tailwind itself. For Next.js I have found that using the class
configuration option is the most optimal way as it integrates well with other solutions and will prevent content flashing since the layout will be server side rendered first.
Now whenever we want to see dark mode CSS applied via Tailwind we only need to toggle dark
class on the html
element.
If the class on html
element is set to 'dark' now the text will be white and black if not.
Handling the actual change and saving user preferences is slightly more involving.
There are two main parts of the functionality we need to handle:
To keep user preferences regarding the theme we will use cookies as they are a key-value pair "database" but in the browser and they are sent to the server on every request. This fits our purpose perfectly as we need that value on initial render (for Server Side Rendering) and it stays with the user and is under his or hers control.
To prevent hydration errors we need to share the same state between server and client and both must be the same.
To store the theme for the client (browser) we will use React's Context API . In React we create Context when we want to share a property between components without prop-drilling (passing it down manually to all components).
I'll start slowly and explain crucial parts of the code, if you are familiar with the pattern you can scroll below to see the full source code.
To create new Context (and you can have many in your application) we use createContext
function provided by React.
Let's think what we need our Context to be and what to provide:
So our Context will need to keep both theme
variable (that can be set to light
or dark
) and setTheme
function to manipulate the theme
. Knowing that let's name give it a name of ThemeContext
and strongly type it:
To keep the theme
variable in check we limit it's values to light
and dark
with the ColorThemeType
type:
That way it will only be able to be set to predefined values when using TypeScript.
Now that we have design of our function we can start coding it.
This will create new Context (think of it as an external slot or bucket for variables, outside of the components tree) and assign it a value of null
(more on this in a second).
Context MUST be created OUTSIDE of the component itself.
I've decided to name it Theme
but you will see components like this named ThemeProvider
, 'ThemeContext' or similar more often to indicate they are utilizing the Context API.
Reference: React createContext
documentation
You can see that we are allowing to set the initialTheme
, this is very important for SSR and just in case the value is not provided we are setting it to light
by default. That way we are ensuring that the value is always available and stays consistent.
You will note that we are using Provider
sub component that is a part of ThemeContest
. The provider accepts a value
prop that is the value/object/property that we want to share across the component's tree. It will be passed to all underlying components (children) of the Theme
component. That value can be anything we want and usually it will be an object holding multiple properties or it could be a single property if we wanted theme to be read only.
Exxample of passing single read only value to context from React documentation:
Reference: React documentation .
Finally we are rendering the children
passed to the Theme
- this will render all the components that we pass as the children of the Theme
. This allows us to control from what level the theme
and setTheme
props are available.
One unusual thing about the way this Context Provider is written is the creation of useThemeContext
hook.
Let's first look at how we would utilize ThemeContext
without the custom hook.
Without the custom useThemeContext
hook we would need to:
theme
and setTheme
values actually exist in the context...I think you can see the pattern here, and we would need to do this every time. And then there are those pesky TypeScript errors. As someone wise once said, that's no way to live your life.
This is why for every context we create we'll also provide a custom hook that will look like this:
By utilizing this pattern we encapsulate all the logic related to verifying that the hook can be safely used, we make our TypeScript checks happy and can simplify the way the consumers (developers) interact with the ThemeContext
.
Now we only need to destructure theme
and setTheme
variables from the useThemeContext
hook. If the hook is used outside of where the ThemeContext
is defined it will throw an error notifying the developer that the function cannot be used at that level.
Here's the full code for ThemeContext
in TypeScript.
Note that Context use requires us to use the 'use client';
directive in Next.js.
Our theme sharing logic is complete, we can use the ThemeContext
to share what theme is currently selected with theme
variable and we can change it via setTheme
function shared by the useThemeContext
hook.
But our users can't..
With the business logic part done we can now start providing the functionality to the users on frontend of our application.
First we need to use the Theme
component in a place from which we want to start allowing changes to the theme. Since theme is a part of every component it will usually be the root of the application and in Next.js 13 and later with App Router the best place to do this would be the main layout.tsx
file located at src/app/layout.tsx
.
Note that some code was omitted for the sake of brevity.
Note that this component does not use the use client;
directive as it is a server side component and must be used on the server as it uses data from the cookie to retrieve the saved theme before it gets sent to the user.
The part where we retrieve the theme
variable from the cookies and set it while the page is still rendering on the server is the secret sauce to preventing content flashing as it will always be in sync with that the user sets on the clinet (soon) and by utilizing Tailwind we are sure that all the CSS we need for the page is already present.
We are also making sure that if the cookie is not set a default value is passed.
Then we pass that variable as initialTheme
, since that value can change in the future, to the Theme
context component.
We also set html class to the value of the savedTheme
:
The data-color-theme
helps to clearly see what theme is set in case your html tag is heavily customized.
This way we have completed the retrieving theme setting for the user.
For now we have achieved these goals:
But we still cannot change the theme. Let's work on that next.
Cookie operations provided by next are available only on the server side, but we need to use it on the client side as well.
To manipulate cookies we'll use the js-cookie
package.
install it with:
Then we'll create a ThemeToggle
component.
Thanks to our custom useThemeContext
hook we can easily get current theme
and setTheme
functions.
Then:
theme
variable holds andhandleThemeChange
when it's clicked.handleThemeChange
function will in turn:
theme
value.Again some parts of the code (like imports and classes) had been removed to focus on the solution.
After displaying the ThemeToggle
on a page and clicking the sun (or moon) icon the class assigned to the html tag changes!
With this we have achieved our final goal of allowing user to change the theme and save it.
Defining dark mode in TailwindCSS is straightforward: just add dark:
prefix to the utility class name and it will only activate in dark mode. But now we can both use custom classes that we have full control of with tailwind and we have fully functional dark mode in our app.
How cool is that?! 😁
NextUI is a set of components that are beautifully crafted with nice touch of sensible animations styled with TailwindCSS 🤯. With the presented setup it will work out of the box after following the installation process as it uses Tailwind and class based detection for the dark mode. See the installation guide for nextui components for more information.
With the presented approach we have successfully created the design system with Tailwind and Next.js, configured and setup dark mode without content flashing and provided user with simple way to preserve the theme between site visits.
I hope you found this helpful and enjoyed it, if you have any questions please reach out to me on Twitter or Contact Page.
Back to Articles