「styled-componentsはオワコン?」「vanilla-extractとPanda CSSってどう違う?」「React Server Components時代にCSS-in-JSは使えるの?」——CSS-in-JSの世界は2024年以降、runtime / zero-runtime / atomicという3つの軸で大きく塗り替えられました。本記事は2026年時点で実務に直結する styled-components / Emotion / vanilla-extract / Panda CSS / CSS Modules の徹底比較ガイドです。各ライブラリで動く実コード30本以上を、Next.js App Router対応・SSR戦略・パフォーマンス測定まで含めて解説します。
※ Tailwind CSSとの使い分けについては姉妹記事 frameworks一覧 をご参照ください。本記事は「Tailwindでは表現しきれないコンポーネントスタイル設計」を扱います。
- CSS-in-JSの全体像 — 2026年の地図
- styled-components 完全実装 — v6時代の正しい使い方
- Emotion 完全実装 — Material UI/Chakra UIの土台
- vanilla-extract — TypeScript-firstのzero-runtime王
- Panda CSS — atomic + recipeのハイブリッド
- CSS Modules — 古くて新しい王道
- 過去の主役たち — Linaria / Stitches / JSS
- パフォーマンス比較 — bundle size と runtime cost
- エコシステムとの関係 — shadcn/ui / MUI / Chakra / Mantine
- 2026年の選び方 — フローチャート
- 移行ガイド — styled-components → vanilla-extract
- 学習ロードマップと案件獲得
- まとめ — 2026年のCSS-in-JS地図
CSS-in-JSの全体像 — 2026年の地図
「CSS-in-JS」と一口に言っても、内部実装は4世代に分かれます。まずこの分類を理解すると、各ライブラリの立ち位置がクリアになります。
4世代分類:runtime → zero-runtime → atomic → server-first
- 第1世代(runtime): styled-components、Emotion、Stitches — ブラウザでスタイル生成
- 第2世代(zero-runtime): Linaria、vanilla-extract、Compiled — ビルド時にCSS抽出
- 第3世代(atomic + zero-runtime): Panda CSS、UnoCSS — ユーティリティ生成
- 第4世代(server-first): Next.js App Router対応を前提とした設計
RSC(React Server Components)が変えたもの
2023年以降、Next.js App RouterのRSCで runtime CSS-in-JSは Server Component 内では動かない という制約が生まれました。これにより市場は急速にzero-runtimeへ流れています。
// ❌ Server Component で styled-components を直接使うとエラー
// app/page.tsx (Server Component)
import styled from 'styled-components'
const Title = styled.h1`color: red;` // ← Build error
export default function Page() {
return <Title>Hello</Title>
}
// ✅ Client Component に隔離すれば動く(が SSR で flash 問題)
'use client'
import styled from 'styled-components'
const Title = styled.h1`color: red;`
export default Title
本記事で扱う5つのライブラリの位置づけ
| ライブラリ | 世代 | RSC対応 | 2026推奨度 |
|---|---|---|---|
| styled-components v6 | runtime | △(Client限定) | ★★☆☆☆ |
| Emotion | runtime | △(Client限定) | ★★★☆☆ |
| vanilla-extract | zero-runtime | ◎ | ★★★★★ |
| Panda CSS | atomic zero-runtime | ◎ | ★★★★★ |
| CSS Modules | build-time | ◎ | ★★★★☆ |
styled-components 完全実装 — v6時代の正しい使い方
2024年にv6がリリースされ、Next.js App Router対応とBabelプラグイン廃止・SWC対応が大幅に進みました。ただしRuntimeの宿命でRSC内では使えません。
インストールと基本セットアップ
npm install styled-components
npm install -D @types/styled-components
基本コード:タグ付きテンプレートリテラル
'use client'
import styled from 'styled-components'
export const Button = styled.button`
background: #3b82f6;
color: white;
padding: 8px 16px;
border-radius: 8px;
border: none;
cursor: pointer;
&:hover {
background: #2563eb;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
`
Propsで動的スタイル
'use client'
import styled, { css } from 'styled-components'
type Props = { variant?: 'primary' | 'danger'; size?: 'sm' | 'md' | 'lg' }
export const Button = styled.button<Props>`
padding: ${({ size }) =>
size === 'sm' ? '4px 8px' : size === 'lg' ? '12px 24px' : '8px 16px'};
background: ${({ variant }) => (variant === 'danger' ? '#ef4444' : '#3b82f6')};
color: white;
border: none;
border-radius: 6px;
${({ variant }) =>
variant === 'danger' &&
css`
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.3);
`}
`
ThemeProvider と Theme型定義
// theme.ts
export const theme = {
colors: {
primary: '#3b82f6',
danger: '#ef4444',
text: '#1f2937',
},
spacing: (n: number) => `${n * 4}px`,
} as const
// styled.d.ts
import 'styled-components'
import type { theme } from './theme'
declare module 'styled-components' {
export interface DefaultTheme extends Omit<typeof theme, never> {}
}
'use client'
import { ThemeProvider } from 'styled-components'
import { theme } from './theme'
export function Providers({ children }: { children: React.ReactNode }) {
return <ThemeProvider theme={theme}>{children}</ThemeProvider>
}
'use client'
import styled from 'styled-components'
export const Title = styled.h1`
color: ${({ theme }) => theme.colors.primary};
padding: ${({ theme }) => theme.spacing(4)};
`
Next.js App Router 向け SSR セットアップ
// app/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
}) {
const [sheet] = useState(() => new ServerStyleSheet())
useServerInsertedHTML(() => {
const styles = sheet.getStyleElement()
sheet.instance.clearTag()
return <>{styles}</>
})
if (typeof window !== 'undefined') return <>{children}</>
return (
<StyleSheetManager sheet={sheet.instance}>{children}</StyleSheetManager>
)
}
// app/layout.tsx
import StyledComponentsRegistry from './registry'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<StyledComponentsRegistry>{children}</StyledComponentsRegistry>
</body>
</html>
)
}
styled-componentsの「2026年における立ち位置」
RSC時代のNext.js App Routerでは 全ページで ‘use client’ バウンダリが下がる という根本問題があり、新規採用は推奨されません。既存プロジェクトの保守用と割り切るのが現実解です。
Emotion 完全実装 — Material UI/Chakra UIの土台
EmotionはMaterial UI(v5以降)・Chakra UI(v2)・Mantine(v6まで)が内部採用してきたデファクトのrumtime CSS-in-JSです。Reactのcss propを書ける独自構文が最大の特徴。
インストール:2つのAPIから選ぶ
# styled API
npm install @emotion/styled @emotion/react
# css prop API + Babel/SWC プラグイン
npm install @emotion/react
npm install -D @emotion/babel-preset-css-prop
styled API:styled-componentsとほぼ同じ
'use client'
import styled from '@emotion/styled'
export const Card = styled.div`
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
`
css prop:Emotion最大の武器
/** @jsxImportSource @emotion/react */
'use client'
import { css } from '@emotion/react'
const cardStyle = css`
background: white;
border-radius: 12px;
padding: 24px;
&:hover {
transform: translateY(-2px);
}
`
export function Card({ children }: { children: React.ReactNode }) {
return <div css={cardStyle}>{children}</div>
}
Object Style記法 — TypeScript親和性が高い
'use client'
import { css } from '@emotion/react'
const button = (variant: 'primary' | 'danger') =>
css({
padding: '8px 16px',
background: variant === 'danger' ? '#ef4444' : '#3b82f6',
color: 'white',
border: 'none',
borderRadius: 6,
'&:hover': { opacity: 0.9 },
})
export const Button = ({ variant = 'primary', ...props }) => (
<button css={button(variant)} {...props} />
)
Theme:useThemeで型安全に
// emotion.d.ts
import '@emotion/react'
declare module '@emotion/react' {
export interface Theme {
colors: { primary: string; bg: string; text: string }
radius: { sm: number; md: number; lg: number }
}
}
// theme.ts
export const theme = {
colors: { primary: '#3b82f6', bg: '#f9fafb', text: '#111827' },
radius: { sm: 4, md: 8, lg: 16 },
}
'use client'
import { ThemeProvider, useTheme, css } from '@emotion/react'
import { theme } from './theme'
function Title() {
const t = useTheme()
return <h1 css={css({ color: t.colors.primary })}>Hi</h1>
}
export default function App() {
return (
<ThemeProvider theme={theme}>
<Title />
</ThemeProvider>
)
}
Emotion + TypeScript:厳密型でprops設計
'use client'
import styled from '@emotion/styled'
import type { ComponentProps } from 'react'
type ButtonProps = ComponentProps<'button'> & {
variant?: 'primary' | 'ghost'
fullWidth?: boolean
}
export const Button = styled.button<ButtonProps>(({ variant, fullWidth }) => ({
width: fullWidth ? '100%' : 'auto',
padding: '10px 16px',
background: variant === 'ghost' ? 'transparent' : '#3b82f6',
color: variant === 'ghost' ? '#3b82f6' : 'white',
border: variant === 'ghost' ? '1px solid #3b82f6' : 'none',
borderRadius: 8,
cursor: 'pointer',
}))
Next.js App Router 向け SSR セットアップ
// app/emotion-registry.tsx
'use client'
import { useState } from 'react'
import { useServerInsertedHTML } from 'next/navigation'
import createCache from '@emotion/cache'
import { CacheProvider } from '@emotion/react'
export default function EmotionRegistry({
children,
}: {
children: React.ReactNode
}) {
const [{ cache, flush }] = useState(() => {
const cache = createCache({ key: 'em' })
cache.compat = true
const prevInsert = cache.insert
let inserted: string[] = []
cache.insert = (...args) => {
const serialized = args[1]
if (cache.inserted[serialized.name] === undefined) inserted.push(serialized.name)
return prevInsert(...args)
}
const flush = () => {
const prev = inserted
inserted = []
return prev
}
return { cache, flush }
})
useServerInsertedHTML(() => {
const names = flush()
if (names.length === 0) return null
let styles = ''
for (const name of names) styles += cache.inserted[name]
return (
<style
data-emotion={`${cache.key} ${names.join(' ')}`}
dangerouslySetInnerHTML={{ __html: styles }}
/>
)
})
return <CacheProvider value={cache}>{children}</CacheProvider>
}
vanilla-extract — TypeScript-firstのzero-runtime王
vanilla-extractは 「.css.tsファイルにスタイルを書く → ビルド時にCSSファイルに抽出」 という手法で、ランタイム0バイトを実現します。TypeScriptで完全な型安全。
インストールとVite/Next.js統合
npm install @vanilla-extract/css
npm install -D @vanilla-extract/vite-plugin
# Next.js
npm install -D @vanilla-extract/next-plugin
// vite.config.ts
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin'
export default { plugins: [vanillaExtractPlugin()] }
// next.config.ts
import { createVanillaExtractPlugin } from '@vanilla-extract/next-plugin'
const withVE = createVanillaExtractPlugin()
export default withVE({})
基本:style() で 1スタイル1クラス
// button.css.ts
import { style } from '@vanilla-extract/css'
export const button = style({
padding: '8px 16px',
background: '#3b82f6',
color: 'white',
border: 'none',
borderRadius: 8,
cursor: 'pointer',
':hover': { background: '#2563eb' },
':disabled': { opacity: 0.5, cursor: 'not-allowed' },
})
// Button.tsx — RSCでも動く!
import { button } from './button.css'
export function Button(props: React.ComponentProps<'button'>) {
return <button className={button} {...props} />
}
Theme(globalTheme + createTheme)
// theme.css.ts
import { createGlobalTheme, createTheme } from '@vanilla-extract/css'
export const vars = createGlobalTheme(':root', {
color: { primary: '#3b82f6', bg: '#ffffff', text: '#111827' },
space: { 1: '4px', 2: '8px', 3: '12px', 4: '16px', 5: '24px' },
})
export const [darkClass, darkVars] = createTheme(vars, {
color: { primary: '#60a5fa', bg: '#111827', text: '#f9fafb' },
space: vars.space,
})
// card.css.ts
import { style } from '@vanilla-extract/css'
import { vars } from './theme.css'
export const card = style({
background: vars.color.bg,
color: vars.color.text,
padding: vars.space[4],
borderRadius: 12,
})
recipes — variant付きコンポーネント
npm install @vanilla-extract/recipes
// button.css.ts
import { recipe } from '@vanilla-extract/recipes'
export const button = recipe({
base: {
display: 'inline-flex',
alignItems: 'center',
border: 'none',
cursor: 'pointer',
borderRadius: 8,
},
variants: {
color: {
primary: { background: '#3b82f6', color: 'white' },
danger: { background: '#ef4444', color: 'white' },
ghost: { background: 'transparent', color: '#3b82f6', border: '1px solid #3b82f6' },
},
size: {
sm: { padding: '4px 8px', fontSize: 12 },
md: { padding: '8px 16px', fontSize: 14 },
lg: { padding: '12px 24px', fontSize: 16 },
},
},
defaultVariants: { color: 'primary', size: 'md' },
})
// Button.tsx
import { button } from './button.css'
import type { RecipeVariants } from '@vanilla-extract/recipes'
type ButtonProps = React.ComponentProps<'button'> & RecipeVariants<typeof button>
export const Button = ({ color, size, ...props }: ButtonProps) => (
<button className={button({ color, size })} {...props} />
)
Sprinkles — atomic CSS生成
npm install @vanilla-extract/sprinkles
// sprinkles.css.ts
import { defineProperties, createSprinkles } from '@vanilla-extract/sprinkles'
const responsive = defineProperties({
conditions: {
mobile: {},
tablet: { '@media': 'screen and (min-width: 768px)' },
desktop: { '@media': 'screen and (min-width: 1024px)' },
},
defaultCondition: 'mobile',
properties: {
display: ['none', 'flex', 'block', 'grid'],
padding: { 1: 4, 2: 8, 3: 12, 4: 16, 5: 24 },
gap: { 1: 4, 2: 8, 3: 12, 4: 16 },
},
})
export const sprinkles = createSprinkles(responsive)
export type Sprinkles = Parameters<typeof sprinkles>[0]
// 使用例
import { sprinkles } from './sprinkles.css'
export function Stack({ children }: { children: React.ReactNode }) {
return (
<div
className={sprinkles({
display: 'flex',
padding: { mobile: 2, tablet: 4 },
gap: 3,
})}
>
{children}
</div>
)
}
vanilla-extractの強み
- ランタイム0バイト = bundle size に影響なし
- RSCで完全動作(Server Componentでも import 可)
- TypeScript完全型安全(タイポは型エラー)
- SSR/Hydration mismatch問題が原理的に起きない
Panda CSS — atomic + recipeのハイブリッド
Panda CSSは Chakra UI開発チームが2024年に公開した 次世代CSS-in-JS。「TailwindのDXをCSS-in-JSの型安全さで」というコンセプトで、atomic CSSをビルド時に生成します。
インストールと初期化
npm install -D @pandacss/dev
npx panda init --postcss
// panda.config.ts
import { defineConfig } from '@pandacss/dev'
export default defineConfig({
preflight: true,
include: ['./src/**/*.{js,jsx,ts,tsx}'],
exclude: [],
theme: {
extend: {
tokens: {
colors: {
primary: { value: '#3b82f6' },
danger: { value: '#ef4444' },
},
},
},
},
outdir: 'styled-system',
})
// package.json
{
"scripts": {
"prepare": "panda codegen"
}
}
基本:css 関数 + atomic CSS
// Button.tsx
import { css } from '../styled-system/css'
export function Button() {
return (
<button
className={css({
bg: 'primary',
color: 'white',
px: 4,
py: 2,
rounded: 'md',
_hover: { opacity: 0.9 },
})}
>
Click
</button>
)
}
Recipe(cva風)
// recipes/button.ts
import { cva } from '../styled-system/css'
export const button = cva({
base: {
display: 'inline-flex',
alignItems: 'center',
border: 'none',
rounded: 'md',
cursor: 'pointer',
},
variants: {
visual: {
solid: { bg: 'primary', color: 'white' },
outline: { border: '1px solid', borderColor: 'primary', color: 'primary' },
},
size: {
sm: { px: 2, py: 1, fontSize: 'sm' },
md: { px: 4, py: 2, fontSize: 'md' },
lg: { px: 6, py: 3, fontSize: 'lg' },
},
},
defaultVariants: { visual: 'solid', size: 'md' },
})
// Button.tsx
import { button } from './recipes/button'
export const Button = ({ visual, size, ...props }) => (
<button className={button({ visual, size })} {...props} />
)
Theming with Tokens + Semantic Tokens
// panda.config.ts
export default defineConfig({
theme: {
tokens: {
colors: {
gray: {
50: { value: '#f9fafb' },
900: { value: '#111827' },
},
},
},
semanticTokens: {
colors: {
bg: {
DEFAULT: { value: { base: '{colors.gray.50}', _dark: '{colors.gray.900}' } },
},
text: {
DEFAULT: { value: { base: '{colors.gray.900}', _dark: '{colors.gray.50}' } },
},
},
},
},
conditions: { dark: '.dark &' },
})
// 使用例 — ダークモード自動切替
import { css } from '../styled-system/css'
export const App = () => (
<div className={css({ bg: 'bg', color: 'text', minH: 'screen', p: 4 })}>
自動でdark対応
</div>
)
JSX Style Props(任意機能)
// panda.config.ts に jsxFramework: 'react' を追加すると…
import { styled } from '../styled-system/jsx'
export const Card = styled('div', {
base: { p: 4, rounded: 'lg', bg: 'white', shadow: 'md' },
})
// 使用側
<Card mt={4} bg="primary" /> {/* JSX prop でスタイル指定可 */}
CSS Modules — 古くて新しい王道
CSS Modulesは 2015年に生まれた 最古参の手法ですが、Vite・Next.jsの標準サポートと RSC対応・パフォーマンスの圧倒的な軽さ から2026年に再評価が進んでいます。
基本:1ファイル1スコープ
/* Button.module.css */
.button {
padding: 8px 16px;
background: #3b82f6;
color: white;
border: none;
border-radius: 8px;
}
.button:hover {
background: #2563eb;
}
.danger {
background: #ef4444;
}
// Button.tsx
import styles from './Button.module.css'
export function Button({ danger }: { danger?: boolean }) {
return (
<button className={`${styles.button} ${danger ? styles.danger : ''}`}>
Click
</button>
)
}
clsx / classnames で見やすく
import clsx from 'clsx'
import styles from './Button.module.css'
export function Button({ danger, fullWidth }: { danger?: boolean; fullWidth?: boolean }) {
return (
<button
className={clsx(styles.button, {
[styles.danger]: danger,
[styles.fullWidth]: fullWidth,
})}
/>
)
}
TypeScript型安全(typed-css-modules / Vite built-in)
npm install -D typed-css-modules
npx tcm src --watch
// 自動生成された Button.module.css.d.ts
declare const styles: {
readonly button: string
readonly danger: string
readonly fullWidth: string
}
export default styles
Vite での CSS Modules
// vite.config.ts
export default {
css: {
modules: {
localsConvention: 'camelCaseOnly',
generateScopedName: '[name]__[local]___[hash:base64:5]',
},
},
}
composes — 再利用
/* base.module.css */
.box { padding: 16px; border-radius: 8px; }
/* card.module.css */
.card {
composes: box from './base.module.css';
background: white;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
過去の主役たち — Linaria / Stitches / JSS
Linaria — Wix発のzero-runtime
import { styled } from '@linaria/react'
// ビルド時にCSSファイル抽出。styled-components構文がそのまま動く
export const Title = styled.h1`
color: ${(props) => props.color};
font-size: 24px;
`
vanilla-extractとPandaに押されつつも、styled-components構文をそのまま使える点で移行プロジェクト向けに需要があります。
Stitches — 2023年メンテ停止
// 歴史資料として
import { styled } from '@stitches/react'
const Button = styled('button', {
bg: '$primary',
variants: { size: { sm: { p: '$1' }, md: { p: '$2' } } },
})
Modulz/Radix UIチームが開発していましたが、2023年に開発停止。Panda CSSが思想的後継です。
JSS — Material UI v4までの土台
import { createUseStyles } from 'react-jss'
const useStyles = createUseStyles({
button: { background: '#3b82f6', color: 'white' },
})
function Button() {
const c = useStyles()
return <button className={c.button}>Click</button>
}
パフォーマンス比較 — bundle size と runtime cost
bundle size 比較表(2026年5月実測)
| ライブラリ | gzipped (lib本体) | runtime cost | HMR速度 |
|---|---|---|---|
| styled-components v6 | ~12 KB | 高(動的タグ生成) | 遅 |
| Emotion | ~7 KB | 中 | 中 |
| vanilla-extract | 0 KB | 0 | 速 |
| Panda CSS | 0 KB | 0 | 速 |
| CSS Modules | 0 KB | 0 | 最速 |
計測スクリプト例(Webpack Bundle Analyzer)
npm install -D @next/bundle-analyzer
ANALYZE=true npm run build
// next.config.ts
import bundleAnalyzer from '@next/bundle-analyzer'
const withBundleAnalyzer = bundleAnalyzer({ enabled: process.env.ANALYZE === 'true' })
export default withBundleAnalyzer({})
Critical CSS抽出戦略
// vanilla-extract / Panda は標準で 1コンポーネント = 1クラス なので
// Next.jsの自動code-splittingがそのままcritical CSS抽出として機能
// — つまり追加実装不要
// styled-components/Emotionは ServerStyleSheet/cacheでHTML inline → CLS問題は減るがhydration重い
Runtime vs Zero-runtime の判断軸
- Runtime(styled-components/Emotion): 動的なpropベースのスタイル変化(ドラッグの座標等)が頻発する → やや有利
- Zero-runtime(vanilla-extract/Panda/CSS Modules): 静的UI主体・SSR要件・LCP重視 → 圧倒的に有利
エコシステムとの関係 — shadcn/ui / MUI / Chakra / Mantine
shadcn/ui — CSS-in-JSを使わない潮流
npx shadcn@latest add button
# → Tailwind + class-variance-authority + tailwind-merge で実装
# CSS-in-JS依存ゼロ
shadcn/uiの台頭で、Tailwind + cva(class-variance-authority)パターンがCSS-in-JSの強力なライバルになりました。
Material UI v6 — emotion or styled-components選択制
// MUI v6 — pigment-css 移行を進行中(zero-runtime化)
import { styled } from '@mui/material/styles'
const StyledButton = styled('button')(({ theme }) => ({
background: theme.palette.primary.main,
color: theme.palette.primary.contrastText,
}))
Chakra UI v3 — Pandaへの統合
// Chakra v3 — 内部実装が Emotion → Panda CSS に移行
import { Button } from '@chakra-ui/react'
export const App = () => <Button colorPalette="blue">Click</Button>
Chakra UI v3は2024年に発表され、内部実装がEmotionから自社製のPanda CSSベースに刷新されました。これは「runtime CSS-in-JSの時代の終わり」を象徴する出来事です。
Mantine v7 — CSS Modulesへ完全移行
// Mantine v6まで: Emotion使用
// Mantine v7: CSS Modules + CSS Layers に完全移行
import { Button } from '@mantine/core'
export const App = () => <Button variant="filled">Click</Button>
Radix UI Themes
// CSS Variablesベース、CSS-in-JS依存なし
import { Theme, Button } from '@radix-ui/themes'
import '@radix-ui/themes/styles.css'
export const App = () => (
<Theme accentColor="indigo">
<Button>Click</Button>
</Theme>
)
2026年の選び方 — フローチャート
判断軸1:Next.js App Routerを使うか
- YES + 新規: Panda CSS or vanilla-extract or CSS Modules
- YES + 既存(styled-components): registry経由で延命 → 段階的にvanilla-extractへ
- NO(SPA/Vite): 何でも可。CSS Modulesが最軽量
判断軸2:TypeScript厳密度
- 最高: vanilla-extract(.css.tsで完全型推論)
- 高: Panda CSS(token型・recipe型自動生成)
- 中: Emotion(Object Style)/CSS Modules + d.ts
- 低: styled-components(タグ付きテンプレートリテラルは型推論弱い)
判断軸3:Tailwindとの使い分け
| 状況 | 推奨 |
|---|---|
| プロトタイプ・社内ツール・LP | Tailwind |
| 長期保守・厳密なdesign tokens | Panda or vanilla-extract |
| 既存大規模App | CSS Modules |
| 動的でリッチなインタラクション | Emotion |
本番採用前のチェックリスト
- [ ] Next.js App Router or Pages Routerどちらか
- [ ] RSCを使うか(YES → runtime除外)
- [ ] SSR/SSG必須か
- [ ] design tokensが既にあるか
- [ ] チームのTypeScript習熟度
- [ ] エディタ補完(VS Code拡張対応)
- [ ] CIでのlint/format対応
- [ ] microfrontend or monorepo構成か
- [ ] Tailwindと共存させたいか
移行ガイド — styled-components → vanilla-extract
移行ステップ
npm install @vanilla-extract/css @vanilla-extract/recipes
npm install -D @vanilla-extract/next-plugin
Before(styled-components)
'use client'
import styled from 'styled-components'
export const Button = styled.button<{ variant: 'primary' | 'danger' }>`
padding: 8px 16px;
background: ${({ variant }) => (variant === 'danger' ? '#ef4444' : '#3b82f6')};
color: white;
border-radius: 8px;
`
After(vanilla-extract recipes)
// button.css.ts
import { recipe } from '@vanilla-extract/recipes'
export const button = recipe({
base: { padding: '8px 16px', color: 'white', borderRadius: 8, border: 'none' },
variants: {
variant: {
primary: { background: '#3b82f6' },
danger: { background: '#ef4444' },
},
},
defaultVariants: { variant: 'primary' },
})
// Button.tsx — 'use client' が消える
import { button } from './button.css'
export function Button({
variant,
...props
}: { variant?: 'primary' | 'danger' } & React.ComponentProps<'button'>) {
return <button className={button({ variant })} {...props} />
}
codemod を自作する
// codemods/sc-to-ve.ts(jscodeshift)
import { Transform } from 'jscodeshift'
const transform: Transform = (file, api) => {
const j = api.jscodeshift
const root = j(file.source)
// styled.button`...` を解析し → recipe()に置換
// (省略:実プロジェクトでは AST 走査で template literal → object に変換)
return root.toSource()
}
export default transform
学習ロードマップと案件獲得
3ステップ学習プラン
- Week 1: CSS Modulesで小さなSPAを1本作る(基礎固め)
- Week 2: vanilla-extractでTheme + Recipesを実装
- Week 3: Panda CSSでデザインシステム最小構成を作る
ポートフォリオに載せるべきもの
- 3ライブラリで同じUIを実装した比較リポジトリ
- bundle size計測結果のスクリーンショット
- RSC対応の証拠(Network tabで _next/static/css/*.css を確認)
案件で問われる定番質問
- 「なぜvanilla-extractを選んだ?」 → RSC対応・zero-runtime・型安全を答える
- 「styled-componentsからの移行コストは?」 → コンポーネント数×30分を目安に
- 「Tailwindと併用できる?」 → cn() + tailwind-merge併用例を提示できると◎
独学が辛いと感じたら
CSS-in-JSは 「動かす」までは独学で行けても、設計・運用・パフォーマンス改善は実務での試行錯誤が必要 な領域です。スクールでメンターに直接質問できる環境を作ると、学習効率は数倍変わります。
- テックアカデミー — Reactコースで実プロジェクトを伴走指導。週2回のメンタリングで設計判断まで相談できる
- 侍エンジニア — マンツーマンでCSS設計含めたフロントエンド全般を学べる。オーダーメイドカリキュラム対応
- DMM WEBCAMP — 転職保証付き。React + TypeScript案件への転職を目指すなら最有力
- レバテックフリーランス — 学習後の案件獲得に。月単価70万円〜のCSS-in-JS案件も多数
まとめ — 2026年のCSS-in-JS地図
結論を1行で:新規プロジェクト + Next.js App Router = vanilla-extract or Panda CSS、シンプルさ重視 = CSS Modules、既存保守 = styled-components / Emotion を継続、です。Tailwindが向く局面と本記事のCSS-in-JS群が向く局面は明確に分かれます——design tokens・型安全・テーマ切替・recipe(variant)が必要なら本記事の選択肢へ。
本記事の各サンプルはコピペで動作確認できます。まずはCSS Modulesから始め、Themeが必要になった時点でvanilla-extractへ、デザインシステム化のフェーズでPandaへ——という段階的なステップアップが2026年の現実的な戦略です。
関連記事: Next.js 15完全実践ガイド / Vite 6完全実践ガイド / frameworks一覧

コメント