Composition of Styles: Strings vs Objects

The most convenient way to write CSS-in-JS styles is with strings. This has multiple benefits such as

  • easier copy/paste from existing code and examples
  • familiarity: most code we've written in the history of CSS is string-based

It also has some drawbacks. The one we're focusing on today is that CSS rulesets can only be composed "in chunks" or with specialized functions.

Strings

First, lets take a look at what it looks like to compose CSS written with strings in a CSS-in-JS world.

import { css } from '@emotion/core';
const colorStyles = css`
color: red;
`;
const spacingStyles = css`
padding: 1rem;
margin-top: 1rem;
`
default props => <div {...props}
css={[colorStyles, spacingStyles]}
/>

This isn't horrible. We can use similar naming conventions to BEM if we want, we can deterministically order the CSS rules, and we can combine chunks of CSS just fine. It's a perfectly acceptable way to write CSS... So lets take a look at what objects can give us on top of that.

Objects

Objects present a greater opportunity to compose and modify styles. Take our last example for instance. Using objects allows us to shed the import at the top (which may seem minor, but removing steps between the user and the end result is almost always useful) and we can switch to obj/rest spread, a JS language feature, to compose two sets of styles together. (note that if we want to use classic CSS approaches like setting multiple backgrounds as fallbacks, we'll likely still want to use the array syntax).

const colorStyles = {
color: 'red'
};
const spacingStyles = {
padding: '1rem',
marginTop: '1rem',
}
default props => <div {...props}
css={{ ...colorStyles, ...spacingStyles }}
/>

Functions

One example in which strings and objects are fairly similar is the manipulation of values. We could, for example, set our spacing with a modular scale.

import { modularScale } from 'polished';
const spacingStyles = css`
padding: ${modularScale(1)};
margin-top: ${modularScale(1)};
`
import { modularScale } from 'polished';
const spacingStyles = {
padding: modularScale(1),
marginTop: modularScale(1),
}

You can see how using strings with function manipulation is a little bit noisier, but it's not a huge problem. One benefit of objects is that it means we're using a data structure to deal with our CSS instead of a string. For manipulation of rulesets, this is a win in itself as it lets us use JS language features to manipulate our CSS output.

If we take our example, and we want to specify only scale values instead of pixel, rem, etc values. We can do that with base objects. To keep it simple, we'll test to see if the value passed in is a number; if it is, we'll convert that number to a modular scale value.

import { modularScale } from 'polished';
// maybe defined somewhere else
const spacingBaseStyles = {
padding: 1,
marginTop: 1,
}
// our usage
const spacingStyles = Object
.entries(spacingBaseStyles)
.map([key, value] => {
if (typeof value === 'number') {
return [key, modularScale(value)]
}
})
default props => <div {...props}
css={[ colorStyles, ...spacingStyles ]}
/>

Note that we're doing this manually here, but we could also implement this as a stylis plugin like the more complex stylis-rtl which uses cssjanus. Or we could implement it as a theme function so people can opt-in.

export default props => <div {...props}
css={({withScale}) => [
colorStyles,
...withScale(spacingBaseStyles)
]}
/>

If you're familiar with styled-system this will look familiar because that's how it works too. If you're not familiar with styled-system, go read Alan's article "Build Better Component Libraries with Styled System".

Modifying Keys

If we continue this pattern we can take it beyond what is possible with CSS strings and into the manipulation of keys. This is what libraries like facepaint do. To set up a series of media queries we'd define them once at a central location.

const mq = facepaint([
'@media(min-width: 420px)'
'@media(min-width: 920px)'
], { literal: true })

We can then import the mq function or pass it through context in our theme. The following example changes the color based on the media query using an array syntax for values.

const expandedStyles = mq({
color: ['red', 'green']
})

There's a decent amount going on here, so to break it down the output will look like:

{
'@media(min-width: 420px)': {
color: 'red'
},
'@media(min-width: 920px)': {
color: 'green'
}
}

notice that the array structure we used for color has expanded into multiple media queries. This is a powerful pattern for complex style creation. In our declarations we get to use an array, but in our output our arrays are processed into full-fledged CSS rulesets.

Lets take a look at a more complicated example.

import facepaint from 'facepaint'
const mq = facepaint([
'@media(min-width: 420px)',
'@media(min-width: 920px)',
'@media(min-width: 1120px)'
])
const myClassName = mq({
backgroundColor: 'hotpink',
textAlign: 'center',
width: ['25%', '50%', '75%', '100%'],
'& .foo': {
color: ['red', 'green', 'blue', 'darkorchid'],
'& img': {
height: [10, 15, 20, 25]
}
}
})
.css-rbuh8g {
background-color: hotpink;
text-align: center;
width: 25%;
}
@media (min-width:420px) {
.css-rbuh8g {
width: 50%;
}
}
@media (min-width:920px) {
.css-rbuh8g {
width: 75%;
}
}
@media (min-width:1120px) {
.css-rbuh8g {
width: 100%;
}
}
.css-rbuh8g .foo {
color: red;
}
@media (min-width:420px) {
.css-rbuh8g .foo {
color: green;
}
}
@media (min-width:920px) {
.css-rbuh8g .foo {
color: blue;
}
}
@media (min-width:1120px) {
.css-rbuh8g .foo {
color: darkorchid;
}
}
.css-rbuh8g .foo img {
height: 10px;
}
@media (min-width:420px) {
.css-rbuh8g .foo img {
height: 15px;
}
}
@media (min-width:920px) {
.css-rbuh8g .foo img {
height: 20px;
}
}
@media (min-width:1120px) {
.css-rbuh8g .foo img {
height: 25px;
}
}

This approach expands from class selectors to "self" (&) selectors to pseudo-selectors and anything CSS can support.

Fin

Strings are familiar and powerful for writing CSS, but objects contain an order of magnitude more power in how we author, transform, and apply styles. Whether we're using low-level stylis plugins or high level importable functions like modularScale, objects support significantly more compositional power than strings. So next time you write some CSS-in-JS, consider objects... and if you need some automation check out transform.now.sh to convert your strings to objects.


Web Mentions

𝙎𝙘𝙤𝙩𝙩 𝙎𝙥𝙚𝙣𝙘𝙚 👨‍💻

This is epic!

Basit Ali

facepaint looks so neat!

Sunil Pai

Terrific stuff

Henri Viiralt

objects & style functions ftw

Matt Hagner

I feel like it’s the 5 stages of grief. Denial that css in js is a valid idea. Anger that other people would use it. Bargaining with css in js by using strings. Depression that other people won’t give css in js a chance. And then acceptance that you should use objects for styles.

Max Stoiber

Chris did you just steal my blog post

😜

Just kidding, great one!

Joe Bell

I was always anti-object styles - I take it back, you've converted me

Max Stoiber

Just a draft right now—working on it, 🔜

Kyle Mathews @ Barcelona 🇪🇸

Oh man. I just don't get as attached I guess to tech choices. I switched to object styles almost immediately in 2014 when I started using react. The only thing I felt was guilt at how much I loved it as i thought (inline styles) we're "bad" but decided not true for components.

Matt Hagner

I was mostly joking 😂 I just never made the jump to objects because the string style felt “comfortable” and was always an easy way for me to convert others, “No camel casing and you don’t have to wrap the values in strings”.

Kyle Mathews @ Barcelona 🇪🇸

Yeeessss! I don't know why this isn't more widely understood.

Julien Goux

Great post! Objects can also be fully typed if you use a compatible language.

:party-corgi:

Yes! This goes great with another post I made recently about custom theme contexts instead of using a generic ThemeProvider

:party-corgi:

Glad it was useful! I used to do the same thing re: fallback. strings were comfortable, reliable, what I've known forever, etc

Heather Buchel

Thank you for this! I find myself falling back to using strings because it feels like 'ole reliable'. Nice to see a use case for using objects broken down like this.