CSS-in-JS Overview
.css files. Libraries like styled-components and Emotion are the most widely used implementations, and both work by generating real CSS at build time or runtime and injecting it into the page, with scoped, auto-generated class names attached to each component.sx prop and styled MUI component you see rendered on this page went through a CSS-in-JS pipeline before it reached the browser — it's not a purely theoretical topic here.What it looks like
The exact syntax differs by library, but the core idea is always the same: a tagged template literal or a style object lives inside the component file, and the library turns it into a real CSS rule with a unique, collision-free class name.
// styled-components
import styled from 'styled-components'
const Card = styled.div`
padding: 1rem;
border-radius: 8px;
background: ${(props) => (props.$featured ? '#fff8e1' : '#ffffff')};
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.16);
}
`
function ProductCard({ featured, children }) {
return <Card $featured={featured}>{children}</Card>
}// Emotion, using the css prop
import { css } from '@emotion/react'
function ProductCard({ featured, children }) {
return (
<div
css={css`
padding: 1rem;
border-radius: 8px;
background: ${featured ? '#fff8e1' : '#ffffff'};
`}
>
{children}
</div>
)
}featured) directly — that's the core capability CSS-in-JS is built around, and it's difficult to replicate as cleanly with a plain .css file.Why teams adopt it
Benefit | What it means in practice |
|---|---|
Co-location | A component and its styles live in the same file, so there is no separate stylesheet to keep in sync, hunt down, or accidentally leave orphaned when the component is deleted. |
Dynamic styles from props/state | Styles can read component props or state directly, producing values a static CSS file cannot compute — no need to toggle a class name from JS just to swap a color. |
Automatic scoping | Generated class names are unique per style block, which removes the naming-collision problem that motivated methodologies like BEM in a plain-CSS world. |
Dead style elimination | Because styles are tied to component code, unused components and their styles tend to get removed together rather than leaving stale CSS behind. |
The trade-offs
None of this is free. Traditional, runtime CSS-in-JS libraries parse template literals or style objects and inject <style> tags into the page while the app is running, which adds real JavaScript execution cost on every render pass that touches styles — worse on low-end devices, and worse the more dynamic the styles are.
Runtime performance cost — style computation and injection happen in the browser, on the main thread, competing with everything else your app is doing.
Extra build tooling — babel/SWC plugins, bundler configuration, and sometimes server-side rendering extraction steps are needed to avoid a flash of unstyled content.
Larger JavaScript bundles — the styling library itself, plus the serialized style logic for every component, ships as JS rather than a separately cacheable CSS file.
Debugging friction — generated class names in DevTools are often opaque hashes rather than something you wrote, though most libraries offer a debug mode that improves this.
.css files at build time, so nothing runs in the browser at render time. This "zero-runtime CSS-in-JS" category is still maturing and doesn't yet support every dynamic-styling pattern the runtime libraries do.box-sizing: border-box, there is real, ongoing disagreement among experienced teams about whether CSS-in-JS, CSS Modules, or utility-first CSS is the best default choice for a given project. The right answer depends on team size, rendering strategy (especially server-side rendering performance budgets), and how dynamic the styling genuinely needs to be — treat any strong one-size-fits-all opinion you read online with some skepticism.