4 New Theme Based React UI Toolkits and Why Itās Going To Change How You Think
- Emotion
- Performance Is No Longer an Issue
- Flexbox and Evergreen
- The Beginning of an Idea
- Theme Oriented Styling
- The Road to Design Systems
- Converging Concepts
- Style props
- Variants
- ThemeProvider
- Box
- Concerns
- Composition vs. Inheritance
- Toolkit vs. Framwork
- Rebass
- Styled System
- Theme UI
- Chakra UI
- Styling after Styling
- Finding Themes
- Conclusion
styled-components
created a new standard for how we do CSS-in-JS. It introduced a sweet spot between writing CSS and React components, and it looked like this:
With this new API you could:
- Use something that looks like CSS, using the new template literal feature from recent ECMAScript versions
- Create atomic-feeling React components
- Still preserve flexibility with access to
props
and use them through out the template literal freely
Made for a very low-friction developer experience, that was widely adopted, and then widely imitated (in a good way). We now have the same styled
API in Emotion and others.
From here on, styled-components evolved to include themes and a ThemeProvider
as well as a rich ecosystem.
styled-components is considered to be one of the most popular libraries for doing CSS-in-JS, and with version 5.0.0 just out in 2020, it catches up on performance with no API breaking changes.
Emotion
Emotion is another great library for your CSS-in-JS. If we avoid comparing it to styled-components (there are many comparisons already), it seemed to have settled on a niche of infrastructure for component libraries. Though Iām not sure as to why it did, Iām guessing that for a long while, it was considered to have first-class performance, while styled-components wasnāt (this is not the case anymore with styled-components 5.0.0).
For this reason, many, if not all of the most important UI frameworks were built on Emotion, and not on styled-components.
Largely, Emotion and styled-components have similar but not identical API and developer experience. If you want, both have the styled
API, and using styled
is a safe way to build something that can work with both:
But, and this is where our story starts to become more interesting, Emotion had put emphasis (and bet) on their now widely-known css prop.
They state that their primary (better?) way to style components is through a special css
prop that does a few things behind the scenes:
A few notes about the css
prop (from Emotion docs):
- Similar to the style prop, but also has support for auto vendor-prefixing, nested selectors, and media queries.
- Allows developers to skip the styled API abstraction and style components and elements directly.
- The css prop also accepts a function that is called with your theme as an argument allowing developers easy access to common and customizable values.
- Reduces boilerplate when composing components and styled with emotion.
- Server side rendering with zero configuration.
- Theming works out of the box.
Obviously, although the css
prop appear to be inline with your component, its contents are compiled, extracted and put as a standard CSS block.
The magic, in this example is done by the jsx
pragma at the top. With it, React creates new components with the Emotion jsx
function, which is a custom way to create React components, with the relevant css prop logic applied.
Performance Is No Longer an Issue
Performance was a key differentiator between CSS-in-JS libraries for a long while. Well it isnāt an issue anymore:
You might want to look at the styled-components 5.0.0 for a nice comparison.
With that out of the equation, we can have an honest look at the abstractions around UI frameworks or toolkits that were created using both Emotion and styled-components with a material view of what developer experience they supply us with.
Flexbox and Evergreen
Weāre lucky to be living in a Flexbox and Evergreen world today. This means, that for the most part we can use Flexbox and get away with handling browser layout quirks. Which also means we have a huge burden of hacking around browser support and bridging incompatible rendering on our own, lifted from our backs.
In turn this creates less code, less hacks, and more opportunity for CSS-in-JS frameworks to be a legitimate replacement for CSSāāāwe no longer need that demanding skillset of āknowing CSSā, in the sense that knowing CSS was really about knowing the differences between browsers and how to bridge those creatively (read: find hacks) and in a reliable manner.
This also puts a lot of the classic grid systems largely out of a job. Building grid system with Flexbox is a trivial matter today.
The Beginning of an Idea
Before we move on, Iād like to plant an idea in your mind. We said that the styled
interface also allows using prop
like any other React component. So basically this:
And now, if we wanted to use a theme variable, having that a theme is supplied as a prop named theme
how about this?
The important bit of information is that we call our background prop bg
, so how about we create this function?
This function builds theme accessor functions, and now, we can use it to have a bit more of an elegant approach in our styled
clause:
And this is the idea I wanted you to have in mind. From here, you can create a wide range of tools: theme getters, variants, responsive styles and more, all based on the same principle.
In fact, many of the next-generation libraries weāre going to look at next use this idea as their basic building block.
Theme Oriented Styling
What if we take our idea further? Say we have this block:
Thereās no such color bg
, but if we already go through the process of "compiling" template literals from what looks like CSS to actual CSS, could we tweak our infrastructure to replace bg
from a name to an actual value in our theme?
For this to happen we need to:
- Have a theme ready in our context and available for every component (we need to know where to get the theme from)
- Where does the actual value for
bg
sit? is this the background for button? is this just one of a set of agreed-upon colors? - Have an idea of what actually is a theme
So basically if we answer for (3) we get free answers for our first two questions.
First attempt:
const theme = {
bg: '#fff'
}
Second attempt, a bit more verbose:
const theme = {
background: {
bg: '#fff'
}
}
Now I need to āteachā my infrastructure code that all CSS props that deal with background
need to key into the .background
prop in our theme, always. Nice idea, right?
But, if you notice weāre dealing with buttons, we could also do this:
const theme = {
buttons: {
one: {
background: '#fff'
}
}
}
And if we could, with a bit of imagination, use something like this instead of our styled
interface:
And we hope someone is listening in to this variant prop, and pulls the correct style from the buttons section, and injects it into our styling for our particular button.
Well, surprise! all these ideas, and more, are part of what I call next-gen CSS-in-JS frameworks.
But all these ideas need structure, which if we think about where everything starts fromāāāthis structure exist in our modeling of a theme.
This is where the system-ui theme specification comes into play. It is somewhat widely adopted, and as of this writing thereās not much deviation from this āstandardā, which is good news.
Hereās an example:
// example fontSizes scale as an array
fontSizes: [
12, 14, 16, 20, 24, 32
]
// example colors object
colors: {
blue: '#07c',
green: '#0fa',
}
// example nested colors object
colors: {
blue: '#07c',
blues: [
'#004170',
'#006fbe',
'#2d8fd5',
'#5aa7de',
]
}
The theme specification takes care of:
- Order and convention. With this we know where colors go, where spaces go and so on
- Context. With a well-known spec, we can build tools (and people did) to apply proper understanding of what React component weāre trying to theme, and to pull the right style from a theme
- CSS-prop to theme-prop mapping. The spec determines that, for example, the
sizes
theme prop apply towidth, height, min-width, max-width, min-height, max-height
CSS props
And this is how we use it:
More on the sx
prop later, but the gist of it is that width
is now super-powered with our theme infrastructure, because we know a CSS width
is mapped to our theme sizes
prop, we know it indexes into the 0
and 2
positions which may resolve to, say, ā768pxā and ā1070pxā.
Why the array? Because we are responsive by default. In this example, [0,2]
relate to mobile/desktop respectively (you can apply more breakpoints).
The Road to Design Systems
If we have a theme specification, that locks in our style guide, design tokens and lets us centrally apply a policy around our styles, then we have much of the work done for a pretty good design system.
The kind of design system thatās managed centrally, in one place that is shared between engineering and design needs to have very transparent and comprehensible conceptsāāāit needs to be almost as simple as a JSON file, and this is to a large degree what the theme specification represents.
You can see part of what you can do here.
Converging Concepts
When we look at next-gen UI frameworks like Theme UI, Rebass, Chakra, and others, we see a few concepts that are very similar, and luckily enough are converging so we donāt have to learn many different things.
Style props
Style props give us HTML 3.0 back again (kidding!):
<div mr="30px" width="200px"/>
Here mr
is a shorthand for margin-right
and width
is CSS width
, and not your regular HTML element (HTML 3.0ish) width.
The great advantage that style props bring is hackability and familiarity. You can use a component and not factor all of the edge cases into your component. This helps you create a component library thatās more resilient to change, and for example, you donāt have to make components incorporate logic to care of their spacing (margin, padding) in order to lay themselves out in a page.
Variants
It seems that most frameworks now add the concept of a variant, which looks like this:
<div variant="button"/>
If I could pick a concept that already exists in CSS that you are familiar with, it would be to say that a variant is kind of similar to a CSS class. Only itās not cascading, and you canāt apply many of these, and so on.
A variant lets you swap a componentās style entirely, and to key into a theme style. So you can switch themes completely, each having their own set of variants, or you can switch variants:
...
variants: {
button: {
..
}
}
Variants can be generic, as in above, or can be component specific such as:
...
buttons: {
primary: {
..
}
}
And use it like so:
<button variant="primary"/>
And the piece of infrastructure thatās supposed to figure all this out, context, variant mix-in and theming is your UI framework.
ThemeProvider
Having that weāve established that a theme spec is one main aspect of what weāre doing here, almost every next-gen UI framework has a ThemeProvider
or uses the underlying infrastructure's ThemeProvider
such as Emotion's or styled-component's.
Basically itās:
import theme from './theme'
const Index = <ThemeProvider theme={theme}>..</ThemeProvider>
Box
At a first glance Box
looks like a simplistic component. However, what it holds is the entire infrastructure for figuring out themes, style props, variants and more. The idea is that there is a single smart component which you build on, and out of which all of your other components will derive.
So for example to create a button you would do:
<Box as="button" ...>
And of course, when we say Box
we're missing just one more word to make a winning combo: Flex
. And so, may of the newer UI frameworks adopted both Box
to signify the main building block that has all of the magic in it, and Flex
, your main layout component to design your UI with.
Concerns
Box
contains multiple concerns, each takes care of a different aspect of a proper design system. Here's a breakdown (from theme-ui
):
export const Box = styled('div', {
shouldForwardProp,
})(
{
boxSizing: 'border-box',
margin: 0,
minWidth: 0,
},
base,
variant,
space,
color,
sx,
props => props.css
)
This means that theme-ui
's Box
components supports variants, spacing and space-aware props that connects these with our theme, colors -- which is very similar, and your magical sx
and css
props that we discussed previously.
And of course it takes care of your go-to fixes such as stating that this is a border-box
layout model.
A good starting point to understand each UI framework is to start looking at the source for the Box
component. Here is Chakra UI Box
component, which in this case includes concerns such as shadow
and typography
.
Composition vs. Inheritance
Weāve been saying āconcernsā a lot. If we look at React and the component approach from a birdās eye view, we discover that weāre having two patterns hiding in plain sight: composition and inheritance.
Inheritance is augmenting and creating new components by using existing ones, so for example:
const Button = styled.div`
...
`
Here we take a div
and "inherit" from it to create a Button
.
When we look at the Box concept, we discover something different, this is actually composition:
export const Box = styled('div', {
shouldForwardProp,
})(
base,
variant,
space,
color,
sx,
props => props.css
)
The traits, or concerns, base
, variant
and so on, are creating multiple inheritance trees or more specifically -- a concerns/mixin based model, which we know from Software Engineering is more flexible, resilient and adaptable.
So by opting into the new way to build design systems in React, youāre also opting into a better software engineering model.
Toolkit vs. Framwork
As my own opinionated view, I look at the following frameworks as toolkits, not frameworks. Itās hard to accept a toolkit, because weāve been used to all-in-one frameworks back from Bootstrap, and up to Antd.
But when you realize what Iāve discussed up until now, you understand that with composition and a good way to model a framework you can build you own, on the condition that you donāt really need a full-blown enterprise-ready UI frameworks with complex data tables, calendars, dropdowns and more.
Rebass
Rebass was one of the most popular frameworks that pushed this new approach into mass adoption. Approaching both mobile (React Native) and Web, itās been a simple yet pragmatic framework for a long while.
As weāll see with Brent Jackson, it evolved, struck different roots in the process, which mind youāāāis the better way to do things in software, into different and better frameworks.
Styled System
One of the infrastructure that we got from all that action was styled-system which is the infrastructure under most of these new-gen UI frameworks.
Itās actually easy to build your own framework using styled-system, as you get many of the concerns for free.
Theme UI
Theme UI is an evolution of work that Brent already did, you can read about the history here. For a long while it had no components of its own, and so it was seen as a foundational framework, and in that sense people still preferred to use Rebass (also from Brent).
Recently, Theme UI got a components kit which makes it eligible for a one-stop-shop framework. Iāve personally moved to using Theme UI exclusively instead of combining it with something else, like Rebass.
It introduces the sx
prop, which is a way to combine themes and the css
prop from Emotion.
Chakra UI
Chakra UI is a polished, complete and friendly framework. It builds on top of every concept weāve mentioned here, and still, tries to be compatible with Theme UI for a good degree.
Styling after Styling
So once youāve picked your UI framework, how do you customize your styles? There are a few options.
One, is use the sx
, css
, or style props to create your own variant of a component. For this, you'll have to create a new component:
const StyledButton = (..)<Button sx={{background: 'dark'}}>..</Button>
Another way, is to use forward refs and build a new component from an infrastructural point of view. In other words, itās still just a Box
. Here's how Chakra does it:
const Badge = forwardRef(
({ variantColor = "gray", variant = "subtle", ...props }, ref) => {
useVariantColorWarning("Badge", variantColor);
const badgeStyleProps = useBadgeStyle({ color: variantColor, variant });
return (
<Box
ref={ref}
display="inline-block"
....
Lastly, in the general sense you can still use styled-components
, either the Emotion styled
version or if for some reason you want to mix styled-components
and Emotion (in reality it works but officially there are caveats):
const Button = styled(Box)`
...
`
If in doubt, since this entire theory that weāve built in this article really creates less code and cruft, the best thing to do is to dive into the Theme UI codebase and see how new components are built.
With this added infrastructure magic youāll be surprised how elegantly you can create new components and experiences without creating a lot of noise.
Finding Themes
Since themes are the new hot thing here, you can now mix and match and find inspiration for styling in themes that already exist. Take a look here at Theme UIās packages list, and see how themes are made.
Not only that, if youāre a fan of Typography you have a toTheme
helper that can convert a Typography theme to be a first-class citizen Theme UI theme.
Conclusion
I believe the above theory, discussion, building blocks, and implementation and showcases are the next step in UI frameworks, much thanks to the very observant and skillful Brent Jackson. I also believe that in software engineering, the healthy way sometimes is to build something, and then start from scratch a couple times to get at something perfect, which is exactly what was done in the case of the frameworks weāve seen.