Next.js CSS-in-JS #

一、CSS-in-JS概述 #

1.1 什么是CSS-in-JS #

CSS-in-JS是一种在JavaScript中编写CSS的技术:

tsx
const Button = styled.button`
    padding: 0.5rem 1rem;
    background-color: blue;
    color: white;
    border-radius: 0.25rem;
`

1.2 优缺点 #

优点

  • 组件级作用域
  • 动态样式
  • 类型安全
  • 主题支持

缺点

  • 运行时开销
  • 学习成本
  • SSR配置复杂

1.3 常用库 #

特点
styled-components 流行,功能完整
emotion 性能好,灵活
stitches 零运行时
vanilla-extract 零运行时,类型安全

二、styled-components #

2.1 安装 #

bash
npm install styled-components
npm install -D @types/styled-components

2.2 基本用法 #

tsx
'use client'

import styled from 'styled-components'

const Container = styled.div`
    max-width: 1200px;
    margin: 0 auto;
    padding: 1rem;
`

const Title = styled.h1`
    font-size: 2rem;
    color: #1f2937;
`

export default function Page() {
    return (
        <Container>
            <Title>页面标题</Title>
        </Container>
    )
}

2.3 Props #

tsx
interface ButtonProps {
    $primary?: boolean
}

const Button = styled.button<ButtonProps>`
    padding: 0.5rem 1rem;
    border-radius: 0.25rem;
    background-color: ${props => props.$primary ? 'blue' : 'gray'};
    color: white;
`

export default function Form() {
    return (
        <div>
            <Button $primary>主要按钮</Button>
            <Button>次要按钮</Button>
        </div>
    )
}

2.4 SSR配置 #

next.config.ts

typescript
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
    compiler: {
        styledComponents: true,
    },
}

export default nextConfig

Registry组件

tsx
'use client'

import { useState } from 'react'
import { useServerInsertedHTML } from 'next/navigation'
import { ServerStyleSheet, StyleSheetManager } from 'styled-components'

export default function StyledComponentsRegistry({
    children,
}: {
    children: React.ReactNode
}) {
    const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet())
    
    useServerInsertedHTML(() => {
        const styles = styledComponentsStyleSheet.getStyleElement()
        styledComponentsStyleSheet.instance.clearTag()
        return <>{styles}</>
    })
    
    if (typeof window !== 'undefined') {
        return <>{children}</>
    }
    
    return (
        <StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
            {children}
        </StyleSheetManager>
    )
}

根布局使用

tsx
import StyledComponentsRegistry from './registry'

export default function RootLayout({
    children,
}: {
    children: React.ReactNode
}) {
    return (
        <html lang="zh-CN">
            <body>
                <StyledComponentsRegistry>
                    {children}
                </StyledComponentsRegistry>
            </body>
        </html>
    )
}

三、Emotion #

3.1 安装 #

bash
npm install @emotion/react @emotion/styled

3.2 基本用法 #

tsx
'use client'

import styled from '@emotion/styled'

const Container = styled.div`
    max-width: 1200px;
    margin: 0 auto;
    padding: 1rem;
`

export default function Page() {
    return <Container>内容</Container>
}

3.3 css prop #

tsx
'use client'

import { css } from '@emotion/react'

const containerStyle = css`
    max-width: 1200px;
    margin: 0 auto;
    padding: 1rem;
`

export default function Page() {
    return <div css={containerStyle}>内容</div>
}

3.4 主题 #

tsx
'use client'

import { ThemeProvider } from '@emotion/react'
import styled from '@emotion/styled'

const theme = {
    colors: {
        primary: '#3b82f6',
        secondary: '#64748b',
    },
    spacing: {
        sm: '0.5rem',
        md: '1rem',
        lg: '2rem',
    },
}

const Button = styled.button`
    padding: ${props => props.theme.spacing.md};
    background-color: ${props => props.theme.colors.primary};
    color: white;
`

export default function App({ children }: { children: React.ReactNode }) {
    return (
        <ThemeProvider theme={theme}>
            <Button>主题按钮</Button>
        </ThemeProvider>
    )
}

四、Stitches #

4.1 安装 #

bash
npm install @stitches/react

4.2 配置 #

tsx
import { createStitches } from '@stitches/react'

