Modern Next.js apps default to the App Router and React Server Components (RSC). That unlocks streaming, data-fetching on the server, and better performance—but it also changes how runtime styling libraries plug in. This guide shows you how to use popular CSS-in-JS options (primarily styled-components and styled-jsx) in the app directory with Next.js 15. We’ll build up from a clean install, explain where Client Components are required, and add SSR-safe style injection so you avoid hydration flicker.
What you’ll build and why it works
Next.js 15 ships an SWC-based compiler that knows about common CSS-in-JS transforms and integrates with RSC. Styled-components v6 has an official pattern for collecting styles during server rendering and injecting them into <head>
as the HTML streams. Styled-jsx has a similar “style registry” pattern. Emotion support is improving via the compiler, but full, first-party App Router examples are still catching up; when you use Emotion, keep it in Client Components and enable the SWC transform for a smoother DX.
Create a new app with the latest Next.js (this will default to Next.js 15 and the App Router):
npx create-next-app@latest my-app
cd my-app
npm run dev
Next.js 15 includes React 19 support and a stable dev server, so the defaults are already tuned for the app directory.
Server vs Client Components: where CSS-in-JS fits
By default, files under app/ are Server Components. Most CSS-in-JS libraries render styles at runtime and therefore belong in Client Components. You’ll wrap your app once, near the root, with a small Client Component that collects and flushes styles during SSR; your leaf components can remain Server Components unless they use runtime styling or interactivity.
If you see errors like “createContext only works in Client Components,” the fix is to move that styled component into a Client Component boundary (add "use client"
at the top of the file or lift the styled element into a client wrapper).
Option A: styled-components v6 (recommended today)
1) Enable the compiler transform
Add the styled-components transform in next.config.js
:
/** @type {import('next').NextConfig} */
const nextConfig = {
compiler: {
styledComponents: true,
},
}
module.exports = nextConfig
This enables SWC transforms like displayName
and ssr
so class names are stable and styles can be extracted efficiently.
2) Create a style registry (Client Component)
Make a lib/styled-registry.tsx
:
'use client'
import React, { useState } from 'react'
import { useServerInsertedHTML } from 'next/navigation'
import { ServerStyleSheet, StyleSheetManager } from 'styled-components'
export default function StyledComponentsRegistry({
children,
}: {
children: React.ReactNode
}) {
// Create a single stylesheet instance for this render
const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet())
useServerInsertedHTML(() => {
const styles = styledComponentsStyleSheet.getStyleElement()
// Important: clear the sheet so styles aren’t duplicated between chunks
styledComponentsStyleSheet.instance.clearTag()
return <>{styles}</>
})
// On the client, styled-components takes over injection
if (typeof window !== 'undefined') return <>{children}</>
// During SSR/streaming, capture styles
return (
<StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
{children}
</StyleSheetManager>
)
}
This component collects styles during SSR and injects them into <head>
just before chunks stream to the browser.
3) Wrap your root layout
Edit app/layout.tsx
:
import StyledComponentsRegistry from '@/lib/styled-registry'
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'My App',
description: 'Next.js 15 + styled-components',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<StyledComponentsRegistry>{children}</StyledComponentsRegistry>
</body>
</html>
)
}
This keeps style extraction at the top of the tree and avoids bloating the Server Component payload with CSS.
4) Use it in components
You can keep most components as Server Components. Only components that create styled elements (i.e., call styled.button
inline) need to be client. A pragmatic pattern is to create small, reusable, client-side “primitive” components and import them from Server Components.
// app/(site)/Button.tsx
'use client'
import styled from 'styled-components'
export const Button = styled.button`
padding: 0.625rem 1rem;
border-radius: 0.5rem;
border: 0;
font-weight: 600;
cursor: pointer;
background: rgb(0 122 255);
color: white;
&:hover { opacity: 0.9; }
`
// app/(site)/page.tsx — server by default
import { Button } from './Button'
export default function Page() {
return <Button>Click me</Button>
}
Why this works: the registry handles SSR extraction, the compiler handles transform details, and only the Button itself is a Client Component, keeping most of your UI on the server.
Option B: styled-jsx (ships with Next.js)
If you want zero external dependencies but still prefer CSS-in-JS, styled-jsx works in Client Components with a similar registry.
Create app/registry.tsx
:
'use client'
import React, { useState } from 'react'
import { useServerInsertedHTML } from 'next/navigation'
import { StyleRegistry, createStyleRegistry } from 'styled-jsx'
export default function StyledJsxRegistry({ children }: { children: React.ReactNode }) {
const [jsxStyleRegistry] = useState(() => createStyleRegistry())
useServerInsertedHTML(() => {
const styles = jsxStyleRegistry.styles()
jsxStyleRegistry.flush()
return <>{styles}</>
})
return <StyleRegistry registry={jsxStyleRegistry}>{children}</StyleRegistry>
}
Wrap the root layout:
import StyledJsxRegistry from './registry'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<StyledJsxRegistry>{children}</StyledJsxRegistry>
</body>
</html>
)
}
Then write components as usual:
'use client'
export default function Card({ children }: { children: React.ReactNode }) {
return (
<div className="card">
{children}
<style jsx>{`
.card {
padding: 16px;
border: 1px solid #e5e5e5;
border-radius: 12px;
}
`}</style>
</div>
)
}
This follows the same collection/flush pattern as styled-components.
Option C: Emotion (status and safe usage)
Next.js’s compiler exposes an compiler.emotion
setting that handles common Emotion transforms (labels, source maps, etc.). App-directory SSR examples are still evolving; until there’s an official pattern mirroring the styled-components registry, prefer using Emotion inside Client Components and enable the transform for good DX. In next.config.js
:
/** @type {import('next').NextConfig} */
const nextConfig = {
compiler: {
emotion: {
// sensible dev defaults; these are ignored in production
sourceMap: true,
autoLabel: 'dev-only',
labelFormat: '[local]',
},
},
}
module.exports = nextConfig
Then use Emotion in a client component:
'use client'
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'
export default function Badge({ children }: { children: React.ReactNode }) {
return (
<span
css={css`
display: inline-block;
padding: 2px 8px;
border-radius: 9999px;
background: rgba(0, 122, 255, 0.1);
color: rgb(0, 122, 255);
font-weight: 600;
`}
>
{children}
</span>
)
}
If you absolutely need SSR critical-CSS extraction with Emotion in the app directory, keep an eye on updated examples as compiler integration matures; for now, many teams choose styled-components for SSR-heavy pages and use Emotion for purely client-side areas.
Theming and globals with styled-components
You can provide a theme once and consume it in client components. Add a theme file:
// theme.ts
export const theme = {
colors: {
brand: 'rgb(0 122 255)',
text: '#111827',
},
}
Set up a themed provider:
'use client'
import { ThemeProvider, createGlobalStyle } from 'styled-components'
import { theme } from '@/theme'
const GlobalStyle = createGlobalStyle`
:root { color-scheme: light; }
html, body { margin: 0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Inter, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; }
`
export function AppTheme({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider theme={theme}>
<GlobalStyle />
{children}
</ThemeProvider>
)
}
Mount it right below your registry:
import StyledComponentsRegistry from '@/lib/styled-registry'
import { AppTheme } from '@/lib/theme-provider'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<StyledComponentsRegistry>
<AppTheme>{children}</AppTheme>
</StyledComponentsRegistry>
</body>
</html>
)
}
Styled-components’ SSR extraction handles createGlobalStyle
cleanly, so global CSS renders with the initial HTML and avoids flicker.
Troubleshooting and performance notes
Next’s compiler handles minification and transforms by default; you don’t need Babel or Terser here, and the swcMinify
flag has been removed in v15. If you add a .babelrc
, Next.js will fall back to Babel for file transforms, which can slow builds and complicate CSS-in-JS plugins—avoid unless you truly need custom Babel behavior.
If hydration mismatches appear only in development, verify that your styled components live inside a Client Component boundary and that you’re not accidentally sharing a stale style registry across requests. Keeping the registry component as shown above ensures styles are flushed per render.
Where UI libraries fit
Libraries like MUI, Chakra, Tamagui, and PandaCSS can run in Client Components under the app directory today. MUI and Chakra (Emotion-based) typically mount a client provider at the root and render fine with the app router; if you need strict SSR critical-CSS for first paint, styled-components currently has the most direct App Router recipe in the official docs.
What you have now and next steps
You’ve configured a Next.js 15 app to use CSS-in-JS in the app directory with SSR-safe style injection:
- styled-components v6 with the compiler transform and a head-inserting registry
- styled-jsx with a similar registry pattern
- Emotion with compiler support for labels and DX, scoped to Client Components until first-party SSR examples land
From here, add your design system, split Server/Client boundaries deliberately, and measure before you micro-optimize. When you’re ready to host, commit to a repo and hook it up to your cloud of choice—the steps are straightforward if you already keep a DigitalOcean account for App Platform.