emotion, styled-components, styled-system, and theme-ui are JavaScript libraries that enable dynamic, component-scoped styling in React applications using CSS-in-JS patterns. They support theming, responsive design, and design system constraints, but differ significantly in architecture, API surface, and integration approach. emotion and styled-components focus on runtime or compile-time CSS generation with tagged template literals or object styles, while styled-system provides a utility-based prop system for applying design tokens (like spacing, colors, and typography) to components. theme-ui combines styled-system's prop-based API with Emotion under the hood, offering a full opinionated framework for building themeable UIs with Markdown support and built-in accessibility features.
When building modern React applications with consistent design systems, developers often reach for CSS-in-JS solutions that go beyond basic inline styles. The libraries emotion, styled-components, styled-system, and theme-ui each offer different philosophies for managing styles, themes, and responsive behavior. Let’s compare how they solve real-world problems.
emotion supports multiple syntaxes — you can use tagged templates or object styles, with or without React.
// emotion: string syntax
import { css } from '@emotion/react';
const button = css`
background: blue;
padding: ${props => props.theme.space[2]}px;
`;
// emotion: object syntax
const card = css({
borderRadius: '4px',
boxShadow: t => t.shadows[1]
});
styled-components uses only tagged template literals and requires React.
// styled-components: only string templates
import styled from 'styled-components';
const Button = styled.button`
background: ${props => props.theme.colors.primary};
padding: ${props => props.theme.space[2]}px;
`;
styled-system doesn’t render CSS — it provides functions that return style objects based on props.
// styled-system: style functions
import { space, color } from 'styled-system';
const Box = props => <div style={{ ...space(props), ...color(props) }} />;
// Usage: <Box p={3} bg="primary" />
theme-ui uses Emotion under the hood but exposes styled-system props directly on JSX elements.
// theme-ui: prop-based styling
/** @jsxImportSource theme-ui */
<div sx={{ p: 3, bg: 'primary', borderRadius: 2 }} />
All four support theming, but the mechanics differ.
emotion uses a ThemeContext but doesn’t require it — you can pass themes manually or use the useTheme hook.
// emotion theming
import { ThemeProvider, useTheme } from '@emotion/react';
const ThemedButton = () => {
const theme = useTheme();
return <button css={{ color: theme.colors.primary }} />;
};
styled-components requires wrapping your app in a ThemeProvider to access theme values inside styled components.
// styled-components theming
import { ThemeProvider } from 'styled-components';
const Button = styled.button`
color: ${props => props.theme.colors.primary};
`;
styled-system expects a theme object to be passed as props or via context (but doesn’t provide the context itself).
// styled-system theming (manual)
const theme = { space: [0, 4, 8, 16], colors: { primary: 'blue' } };
<Box theme={theme} p={2} color="primary" />
theme-ui mandates a global ThemeProvider and uses a strict theme specification (based on System UI Theme Spec).
// theme-ui theming
import { ThemeProvider } from 'theme-ui';
const theme = { space: [0, 4, 8], colors: { primary: 'tomato' } };
<ThemeProvider theme={theme}>
<div sx={{ color: 'primary' }} />
</ThemeProvider>
emotion and styled-components handle responsiveness through custom logic in their style definitions.
// emotion responsive
const Card = styled.div`
padding: 8px;
@media (min-width: 768px) {
padding: 16px;
}
`;
// Or with object syntax
const card = css({
padding: 8,
'@media (min-width: 768px)': { padding: 16 }
});
styled-system introduces array-based responsive props.
// styled-system responsive
<Box width={[1, 1/2, 1/3]} /> // full width on mobile, half on tablet, third on desktop
theme-ui inherits this array syntax and extends it with the sx prop.
// theme-ui responsive
<div sx={{ width: ['100%', '50%', '33%'] }} />
emotion supports zero-runtime CSS extraction via Babel plugin — styles become static <style> tags with no JavaScript overhead in production.
// With @emotion/babel-plugin, this becomes static CSS
const Button = styled.button`
color: hotpink;
`;
styled-components always ships runtime code to interpolate dynamic styles and manage component IDs. No zero-runtime option exists.
styled-system is pure utility functions — no runtime cost beyond function calls, but you still pay the cost of your underlying CSS-in-JS engine.
theme-ui uses Emotion internally, so it can benefit from Emotion’s zero-runtime mode if configured, but by default runs in runtime mode.
emotion is framework-agnostic — it works in React, Vue, vanilla JS, or even server-side rendering without React.
styled-components is React-only and deeply tied to React’s context and lifecycle.
styled-system is completely unopinionated about rendering — it’s just a set of functions that return style objects. You can use it with Emotion, Styled Components, or even plain React style props.
theme-ui is React-focused and includes extra features like MDXProvider for styling Markdown content consistently with your theme.
// theme-ui + MDX
import { MDXProvider } from '@mdx-js/react';
import { useThemeUI } from 'theme-ui';
const components = {
h1: props => <h1 sx={{ fontSize: 5, color: 'primary' }} {...props} />
};
<MDXProvider components={components}>
<MarkdownContent />
</MDXProvider>
As of 2024, all four packages are actively maintained:
emotion is widely used in large ecosystems (including MUI)styled-components remains popular with steady updatesstyled-system is stable and foundational to many design systemstheme-ui continues to evolve with support for modern React featuresNone are deprecated, but note that styled-system is not a standalone styling solution — it must be combined with a CSS-in-JS library.
| Feature | emotion | styled-components | styled-system | theme-ui |
|---|---|---|---|---|
| Styling Syntax | Strings or objects | Strings only | Style functions (objects) | sx prop (object + arrays) |
| Framework Support | Any (React optional) | React only | Any | React only |
| Theming Required? | No | Yes (for dynamic styles) | No (but expected) | Yes |
| Zero-Runtime Mode | ✅ (with Babel plugin) | ❌ | N/A (depends on engine) | ✅ (via Emotion) |
| Responsive Props | Manual media queries | Manual media queries | ✅ (array syntax) | ✅ (array syntax in sx) |
| Best For | Flexibility & performance | Pure component styling | Building custom design libs | Content sites with MDX |
Remember: styled-system is not a replacement for the others — it’s a complementary layer. In fact, theme-ui is essentially “Emotion + Styled System + conventions.” Choose based on whether you want flexibility (emotion), purity (styled-components), composability (styled-system), or convention (theme-ui).
Choose styled-components if you prefer a purely component-centric styling model with strong encapsulation and automatic vendor prefixing. It enforces a clear boundary between styled components and logic components, and its theming API is tightly integrated via React context. However, it requires a ThemeProvider for theme access and has slightly higher runtime overhead due to its reliance on React context for every styled component.
Choose emotion if you need maximum flexibility in styling approaches — it supports both string-based and object-based styles, works with or without React, and offers zero-runtime extraction via @emotion/babel-plugin. It’s ideal for performance-sensitive applications or when integrating into non-React environments, as it doesn’t require a ThemeProvider for basic usage and allows fine-grained control over CSS injection and caching.
Choose styled-system if you’re building a design system from scratch and want low-level, composable style props (like space, color, fontSize) that map directly to theme scales. It’s framework-agnostic and works with any component library, but it doesn’t generate CSS by itself — you must pair it with a CSS-in-JS engine like Emotion or Styled Components. Avoid using it alone; it’s a utility layer, not a complete styling solution.
Choose theme-ui if you want an opinionated, all-in-one solution that combines Emotion, styled-system props, theme-based configuration, and MDX support out of the box. It’s excellent for content-heavy sites (like blogs or documentation) where you need consistent styling across both React components and Markdown-rendered content. However, its conventions may feel restrictive if you need deep customization beyond its prop-based API.
styled-components is largely maintained by one person. Please help fund the project for consistent long-term support and updates: Open Collective
Style React components with real CSS, scoped automatically and delivered only when needed. No class name juggling, no separate files, no build step required.
@types install, no manual generics.npm install styled-components
pnpm add styled-components
yarn add styled-components
Vary styles based on component props. Prefix transient props with $ to keep them off the DOM.
import styled from 'styled-components';
const Button = styled.button<{ $primary?: boolean }>`
background: ${props => (props.$primary ? 'palevioletred' : 'white')};
color: ${props => (props.$primary ? 'white' : 'palevioletred')};
font-size: 1em;
padding: 0.25em 1em;
border: 2px solid palevioletred;
border-radius: 3px;
`;
<Button>Normal</Button>
<Button $primary>Primary</Button>
Build variants on top of existing styled components.
const TomatoButton = styled(Button)`
background: tomato;
color: white;
border-color: tomato;
`;
as propSwap the rendered element without changing styles.
// Renders a <a> tag with Button styles
<Button as="a" href="/home">Link Button</Button>
Use & to reference the component's generated class name—works with pseudo-classes, pseudo-elements, and nested selectors.
const Input = styled.input`
border: 1px solid #ccc;
border-radius: 4px;
padding: 0.5em;
&:focus {
border-color: palevioletred;
outline: none;
}
&::placeholder {
color: #aaa;
}
`;
Define @keyframes once, reference them across components. Names are scoped automatically.
import styled, { keyframes } from 'styled-components';
const rotate = keyframes`
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
`;
const Spinner = styled.div`
animation: ${rotate} 1s linear infinite;
width: 40px;
height: 40px;
border: 3px solid palevioletred;
border-top-color: transparent;
border-radius: 50%;
`;
Share design tokens across your app via React context. Every styled component receives props.theme.
import styled, { ThemeProvider } from 'styled-components';
const theme = {
fg: 'palevioletred',
bg: 'white',
};
const Card = styled.div`
background: ${props => props.theme.bg};
color: ${props => props.theme.fg};
padding: 2em;
`;
<ThemeProvider theme={theme}>
<Card>Themed content</Card>
</ThemeProvider>
createTheme turns your tokens into CSS custom properties. Class name hashes stay stable across theme variants—no hydration mismatch when switching light/dark.
import styled, { createTheme, ThemeProvider } from 'styled-components';
const { theme, GlobalStyle: ThemeVars } = createTheme({
colors: {
fg: 'palevioletred',
bg: 'white',
},
space: {
md: '1rem',
},
});
const Card = styled.div`
color: ${theme.colors.fg}; /* var(--sc-colors-fg, palevioletred) */
background: ${theme.colors.bg};
padding: ${theme.space.md};
`;
// Render <ThemeVars /> at the root to emit the CSS variable declarations
// Pass the theme to ThemeProvider for stable hashes
<ThemeProvider theme={theme}>
<ThemeVars />
<Card>Token-driven content</Card>
</ThemeProvider>
cssExtract reusable style blocks to share across components or apply conditionally.
import styled, { css } from 'styled-components';
const truncate = css`
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
const Label = styled.span`
${truncate}
max-width: 200px;
`;
Wrap any component that accepts a className prop.
import styled from 'styled-components';
import { Link } from 'react-router-dom';
const StyledLink = styled(Link)`
color: palevioletred;
text-decoration: none;
&:hover {
text-decoration: underline;
}
`;
Inject app-wide CSS like resets and font faces. Supports theming and dynamic updates.
import { createGlobalStyle } from 'styled-components';
const GlobalStyle = createGlobalStyle`
body {
margin: 0;
font-family: system-ui, sans-serif;
}
`;
// Render <GlobalStyle /> at the root of your app
Set default or static HTML attributes so consumers don't have to.
const PasswordInput = styled.input.attrs({
type: 'password',
placeholder: 'Enter password',
})`
border: 1px solid #ccc;
padding: 0.5em;
`;
Contributing guidelines | Code of Conduct | awesome-styled-components
This project exists thanks to all the people who contribute.
Thank you to all our backers! [Become a backer]
Support this project by becoming a sponsor. [Become a sponsor]
This project builds on earlier work by Charlie Somerville, Nik Graf, Sunil Pai, Michael Chan, Andrey Popp, Jed Watson, and Andrey Sitnik. Special thanks to @okonet for the logo.
Licensed under the MIT License, Copyright © 2016-present styled-components contributors. See LICENSE for details.