emotion
The Next Generation of CSS-in-JS
--
Update: This was written for the original version of emotion. Since this article was published we’ve increased performance and removed the babel plugin requirement along with a host of other changes. For an up to date look at emotion check out my article on version 8.
emotion is a high performance, lightweight css-in-js library. The core idea comes from Sunil Pai’s glam library and its philosophy is laid out here. The basic idea is simple. You shouldn’t have to sacrifice runtime performance for good developer experience when writing CSS. emotion minimizes the runtime cost of css-in-js dramatically by parsing your styles with babel and PostCSS. The core runtime is 2.3kb and with React support, 4kb. Total.
First Look
The api will be familiar if you know styled-components. We’ve also taken inspiration from glamorous and css-modules.
const imageBase = css`
width: 32px;
height: 32px;
border-radius: 50%;
`const Avatar = styled.img`
composes: ${imageBase};
border: 2px solid ${p => p.theme.borderColor}
@media(min-width: 420px) {
width: 96px;
height: 96px;
}
`
Benchmarks
This benchmark pushes the libraries by changing a dynamic value in the style block very quickly.
vs. styled-components — emotion is over 25x faster when using highly dynamic prop values at a third of the size.
styled-components warns against using this pattern and instead recommends using inline styles for highly dynamic values. emotion has no such limitations.
vs. glamorous — emotion is up to 2.5x faster on rerender at half the size.
css
import { css } from 'emotion'
css is the ❤️ of emotion. Most of the api, including styled
, are just wrappers around css
.
const imageBase = css`
width: 32px;
height: 32px;
border-radius: 50%;
`
css
is a tagged template literal that accepts a standard css text with a few additional features such as nesting, pseudo selectors, and media queries.
css
returns a string class name that can be used on any element. For our imageBase
style block above it would be something like css-imageBase-12345
.
To aid with composition css
accepts a special property in the style block called composes
.
With composes
we can use our imageBase
from above to quickly create avatar styles.
const avatarStyle = css`
composes: ${imageBase};
border: 1px solid #7519E5
`
Internally emotion will append these css classes to the generated one. avatarStyle
from above would generate a class name like css-imageBase-12345 css-avatarStyle-12345
This allows for some really powerful composition that really shines with styled
.
css
also accepts object styles 😏
const imageStyles = css({
width: 96,
height: 96
})
object styles are not auto-prefixed.
styled
import styled from 'emotion/react'
styled
is a thin wrapper around css
and the supports the same style text and expressions as it does.
styled
is modeled almost exactly like styled-components styled function.
const Avatar = styled.img`
width: 32px;
height: 32px;
border-radius: 50%;
`
styled
also works as a function call. The first argument can be any html tag or React component that accepts a className prop.
const BigAvatar = styled(Avatar)`
width: 96px;
height: 96px;
`
You can also use another styled component as a selector.
const Heading = styled.img`
font-family: serif;
`const Header = styled.header`
display: flex;
${Heading} {
font-size: 48px;
align-self: flex-end;
}
`
Composes works too
const imageBase = css`
width: 32px;
height: 32px;
border-radius: 50%;
`const Avatar = styled.img`
composes: ${imageBase};
@media(min-width: 420px) {
width: 96px;
height: 96px;
}
`
In styled
, the value of a composes
interpolation can be a function. It is called with the current props on render. It must return an className or object style.
const types = {
success: css`
background: green;
`,
error: css`
background: red;
`,
}const Alert = styled.div`
composes: ${props => types[props.type]};
padding: 10px;
`
Remember css just returns a className so this works.
The true power of composes shines through when used with something like styled-system by Brent Jackson.
Theming
import { ThemeProvider } from 'emotion/react'
Theming is provided by the theming library. The api is laid out in detail here. It is both based on styled-components theming and heavily tested which made it a no brainer.
Whenever you provide a theme to ThemeProvider
any styled component has access to those styles via props.theme
. It does not matter how nested your component is inside ThemeProvider
, you still have access to props.theme
.
const theme = {
white: '#f8f9fa',
purple: '#8c81d8',
gold: '#ffd43b'
}const Avatar = styled.img`
width: 32px;
height: 32px;
border-radius: 50%;
border: 1px solid ${props => props.theme.gold}
`<ThemeProvider theme={theme}>
<Avatar src={avatarUrl}>
hello world
</Avatar>
</ThemeProvider>
Extract Mode vs Inline Mode
The emotion babel plugin defaults to what we call “extract mode”. In this mode, we take all of your css that is defined in each file and extract it to [filename].emotion.css
and automatically import it in the top of your js file. Dynamic values are handled with css variables. The only updates at runtime are just changes to css variables! The disadvantages of extract mode include no ie11 support due to css variables and not being able to extract critical css for server side rendering.
In “inline mode” emotion does something a bit different.
Inline mode uses the CSS Object Model (CSSOM) to manipulate css from javascript. This is how every other css-in-js library out there works. What sets emotion apart is that we can precompile all of your style rules. We don’t have to parse and auto-prefix your styles at runtime because we reduce your styles down to raw insertRule calls.
Inline mode’s biggest advantages are that it works on ie9 and above and it has proper server side rendering support with extractCritical
. There is a great example of this over in the next.js repo
Under the hood
The following example is for inline mode. Extract mode works almost exactly the same.
This code
const H1 = styled.h1`
font-size: 48px;
color: ${props => props.color};
`
Is converted to the following when babel compiles it
const H1 = styled(
'h1',
['css-H1-duiy4a'], // generated class names
[props => props.color], // dynamic values
function createEmotionStyledRules (x0) {
return [`.css-H1-duiy4a { font-size:48px; color:${x0} }`]
}
)
Whenever the render method of H1
is called:
styled
goes through each dynamic value in your css block. As it iterates through this list, any value that is a function is called and given the current props.createEmotionStyledRules
is called using the final values from step 1 as arguments. In our example herex0
would be the result ofprops => props.color
.- The result of this call is then inserted into emotions StyleSheet shim where it is aggressively cached.
- The generated class names are appended to the component’s
className
prop.
Take a look at the source of styled
and css
.
Bonus
Keyframes
Animations are supported with the keyframes
function.
https://github.com/tkh44/emotion#keyframes
css prop
Any component within your application that accepts a className
prop can now accept a css
prop.
const flexCenter = css`
display: flex;
align-items: center;
justify-content: center;`
<div
css={`
composes: ${flexCenter};
width: 128px;
height: 128px;
background-color: #8c81d8;
border-radius: 4px;
img {
width: 96px;
height: 96px;
border-radius: 50%;
transition: all 400ms ease-in-out;
&:hover {
transform: scale(1.2);
}
}
`}
>
The prop behaves exactly like the css
function. The resulting class name is appended to the elements className
prop.
Go write some CSS.
I want to thank Sunil Pai for his guidance, patience, and for trusting in me to carry out his idea. 🙏
Huge thanks to Mitchell Hamilton because without him emotion just wouldn’t work as well. His contributions were invaluable in getting us where we we are now.
Thanks to all the contributors, issue reporters, and early testers!
emotion.sh — website
slack.emotion.sh — slack channel — come say hi! 👋