70 %
Chris Biscardi

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.

JS
import { css } from "@emotion/core";
const colorStyles = css`
color: red;
`;
const spacingStyles = css`
padding: 1rem;
margin-top: 1rem;
`;
export 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).

JS
const colorStyles = {
color: "red"
};
const spacingStyles = {
padding: "1rem",
marginTop: "1rem"
};
export 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.

JS
import { modularScale } from "polished";
const spacingStyles = css`
padding: ${modularScale(1)};
margin-top: ${modularScale(1)};
`;
JS
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.

JS
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)]
}
})
export 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.

JS
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.

JS
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.

JS
const expandedStyles = mq({
color: ["red", "green"]
});

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

JS
{
'@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.

JS
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
.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.