styled-components & Emotion

Both libraries are covered by the same default aliases, and every spelling of styled compiles to a static component.

Supported call shapes

1import styled from 'styled-components'
2
3const A = styled.div`
4  color: red;
5`
6const B = styled('div')`
7  color: red;
8`
9const C = styled.div({ bg: 'red' })
10const D = styled('div')({ bg: 'red' })
11const E = styled(Component)({ bg: 'red' })
12const F = styled('div', { bg: 'red' })
13const G = styled(motion.div)`
14  color: red;
15`
16const H = styled(forwardRef(Field))({ bg: 'red' })
1import styled from 'styled-components'
2
3const A = styled.div`
4  color: red;
5`
6const B = styled('div')`
7  color: red;
8`
9const C = styled.div({ bg: 'red' })
10const D = styled('div')({ bg: 'red' })
11const E = styled(Component)({ bg: 'red' })
12const F = styled('div', { bg: 'red' })
13const G = styled(motion.div)`
14  color: red;
15`
16const H = styled(forwardRef(Field))({ bg: 'red' })

Any base renders: a tag, a component, a member such as motion.div, or any other value, read once where the component is declared. null, numbers, booleans and undefined never render, so they are build errors, as is a styled left uncompiled (styled('div') never given styles, styled.div exported bare): its import is removed, and that code would throw at runtime.

A keyframes or css result held in a const of the same file is a build-time value, so css`animation: ${fadeIn} 1s;` and styled.div({ ['.' + card + ':hover &']: { margin: 0 } }) work. In CSS text a class standing where a declaration would is a mixin, composed with the component's class. One imported from another module is not: its name is only known once that file is built, so declare it where it is used.

Object styles follow the libraries: several style arguments, or an array of them, merge into one rule set where a later declaration replaces an earlier one and nested rules merge; Emotion's options argument (styled('div', { shouldForwardProp })(...)) is accepted, and its shouldForwardProp is evaluated at build time. Numbers mean pixels as they do in Emotion and styled-components — padding: 8 is 8px, unitless properties such as lineHeight stay as written — while Devup UI shorthands such as p keep the spacing scale.

A style argument can be conditional — cond && { ... }, cond ? { ... } : { ... }, value || { ... } or value ?? { ... }, in styled as in css. Each property it sets becomes a per-property ternary over what came before (color: cond ? 'blue' : 'red'), so the result does not depend on the order atomic classes land in the stylesheet; a conditional class joins as cond ? cls : '', and the left side of || / ?? joins only when it is a string, as the libraries skip other values. Classes passed beside rule objects compose in styled as in css.

A style argument that cannot be known at build time — a function call the build cannot run, a spread or a computed key it cannot compute — is a build error naming it, rather than a silently missing style; a spread, a selector value or a computed key the module computes (styled.div({ ...cardStyles(), _hover: hoverStyles })) is computed, while the other values stay CSS variables set at runtime. An object constant, declared in the file or imported, reads as if written in place: styled.div(baseStyles), styled.div({ ...baseStyles, color: 'red' }). A single argument that is not a rule object composes as a class, as several do; one holding rules the module computes (const base = makeStyles(), styled.div(base)) applies those rules. In the object form a value may read the theme ((p) => p.theme.brand); any other function of the props is a build error — write it in a template (${(p) => p.color}), where it becomes a CSS variable.

A styled callback cannot return a class name known only at runtime (styled.div(p => { return p.cls; })): the build reports a located Cannot compose error. A condition choosing between known classes or rule objects is supported.

Inside a template, an interpolation standing where a declaration would is a mixin: ${mixin} and ${(p) => p.active && mixin} compose their class with the component's. A literal or constant is written into the CSS wherever it stands — a value, a CSS snippet, a selector. Any other interpolation in a selector or a property name is a build error, as are runtime values in css, keyframes and global styles, which have no element to set a CSS variable on.

.attrs() and .withConfig() chain onto any factory. Attrs are merged over the props in order, as styled-components does — an object is spread, a function is called with the props. In .withConfig(), shouldForwardProp is evaluated at build time; displayName, componentId and the other options are ignored and add no runtime behavior:

1const Button = styled.button
2  .attrs({ type: 'button' })
3  .attrs((props) => ({ disabled: props.loading }))
4  .withConfig({ displayName: 'Button' })`
5  color: red;
6`
1const Button = styled.button
2  .attrs({ type: 'button' })
3  .attrs((props) => ({ disabled: props.loading }))
4  .withConfig({ displayName: 'Button' })`
5  color: red;
6`

