Bridging the Gap Between CSS and JavaScript: CSS-in-JS
Publikováno: 3.12.2018
In this article, we’re going to dig into the concept of CSS-in-JS. If you’re already acquainted with this concept, you might still enjoy a stroll through the philosophy of that approach, and you might be even more interested in the next article.
Web development is very interdisciplinary. We’re used to working closely with multiple languages. And, as developing web applications becomes more commonplace and nuanced, we often look for creative ways to bridge the gaps between those languages to …
The post Bridging the Gap Between CSS and JavaScript: CSS-in-JS appeared first on CSS-Tricks.
In this article, we’re going to dig into the concept of CSS-in-JS. If you’re already acquainted with this concept, you might still enjoy a stroll through the philosophy of that approach, and you might be even more interested in the next article.
Web development is very interdisciplinary. We’re used to working closely with multiple languages. And, as developing web applications becomes more commonplace and nuanced, we often look for creative ways to bridge the gaps between those languages to make our development environments and workflows easier and more efficient.
The most common examples are typically when using templating languages. For example, one language might be used to generate the code of a more verbose language (often HTML). This is one of the key aspects of front end frameworks — what does manipulating HTML look like? The most recent twist in this area was JSX because it’s not really a templating language; it’s a syntax extension to JavaScript, and it makes working with HTML really succinct.
Web applications go through many state combinations and it’s often challenging to manage content alone. This is why CSS sometimes falls by the wayside — even though managing styling through different states and media queries is equally important and just as challenging. In this two-part series, I would like to place CSS in the spotlight and explore bridging the gap between it and JavaScript. Throughout this series, I will assume that you’re using a module bundler like webpack. As such, I will use React in my examples, but the same or similar principles are applicable to other JavaScript frameworks, including Vue.
The CSS landscape is evolving in many directions because there are a lot of challenges to solve and there is no "correct" path. I’ve been spending considerable effort experimenting with various approaches, mostly on personal projects, so the intention behind this series is only to inform, not to prescribe.
Challenges of CSS
Before diving into code, it’s worth explaining the most notable challenges of styling web applications. The ones I’ll talk about in this series are scoping, conditional and dynamic styles, and reusability.
Scoping
Scoping is a well-known CSS challenge, it’s the idea of writing styles that don’t leak outside of the component, thus avoid unintended side effects. We would like to achieve it ideally without compromising authoring experience.
Conditional and dynamic styles
While the state in front-end applications started getting more and more advanced, CSS was still static. We were only able to apply sets of styles conditionally — if a button was primary, we would probably apply the class "primary" and define its styles in a separate CSS file to apply how it’s going to look like on the screen. Having a couple of predefined button variations was manageable, but what if we want to have a variety of buttons, like specific ones tailored for Twitter, Facebook, Pinterest and who knows what else? What we really want to do is simply pass a color and define states with CSS like hover, focus, disabled etc. This is called dynamic styling because we’re no longer switching between predefined styles — we don’t know what’s coming next. Inline styles might come to mind for tackling this problem, but they don’t support pseudo-classes, attribute selectors, media queries, or the like.
Reusability
Reusing rulesets, media queries etc. is a topic I rarely see mentioned lately because it’s been solved by preprocessors like Sass and Less. But I’d still like to revisit it in this series.
I will list some techniques for dealing with these challenges along with their limitations in both parts of this series. No technique is superior to the others and they aren’t even mutually exclusive; you can choose one or combine them, depending on what you decide will improve the quality of your project.
Setup
We’ll demonstrate different styling techniques using an example component called Photo
. We’ll render a responsive image that may have rounded corners while displaying alternative text as a caption. It will be used like this:
<Photo publicId="balloons" alt="Hot air balloons!" rounded />
Before building the actual component, we’ll abstract away the srcSet
attribute to keep the example code brief. So, let’s create a utils.js
file with two utilities for generating images of different widths using Cloudinary:
import { Cloudinary } from 'cloudinary-core'
const cl = Cloudinary.new({ cloud_name: 'demo', secure: true })
export const getSrc = ({ publicId, width }) =>
cl.url(publicId, { crop: 'scale', width })
export const getSrcSet = ({ publicId, widths }) => widths
.map(width => `${getSrc({ publicId, width })} ${width}w`)
.join(', ')
We set up our Cloudinary instance to use the name of Cloudinary’s demo cloud, as well as its url
method to generate URLs for the image publicId
according to the specified options. We’re only interested in modifying the width in this component.
We’ll use these utilities for the src
and srcset
attributes, respectively:
getSrc({ publicId: 'balloons', width: 200 })
// => 'https://res.cloudinary.com/demo/image/upload/c_scale,w_200/balloons'
getSrcSet({ publicId: 'balloons', widths: [200, 400] })
// => 'https://res.cloudinary.com/demo/image/upload/c_scale,w_200/balloons 200w,
https://res.cloudinary.com/demo/image/upload/c_scale,w_400/balloons 400w'
If you’re unfamiliar with srcset
and sizes
attributes, I suggest reading a bit about responsive images first. That way, you’ll have an easier time following the examples.
CSS-in-JS
CSS-in-JS is a styling approach that abstracts the CSS model to the component level, rather than the document level. This idea is that CSS can be scoped to a specific component — and only that component — to the extent that those specific styles aren’t shared with or leaked to other components, and further, called only when they’re needed. CSS-in-JS libraries create styles at runtime by inserting <style>
tags in the <head>
.
One of the first libraries to put this concept to use is JSS. Here is and example employing its syntax:
import React from 'react'
import injectSheet from 'react-jss'
import { getSrc, getSrcSet } from './utils'
const styles = {
photo: {
width: 200,
'@media (min-width: 30rem)': {
width: 400,
},
borderRadius: props => (props.rounded ? '1rem' : 0),
},
}
const Photo = ({ classes, publicId, alt }) => (
<figure>
<img
className={classes.photo}
src={getSrc({ publicId, width: 200 })}
srcSet={getSrcSet({ publicId, widths: [200, 400, 800] })}
sizes="(min-width: 30rem) 400px, 200px"
/>
<figcaption>{alt}</figcaption>
</figure>
)
Photo.defaultProps = {
rounded: false,
}
export default injectSheet(styles)(Photo)
At first glance, the styles
object looks like CSS written in object notation with additional features, like passing a function to set the value based on props. The generated classes are unique, so you never need to worry about them clashing with other styles. In other words, you get scoping for free! This is how most CSS-in-JS libraries work — of course, with some twists in features and syntax that we’ll cover as we go.
You can see by the attributes that the width of our rendered image starts at 200px
, then when the viewport width becomes at least 30rem
, the width increases to 400px
wide. We generated an extra 800
source to cover even larger screen densities:
- 1x screens will use
200
and400
- 2x screens will use
400
and800
styled-components is another CSS-in-JS library, but with a much more familiar syntax that cleverly uses tagged template literals instead of objects to look more like CSS:
import React from 'react'
import styled, { css } from 'styled-components'
import { getSrc, getSrcSet } from './utils'
const mediaQuery = '(min-width: 30rem)'
const roundedStyle = css`
border-radius: 1rem;
`
const Image = styled.img`
width: 200px;
@media ${mediaQuery} {
width: 400px;
}
${props => props.rounded && roundedStyle};
`
const Photo = ({ publicId, alt, rounded }) => (
<figure>
<Image
src={getSrc({ publicId, width: 200 })}
srcSet={getSrcSet({ publicId, widths: [200, 400, 800] })}
sizes={`${mediaQuery} 400px, 200px`}
rounded={rounded}
/>
<figcaption>{alt}</figcaption>
</figure>
)
Photo.defaultProps = {
rounded: false,
}
export default Photo
We often create semantically-neutral elements like <div>
and <span>
solely for styling purposes. This library, and many others, allow us to create and style them in a single motion.
My favorite benefit of this syntax is that it’s like regular CSS, minus interpolations. This means that we can migrate our CSS code more easily and we get to use our existing muscle memory instead of having to familiarize ourselves with writing CSS in the object syntax.
Notice that we can interpolate almost anything into our styles. This specific example demonstrates how we can save the media query in the variable and reuse it in multiple places. Responsive images are an excellent use case for this because the sizes
attribute contains basically CSS, so we can use JavaScript to make the code more DRY.
Let’s say that we decided we want to visually hide the caption, but still make it accessible for screen readers. I know that a better way of achieving this would be to use an alt
attribute instead, but let’s use a different way for the sake of this example. We can use a library of style mixins called polished — it works great with CSS-in-JS libraries making it great for our example. This library includes a mixin called hideVisually
which does exactly what we want and we can use it by interpolating its return value:
import { hideVisually } from 'polished'
const Caption = styled.figcaption`
${hideVisually()};
`
<Caption>{alt}</Caption>
Even though hideVisually
outputs an object, the styled-components library knows how to interpolate it as styles.
CSS-in-JS libraries have many advanced features like theming, vendor prefixing and even inlining critical CSS, which makes it easy to stop writing CSS files entirely. At this point, you can start to see why CSS-in-JS becomes an enticing concept.
Downsides and limitations
The obvious downside to CSS-in-JS is that it introduces a runtime: the styles need to be loaded, parsed and executed via JavaScript. Authors of CSS-in-JS libraries are adding all kinds of smart optimizations, like Babel plugins, but some runtime costs will nevertheless exist.
It’s also important to note that these libraries aren’t being parsed by PostCSS because PostCSS wasn’t designed to be brought into the runtime. Many use stylis instead as a result because it’s much faster. This means that we unfortunately can’t use PostCSS plugins.
The last downside I’ll mention is the tooling. CSS-in-JS is evolving at a really fast rate and text editor extension, linters, code-formatters etc. need to play catch-up with new features to stay on par. For example, people are using the VS Code extension styled-components for similar CSS-in-JS libraries like emotion, even though they don’t all have the same features. I’ve even seen API choices of proposed features being influenced by the goal of retaining syntax highlighting!
The future
There are two new CSS-in-JS libraries, Linaria and astroturf, that have managed zero runtime by extracting CSS into files. Their APIs are similar to styled-components, but they vary in features and goals.
The goal of Linaria is to mimic the API of CSS-in-JS libraries like styled-components by having built-in features like scoping, nesting and vendor prefixing. Conversely, astroturf is built upon CSS Modules, has limited interpolation capabilities, and encourages using a CSS ecosystem instead of deferring to JavaScript.
I built Gatsby plugins for both libraries if you want to play with them:
Two things to have in mind when using these libraries:
- having actual CSS files means that we can process them with familiar tools like PostCSS
- Linaria uses custom properties (a.k.a. CSS variables) under the hood, be sure to take their browser support into consideration before using this library
Conclusion
CSS-in-JS are all-in-one styling solutions for bridging the gap between CSS and JavaScript. They are easy to use and they contain useful built-in optimizations — but all of that comes at a cost. Most notably, by using CSS-in-JS, we’re essentially ejecting from the CSS ecosystem and deferring to JavaScript to solve our problems.
Zero-runtime solutions mitigate some of the downsides by bringing back the CSS tools, which ascends the CSS-in-JS discussion to a much more interesting level. What are the actual limitations of preprocessing tools compared to CSS-in-JS? This will be covered in the next part of this series.
Article Series:
- CSS-in-JS (This post)
- CSS Modules, PostCSS and the Future of CSS
The post Bridging the Gap Between CSS and JavaScript: CSS-in-JS appeared first on CSS-Tricks.