export const { styled, css, globalCss, keyframes, getCssText } = createStitches({
    theme: {
        colors: {
            primary: '#3b82f6',
            secondary: '#64748b',
            text: '#1f2937',
            background: '#ffffff',
        },
        space: {
            1: '0.25rem',
            2: '0.5rem',
            4: '1rem',
            6: '1.5rem',
            8: '2rem',
        },
        radii: {
            sm: '0.25rem',
            md: '0.5rem',
            lg: '1rem',
        },
    },
})

4.3 使用 #

tsx
'use client'

import { styled } from '@/stitches.config'

const Button = styled('button', {
    padding: '$4',
    backgroundColor: '$primary',
    color: 'white',
    borderRadius: '$md',
    border: 'none',
    cursor: 'pointer',
    
    variants: {
        variant: {
            primary: {
                backgroundColor: '$primary',
            },
            secondary: {
                backgroundColor: '$secondary',
            },
        },
        size: {
            sm: {
                padding: '$2 $4',
                fontSize: '0.875rem',
            },
            md: {
                padding: '$4 $6',
            },
            lg: {
                padding: '$6 $8',
                fontSize: '1.125rem',
            },
        },
    },
    
    defaultVariants: {
        variant: 'primary',
        size: 'md',
    },
})

export default function Form() {
    return (
        <div>
            <Button variant="primary" size="lg">主要按钮</Button>
            <Button variant="secondary" size="sm">次要按钮</Button>
        </div>
    )
}

4.4 SSR配置 #

tsx
import { getCssText } from '@/stitches.config'

export default function RootLayout({
    children,
}: {
    children: React.ReactNode
}) {
    return (
        <html lang="zh-CN">
            <head>
                <style id="stitches" dangerouslySetInnerHTML={{ __html: getCssText() }} />
            </head>
            <body>{children}</body>
        </html>
    )
}

五、Vanilla Extract #

5.1 安装 #

bash
npm install @vanilla-extract/css @vanilla-extract/next-plugin

5.2 配置 #

typescript
import type { NextConfig } from 'next'
import { createVanillaExtractPlugin } from '@vanilla-extract/next-plugin'

const withVanillaExtract = createVanillaExtractPlugin()

const nextConfig: NextConfig = {}

export default withVanillaExtract(nextConfig)

5.3 定义样式 #

typescript
import { style, createTheme } from '@vanilla-extract/css'

export const [themeClass, vars] = createTheme({
    color: {
        primary: '#3b82f6',
        secondary: '#64748b',
    },
    space: {
        small: '0.5rem',
        medium: '1rem',
        large: '2rem',
    },
})

export const container = style({
    maxWidth: '1200px',
    margin: '0 auto',
    padding: vars.space.medium,
})

export const button = style({
    padding: `${vars.space.small} ${vars.space.medium}`,
    backgroundColor: vars.color.primary,
    color: 'white',
    borderRadius: '0.25rem',
    border: 'none',
    cursor: 'pointer',
})

5.4 使用 #

tsx
import { container, button, themeClass } from './styles.css'

export default function Page() {
    return (
        <div className={themeClass}>
            <div className={container}>
                <button className={button}>按钮</button>
            </div>
        </div>
    )
}

六、最佳实践 #

6.1 选择建议 #

场景 推荐
简单项目 Tailwind CSS
需要动态样式 styled-components
性能敏感 vanilla-extract
团队熟悉 选择熟悉的库

6.2 服务端组件 #

CSS-in-JS需要在客户端组件中使用:

tsx
'use client'

import styled from 'styled-components'

const Container = styled.div`
    padding: 1rem;
`

export default function ClientComponent() {
    return <Container>内容</Container>
}

6.3 组合使用 #

tsx
import { Container } from './ClientComponent'

export default async function Page() {
    const data = await getData()
    
    return (
        <Container>
            <h1>{data.title}</h1>
        </Container>
    )
}

七、总结 #

CSS-in-JS要点:

特点 适用场景
styled-components 流行,功能完整 动态样式
emotion 性能好,灵活 复杂项目
stitches 零运行时 性能敏感
vanilla-extract 类型安全 大型项目

下一步,让我们学习Sass与全局样式!

最后更新:2026-03-28