The most convenient way to write CSS-in-JS styles is with strings. This has multiple benefits such as
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.
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;`;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 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"};export default props => (<div{...props}css={{ ...colorStyles, ...spacingStyles }}/>);
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 elseconst spacingBaseStyles = {padding: 1,marginTop: 1,}// our usageconst 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.
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".
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.
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.