CSS-in-JSライブラリ完全比較〜styled-components/Emotion/vanilla-extract/Panda CSS/CSS Modules〜【2026年版】

「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では表現しきれないコンポーネントスタイル設計」を扱います。

  1. CSS-in-JSの全体像 — 2026年の地図
    1. 4世代分類:runtime → zero-runtime → atomic → server-first
    2. RSC(React Server Components)が変えたもの
    3. 本記事で扱う5つのライブラリの位置づけ
  2. styled-components 完全実装 — v6時代の正しい使い方
    1. インストールと基本セットアップ
    2. 基本コード:タグ付きテンプレートリテラル
    3. Propsで動的スタイル
    4. ThemeProvider と Theme型定義
    5. Next.js App Router 向け SSR セットアップ
    6. styled-componentsの「2026年における立ち位置」
  3. Emotion 完全実装 — Material UI/Chakra UIの土台
    1. インストール:2つのAPIから選ぶ
    2. styled API:styled-componentsとほぼ同じ
    3. css prop:Emotion最大の武器
    4. Object Style記法 — TypeScript親和性が高い
    5. Theme:useThemeで型安全に
    6. Emotion + TypeScript:厳密型でprops設計
    7. Next.js App Router 向け SSR セットアップ
  4. vanilla-extract — TypeScript-firstのzero-runtime王
    1. インストールとVite/Next.js統合
    2. 基本:style() で 1スタイル1クラス
    3. Theme(globalTheme + createTheme)
    4. recipes — variant付きコンポーネント
    5. Sprinkles — atomic CSS生成
    6. vanilla-extractの強み
  5. Panda CSS — atomic + recipeのハイブリッド
    1. インストールと初期化
    2. 基本:css 関数 + atomic CSS
    3. Recipe(cva風)
    4. Theming with Tokens + Semantic Tokens
    5. JSX Style Props(任意機能)
  6. CSS Modules — 古くて新しい王道
    1. 基本:1ファイル1スコープ
    2. clsx / classnames で見やすく
    3. TypeScript型安全(typed-css-modules / Vite built-in)
    4. Vite での CSS Modules
    5. composes — 再利用
  7. 過去の主役たち — Linaria / Stitches / JSS
    1. Linaria — Wix発のzero-runtime
    2. Stitches — 2023年メンテ停止
    3. JSS — Material UI v4までの土台
  8. パフォーマンス比較 — bundle size と runtime cost
    1. bundle size 比較表(2026年5月実測)
    2. 計測スクリプト例(Webpack Bundle Analyzer)
    3. Critical CSS抽出戦略
    4. Runtime vs Zero-runtime の判断軸
  9. エコシステムとの関係 — shadcn/ui / MUI / Chakra / Mantine
    1. shadcn/ui — CSS-in-JSを使わない潮流
    2. Material UI v6 — emotion or styled-components選択制
    3. Chakra UI v3 — Pandaへの統合
    4. Mantine v7 — CSS Modulesへ完全移行
    5. Radix UI Themes
  10. 2026年の選び方 — フローチャート
    1. 判断軸1:Next.js App Routerを使うか
    2. 判断軸2:TypeScript厳密度
    3. 判断軸3:Tailwindとの使い分け
    4. 本番採用前のチェックリスト
  11. 移行ガイド — styled-components → vanilla-extract
    1. 移行ステップ
    2. Before(styled-components)
    3. After(vanilla-extract recipes)
    4. codemod を自作する
  12. 学習ロードマップと案件獲得
    1. 3ステップ学習プラン
    2. ポートフォリオに載せるべきもの
    3. 案件で問われる定番質問
    4. 独学が辛いと感じたら
  13. まとめ — 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ステップ学習プラン

  1. Week 1: CSS Modulesで小さなSPAを1本作る(基礎固め)
  2. Week 2: vanilla-extractでTheme + Recipesを実装
  3. 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一覧

コメント

タイトルとURLをコピーしました