A file may import styled from several of these libraries at once; every import compiles.

Namespace imports work too. @emotion/styled and styled-components export a single callable, so the namespace binding is that value and it is bound straight to the Devup UI export:

1import * as Emotion from '@emotion/styled'
2// → import { styled as Emotion } from '@devup-ui/react'
3
4const Card = Emotion.div`color: red;`
1import * as Emotion from '@emotion/styled'
2// → import { styled as Emotion } from '@devup-ui/react'
3
4const Card = Emotion.div`color: red;`

Transient props

Props whose names start with $ are transient, as in styled-components: style functions read them, but a styled component never passes them on to what it renders — a tag, a component or what as names. That includes $ props a runtime spread brings and those attrs create, even when shouldForwardProp allows them:

1const Title = styled.h1`
2  color: ${(p) => p.$tone};
3`
4
5function Heading(props) {
6  return <Title {...props} />
7}
8
9// `$tone` sets the color; the <h1> gets `id` but neither `$tone` nor `$track`
10const heading = <Heading $tone="teal" $track="hero" id="intro" />
1const Title = styled.h1`
2  color: ${(p) => p.$tone};
3`
4
5function Heading(props) {
6  return <Title {...props} />
7}
8
9// `$tone` sets the color; the <h1> gets `id` but neither `$tone` nor `$track`
10const heading = <Heading $tone="teal" $track="hero" id="intro" />

Theme interpolation

ThemeProvider redefines CSS variables on a display: contents wrapper, so it scopes the theme to its subtree without touching layout. Interpolations that read the theme are resolved at build time — no runtime lookup:

1const Card = styled.div`
2  color: ${(p) => p.theme.brand};
3  border: 1px solid ${({ theme }) => theme.colors.line};
4`
1const Card = styled.div`
2  color: ${(p) => p.theme.brand};
3  border: 1px solid ${({ theme }) => theme.colors.line};
4`
1.a {
2  color: var(--brand);
3}
4.b {
5  border: 1px solid var(--colors-line);
6}
1.a {
2  color: var(--brand);
3}
4.b {
5  border: 1px solid var(--colors-line);
6}

Nested themes flatten with a hyphen, so theme.colors.line and --colors-line always agree. The object form resolves identically:

1const Card = styled('div')({ color: (p) => p.theme.brand })
1const Card = styled('div')({ color: (p) => p.theme.brand })

Reading the theme in JS gives the same references back:

1import { useTheme } from 'styled-components'
2
3function Divider() {
4  const theme = useTheme()
5  // `${theme.colors.line}` === 'var(--colors-line)'
6  return <hr style={{ borderColor: `${theme.colors.line}` }} />
7}
1import { useTheme } from 'styled-components'
2
3function Divider() {
4  const theme = useTheme()
5  // `${theme.colors.line}` === 'var(--colors-line)'
6  return <hr style={{ borderColor: `${theme.colors.line}` }} />
7}

Global styles

createGlobalStyle keeps its render site — the rules are lifted into the stylesheet and the call collapses to a component that renders nothing:

1const GlobalStyle = createGlobalStyle`body { margin: 0; }`
2// → const GlobalStyle = () => null
1const GlobalStyle = createGlobalStyle`body { margin: 0; }`
2// → const GlobalStyle = () => null

Emotion's <Global styles={...} /> behaves the same way: the styles prop is extracted and the element stops rendering markup.

The rest of the surface

| API | Result | | ---------------------------------------- | -------------------------------------------------------------------------------- | | css, keyframes | Devup UI equivalents | | ThemeProvider, useTheme, withTheme | @devup-ui/react/compat, CSS-variable backed | | createGlobalStyle, Global | extracted, render nothing | | ServerStyleSheet, StyleSheetManager | inert — Devup UI already emits a real stylesheet, so there is nothing to collect | | CacheProvider | renders its children; the cache it configures has nothing to hold | | isStyledComponent | always false | | css prop, jsx, JSX runtime pragmas | compiled to className and style | | ClassNames | replaced by what its child renders, each css / cx call compiled to classes | | Component selectors (${Child}) | a marker class on the selected component | | .attrs(), .withConfig() | attrs merged over props; build-time shouldForwardProp, other options ignored |

Limitations

Composition only the runtime sees — a className from props, an external class, a styled() base the build cannot read — follows CSS specificity and stylesheet order rather than the order you wrote. Styled components and classes defined in another module are not read as build-time values, stylis plugins are not applied, and test utilities and React Native targets are not supported. A component of your own that takes the css prop must pass on both className and style. See Supported Syntax & Limitations for the full list.