Beautiful State & Context based styling with React (CSS-in-JS)

Edmund Reed
Valtech Design
Published in
10 min readJul 17, 2019
Checkout Lucid on Github for the end result

I recently compared ten CSS-in-JS solutions for React in order to determine the ideal holy-grail API to style a simple React accordion. By comparing and contrasting existing solutions I was able to get a better understanding of how I wanted to do things. I found that when using Fela, I had the nicest styles declaration that made great use of props/state, but I preferred the JSX when using React-JSS. I also very much liked the paradigm when using Styled-Components of having dedicated named child components of my modules. Taking all the best bits of the solutions I examined left me with an idealistic API of:

View this example on CodeSandbox

Let me break down why I believe this API to be the “ideal” API (at least for me):

ES6 Objects vs Tagged Template Literals

This discussion is worth having separately, but to find out why I’ve opted for using Objects instead of Sass within TTLs, you can read this article:

React & CSS-in-JS: ES6 Objects vs Tagged Template Literals

Modules/Components

The motivation for these <Module> and <Component> React components comes from both Fela as well as Syled-Components. Fela showed how we could wrap our component in a provider to which we pass the styles object. As all our UI components would have an outer-most wrapper of some sort anyway (usually in the form of a div), it makes sense to treat this wrapper with a higher-level of semantics. Rather than having a div or a provider, we can simply think of it as a Module, or even something like a Block to keep familiar with BEM, if that’s what you’re into.

Using Styled-Components would leave us with <Panel>,<Heading> and <Content> child components, whereas all other solutions at best would leave us with things like <div className='panel'>, <div className='heading'> etc. Thus, <Component>s are all the components that make up a module, and can be passed name attributes, leaving us with a good compromise of <Component name='panel'>, <Component name='heading'> etc (though the JSX syntax achieved with Styled-Components is also achievable with Lucid).

Not only is this more readable than divs with className attributes, it’s also what allows the styles object to map keys to the correct elements. To target the panel component in the styles object, you would pass a key of panel. This is in-line with Fela and avoids having to write CSS selectors as keys (e.g. '.accordion__panel': {...}).

Component Function

The API is designed to allow for all styles pertaining to a single component to be kept within a single object (nested within the main styles object). The value of this object can be a function with the the following signature:

({ state, context, element }) => ({...})

…or the more complete version for the main styles object:

({ state, context, element, theme, config }) => ({...})

State

When you pass a prop to a component, it will be available in the state object of the component function. The reason why this is called state and not props is because the primary use-case is to pass state values as props. A barebones example showing the intended use is:

In this example, the text of the foo component would toggle between red and blue when clicked, based on the isActive state that is mapped to the isActive prop. But more importantly, things like hover are handled by React using React’s state, and internally this.state is merged into the state argument (allowing state.isHovered to be available).

Context

Whilst state can be thought of as the props assigned to the component in question, context can be thought of as the props that are assigned to parent components. This is naturally useful when wanting to style child components based on the value of a parent prop. Any prop of any parent within the Module will become part of the context object. This allows for useful Sass paradigms to work in JavaScript in a friendly way. Consider something like the below Sass:

.heading {
.isOpen & {
...
}
}

…this essentially allows you to style elements that have a heading class when a parent element also has an isOpen class. We can replicate this in JavaScript using this context convention:

heading: ({ context }) => ({
...(context.isOpen && {
...
})
})

You can be more specific by referencing the component’s name if there is risk of conflicting props with parent/grand-parent components:

heading: ({ context }) => ({
...(context.panel.isOpen && {
...
})
})

…similar to the following Sass:

.heading {
.panel.isOpen & {
...
}
}

To understand why we have context as well as the state, consider the following Sass with respect to the previous example:

.panel {
&.isOpen {
...
}
}

…which would be replicated in JavaScript as:

panel: ({ state }) => ({
...(state.isOpen && {
...
})
})

…you can see that context and state here are both paradigms that are often mirrored in Sass using the ampersand. And of course both state and context can be used at the same time:

panel: ({ state, context }) => ({
...(state.isOpen && {
...
}),
...(context.someContext && {
...
})
})

…represented in Sass as:

.panel {
&.isOpen {
...
}
.someContext & {
...
}
}

…which essentially gives us the same control with our props as Sass does with class names (with regards to styling), which seems like the goal of a React CSS-in-JS solution. In Sass, you can apply styles to an element based on the class names applied to that element as well as class names applied to parent elements. In React, you can apply styles to an element based on the props applied to that element as well as props applied to parent elements. It’s the same thing, the APIs to achieve it shouldn’t differ that much between technologies (Sass/JavaScript), and don’t have to, as I have just demonstrated.

Element

This is the underlying DOM node generated by React — whilst it’s typically an anti-pattern to access the underlying DOM in React, since we’re never actually planning to manipulate it, we can take advantage of being able to read it to simulate things like pseudo selectors:

:before, :after, :hover and :focus are supported out the box

Having access to the underlying DOM node can provide really useful escape hatches when you need to apply styles based on information provided by the DOM. When used with existing web APIs, you can achieve some cool stuff such as the above example.

Also consider this example showing how the Node.parentElement web API can be utilised to style parent DOM nodes:

…and could be used to apply styled to some wrapper element:

Here, the div wrapping the <MyModule> instances would be applied a margin-bottom style value of 20px. For this particular use-case, you can use the <Wrapper> Lucid component.

Sign me up!

If the information so far is enough for you, then the project you want to check out is called Lucid. Installation is quick and simple and you can be up and running with the code from the article in minutes without having to faff around with any additional Webpack or Babel configuration (unlike some of the other solutions I looked at).

Demo

…And Beyond

Carry on reading to explore how more advanced concepts work with this solution.

Theming

It wouldn’t be a very good solution if it didn’t somehow offer theming. So far, this approach doesn’t need any dedicated handling for themes in order to be able to use themes.

Lucid offers three ways to provide themes to your modules, depending on your preference/setup.

1. Passing manually: Since state is just a collection of props on the element, we could pass a theme prop to the Module element and access it via state.theme:

…of course this isn’t particularly nice to look at (this level of object destructuring makes me uneasy), which is why any theme prop can be accessed directly without having to do state.theme:

This allows theme to essentially be any JavaScript reference; what you do with it and how you use it is up to you, but typically it would exist as a plain object to provide reusable theming values.

2. Using Theme Provider Component: You can wrap your modules within a <Provider> Lucid component, passing your theme to the themeprop:

This seems to be the most conventional way to pass themes when using React.

3. Using Window: By assigning your theme to window.theme, it will be picked up automatically by <Module> and <Component>:

I personally don’t see much issue with this method.

See the Themes section of the Lucid Wiki for more info

Configurable Modules

Configuration is a pretty general concept; anything can be configured if you try hard enough (or have an actual need). Each Module you create using the <Module> component can accept a config prop which can be exposed to your styles, just like the how the theme prop works. Again, there would be nothing stopping you accessing this prop through the state object, it’s just more friendly to not do so.

Allowing certain cosmetic properties to be configurable allows you to create reusable components that can allow for radically different appearances by just switching out the config object when calling the component.

Config & Props

You may wish to pass a prop when using your module and pass the value through to the styles, rather than relying on having separate objects for multiple configs.

<MyModule fontSize='16px'>My Module</MyComponent>

When passing props to modules/components, remember that they are available as state, which means to get the above to work you can simply do:

Config & Themes

When defining your module’s default configuration, it’s possible you will want access to your theme. If you are defining a default font size for your component, you may wish to use a font size specified by your theme. This is possible by making your config exist as a function that accepts a parameter for the theme.

We can consider that if a style property requires access to the theme, then the likelihood is that the property can be configurable. Keeping to this paradigm in the above example avoids requiring access to theme in the styles object.

Duplicate Properties?

In the simple example above, the config and style functions return objects with similar (or identical, in this case) keys. This isn’t really a coincidence; CSS is a key:value syntax, just like most configuration, so configuration can often directly map to CSS properties when the configuration pertains to cosmetic stylistic properties (things like color, typography etc). If you do end up in the position where your configuration properties map directly to CSS properties, you can take advantage of Lucid’s ability to dynamically retrieve cosmetic CSS properties from configuration, and pass your configuration object as a styles object:

Checkout the article CSS at Scale: Cosmetic vs Layout Properties to explore this concept further

Dynamically Retrieve Cosmetic CSS Properties From State/Props?

Earlier we looked at passing props through to the styles with:

<MyModule fontSize='16px'>My Module</MyComponent>

…however, this had the caveat of requiring the property to be added to the styles object, despite being considered a cosmetic property which could otherwise be applied automatically through configuration.

fontSize: state.fontSize || config.fontSize

If we can dynamically retrieve CSS properties from configuration, we can surely do the same with props if we wanted to go all out, and since props are available to our styles as state, we can just do:

This means that we can add any CSS property as a prop without the styles object/function having awareness of it:

<MyModule padding='10px' fontWeight='bold'>
My Module
</MyModule>

To do the same thing to a child Component, you could do:

<MyModule>
<Component name='myComponent' padding='10px' fontWeight='bold'>
My Component
</Foo>
</MyModule>

N.B — these aren’t necessarily recommended paradigms; they are merely examples of what’s possible with Lucid’s flexible nature

Media Queries

One issue when using JavaScript to handle CSS is media queries. Using ES6’s object spread syntax combined with the matchMedia web API, you can easily simulate media queries with just plain JavaScript:

Combined with Lucid’s API you could make this much nicer by creating a utility function, used like so:

Improving The JSX

It’s hard for me to argue that Styled-Components doesn’t offer the nicest JSX out of all the CSS-in-JS solutions. If we were using Styled-Components, we’d have some JSX resembling:

<Accordion {...props}>
<Panel>
<Heading>...</Heading>
<Content>...</Content>
</Panel>
</Accordion>

…instead of using Lucid’s <Component name={name}>. For me, Lucid’s compromise is fine, however you can still use Lucid to achieve something similar to Styled-Components’ API by using Lucid’s styled API, as shown in the following example of a UI accordion:

Allowing you to define your child components as separate declarations is something one would have to do to achieve the desired JSX syntax when using Styled-Components anyway. However, I personally don’t see the additional overhead here as worth it, when the alternative in this case would be:

…but again, this is all personal opinion.

Human Readable Generated Code

Module and Component can render classes that follow the BEM naming convention, without any garbled nonsense class names like other solutions. The paradigm of having Modules and Components allows for certain paradigms from BEM to be taken, as Modules can map to BEM Blocks, and Components can map to BEM Elements, which is what allows the generated code to be human-readable whilst also scoped at the same time.

Why is human-readable production code important?

Sign me up!

Get started with Lucid

After looking at some of the more advanced stuff that’s possible with Lucid with regards to styling, hopefully you’ll agree that when compared with other existing solutions, the experience is not only more pleasant, but more flexible. If there’s any other solutions I’ve missed that do something similar, let me know so I can check themout!

--

--

Edmund Reed
Valtech Design

Design Systems Architect 🎨 UI•UX designer & developer 💻 I take front-end thought experiments too far 🧪 @valtech 💙