• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • React 通关秘籍

    • 1.关于本小册
    • 2.一网打尽组件常用Hook
    • 3.Hook的闭包陷阱的成因和解决方案
    • 4.React组件如何写TypeScript类型
    • 5.React组件如何调试
    • 6.受控模式VS非受控模式
    • 7.组件实战:迷你Calendar
    • 8.组件实战:Calendar日历组件(上)
    • 9.组件实战:Calendar日历组件(下)
    • 10.快速掌握Storybook
    • 11.React组件如何写单测
    • 12.深入理解Suspense和ErrorBoundary
    • 13.组件实战:Icon图标组件
    • 14.组件实战:Space间距组件
    • 15.React.Children和它的两种替代方案
    • 16.三个简单组件的封装
    • 17.浏览器的5种Observer
    • 18.组件实战:Watermark防删除水印组件
    • 19.手写react-lazyload
    • 20.图解网页的各种距离
    • 21.自定义hook练习
    • 22.自定义hook练习(二)
    • 23.用react-spring做弹簧动画
    • 24.react-spring结合use-gesture手势库实现交互动画
    • 25.用react-transition-group和react-spring做过渡动画
    • 26.快速掌握Tailwind:最流行的原子化CSS框架
    • 27.用CSSModules避免样式冲突
    • 28.CSSInJS:快速掌握styled-components
    • 29.react-spring实现滑入滑出的转场动画
    • 30.组件实战:Message全局提示组件
    • 31.组件实战:Popover气泡卡片组件
    • 32.项目里如何快速定位组件源码
    • 33.一次超爽的React调试体验
    • 34.组件实战:ColorPicker颜色选择器(一)
    • 35.组件实战:ColorPicker颜色选择器(二)
    • 36.组件实战:onBoarding漫游式引导组件
    • 37.组件实战:Upload拖拽上传
    • 38.组件实战:Form表单组件
    • 39.React组件库都是怎么构建的
    • 40.组件库实战:构建esm和cjs产物,发布到npm
    • 41.组件库实战:构建umd产物,通过unpkg访问
    • 42.数据不可变:immutable和immer
    • 43.基于ReactRouter实现keepalive
    • 44.Historyapi和ReactRouter实现原理
    • 45.ReactContext的实现原理和在antd里的应用
    • 46.ReactContext的性能缺点和解决方案
    • 47.手写一个Zustand
    • 48.原子化状态管理库Jotai
    • 49.用react-intl实现国际化
    • 50.国际化资源包如何通过Excel和GoogleSheet分享给产品经理
    • 51.基于react-dnd实现拖拽排序
    • 52.react-dnd实战:拖拽版TodoList
    • 53.ReactPlayground项目实战:需求分析、实现原理
    • 54.ReactPlayground项目实战:布局、代码编辑器
    • 55.ReactPlayground项目实战:多文件切换
    • 56.ReactPlayground项目实战:babel编译、iframe预览
    • 57.ReactPlayground项目实战:文件增删改
    • 58.ReactPlayground项目实战:错误显示、主题切换
    • 59.ReactPlayground项目实战:链接分享、代码下载
    • 60.ReactPlayground项目实战:WebWorker性能优化
    • 61.ReactPlayground项目实战:总结
    • 62.手写MiniReact:思路分析
    • 63.手写MiniReact:代码实现
    • 64.手写MiniReact:和真实React源码的对比
    • 65.React18的并发机制是怎么实现的
    • 66.Ref的实现原理
    • 67.低代码编辑器:核心数据结构、全局store
    • 68.低代码编辑器:拖拽组件到画布、拖拽编辑json
    • 69.低代码编辑器:画布区hover展示高亮框
    • 70.低代码编辑器:画布区click展示编辑框
    • 71.低代码编辑器:组件属性、样式编辑
    • 72.低代码编辑器:预览、大纲
    • 73.低代码编辑器:事件绑定
    • 74.低代码编辑器:动作弹窗
    • 75.低代码编辑器:自定义JS
    • 76.低代码编辑器:组件联动
    • 77.低代码编辑器:拖拽优化、Table组件
    • 78.低代码编辑器:Form组件、store持久化
    • 79.低代码编辑器:项目总结
    • 80.快速掌握ReactFlow画流程图
    • 81.ReactFlow振荡器调音:项目介绍
    • 82.ReactFlow振荡器调音:流程图绘制
    • 83.ReactFlow振荡器调音:合成声音
    • 84.AudioContext实现在线钢琴
    • 85.React服务端渲染:从SSR到hydrate
    • 86.小册总结

CSS in JS,顾名思义就是用 js 来写 css。

它也是一种很流行的 css 管理方案。

比如 styled-components 的样式是这样写:

可以传参数。

然后用的时候当作组件一样用:

样式用 js 写,可以当成组件用,可以传参,这是 CSS in JS 的方案独有的体验。

接下来我们也体验一下:

npx create-vite styled-components-test

用 vite 创建个项目。

安装 styled-components:

npm install

npm install --save styled-components

去掉 index.css 和 StrictMode:

然后改下 App.tsx:

import { styled } from "styled-components";

const Title = styled.h1`
    font-size: 30px;
    text-align: center;
    color: blue;
`;

const Header = styled.div`
    padding: 20px;
    background: pink;
`;

function App() {
    return (
        <Header>
            <Title>Hello World!</Title>
        </Header>
    );
}

export default App;

跑起来看下:

npm run dev

样式生效了:

打开 devtools 看下:

可以看到 styled.div、styled.h1 会创建对应的标签,然后样式会生成一个唯一的 className。

所以说,用 styled-components 不用担心样式冲突的问题。

继续看,styled-components 的 styled.xx 可以作为组件用,那自然是可以传参的:

import { styled } from "styled-components";

const Title = styled.h1<{ color?: string }>`
    font-size: 30px;
    text-align: center;
    color: ${(props) => props.color || "blue"};
`;

const Header = styled.div`
    padding: 20px;
    background: pink;
`;

function App() {
    return (
        <Header>
            <Title>Hello World!</Title>
            <Title color="green">Hello World!</Title>
            <Title color="black">Hello World!</Title>
        </Header>
    );
}

export default App;

我们给 Title 样式组件添加一个 color 参数,然后分别传入 green、black。

看下效果:

确实样式组件用起来和其他 React 组件体验一样,加的 ts 类型也会有提示:

这也是为啥这个库叫 styled-components,样式组件。

有的时候,样式需要基于已有的做扩展,比如我有一个 Button 的样式,另一种 Button 和它大部分一样,但有所不同。

这时候就可以这样写:

import { styled } from "styled-components";

const Button = styled.button<{ color?: string }>`
    font-size: 20px;
    margin: 5px 10px;
    border: 2px solid #000;
    color: ${(props) => props.color || "blue"};
`;

const Button2 = styled(Button)`
    border-radius: 8px;
`;
function App() {
    return (
        <div>
            <Button color="red">Hello World!</Button>
            <Button2 color="red">Hello World!</Button2>
        </div>
    );
}

export default App;

如果你还想改样式组件的标签,可以用 as:

styled() 除了可以给样式组件扩展样式外,还可以给普通组件加上样式:

import { FC, PropsWithChildren } from "react";
import { styled } from "styled-components";

interface LinkProps extends PropsWithChildren {
    href: string;
    className?: string;
}

const Link: FC<LinkProps> = (props) => {
    const { href, className, children } = props;

    return (
        <a href={href} className={className}>
            {children}
        </a>
    );
};

const StyledLink = styled(Link)`
    color: green;
    font-size: 40px;
`;

function App() {
    return (
        <div>
            <StyledLink href="#aaa">click me</StyledLink>
        </div>
    );
}

export default App;

比如我们给 Link 组件加上样式。

这里要注意,Link 组件必须接收 className 参数,因为 styled-components 会把样式放到这个 className 上:

我们知道,样式组件也是可以接受参数的,为了区分两者,我们一般都是样式组件的 props 用 $ 开头:

const StyledLink = styled(Link)<{ $color?: string }>`
    color: ${(props) => props.$color || "green"};
    font-size: 40px;
`;

function App() {
    return (
        <div>
            <StyledLink href="#aaa" $color="purple">
                click me
            </StyledLink>
        </div>
    );
}

默认情况下,样式组件会透传所有不是它的 props 给被包装组件:

样式组件包了一层,自然是可以修改 props 的:

用 attrs 方法,接收传入的 props 返回修改后的 props。

import { FC, PropsWithChildren } from "react";
import { styled } from "styled-components";

interface LinkProps extends PropsWithChildren {
    href: string;
    className?: string;
}

const Link: FC<LinkProps> = (props) => {
    console.log(props);

    const { href, className, children } = props;

    return (
        <a href={href} className={className}>
            {children}
        </a>
    );
};

const StyledLink = styled(Link).attrs<{ $color?: string }>((props) => {
    console.log(props);

    props.$color = "orange";
    props.children = props.children + " 光";
    return props;
})`
    color: ${(props) => props.$color || "green"};
    font-size: 40px;
`;

function App() {
    return (
        <div>
            <StyledLink href="#aaa" $color="purple">
                click me
            </StyledLink>
        </div>
    );
}

export default App;

attrs 支持对象和函数,简单的场景直接传对象也可以:

const Input = styled.input.attrs({ type: "checkbox" })`
    width: 30px;
    height: 30px;
`;

那伪类选择器、伪元素选择器这些呢?

当然也是支持的。

import { styled } from "styled-components";

const ColoredText = styled.div`
    color: blue;

    &:hover {
        color: red;
    }

    &::before {
        content: "* ";
    }
`;

function App() {
    return (
        <>
            <ColoredText>Hello styled components</ColoredText>
        </>
    );
}

export default App;

写法和之前一样。

但 styled components 这个 & 和 scss 里的 & 含义还不大一样。

它指的是同一个样式组件的实例,这里也就是 ColoredText 的实例。

所以可以这样写:

import { styled } from "styled-components";

const ColoredText = styled.div`
    color: blue;

    &:hover {
        color: red;
    }

    &::before {
        content: "* ";
    }

    &.aaa + & {
        background: lightblue;
    }

    &.bbb ~ & {
        background: pink;
    }
`;

function App() {
    return (
        <>
            <ColoredText>Hello styled components</ColoredText>
            <ColoredText className="aaa">Hello styled components</ColoredText>
            <ColoredText>Hello styled components</ColoredText>
            <ColoredText className="bbb">Hello styled components</ColoredText>
            <div>Hello styled components</div>
            <ColoredText>Hello styled components</ColoredText>
            <ColoredText>Hello styled components</ColoredText>
        </>
    );
}

export default App;

这里 &.aaa + & 就是 .aaa 的 ColoredText 样式组件之后的一个 ColoredText 样式组件实例。

&.bbb ~ & 就是 .bbb 的 ColoredText 样式组件之后的所有 ColoredText 样式组件实例。

此外,如果你把 & 全换成 &&,你会发现效果也一样:

那什么时候用 &、什么时候用 && 呢?

当你和全局样式冲突的时候。

styled-components 用 createGlobalStyle 创建全局样式:

我们全局指定 ColoredText 的 color 为 green,然后组件里指定 color 为 blue。

看下效果:

每个 ColorText 组件都会有一个 src-aYaIB 的 className,全局样式就是给这个 className 加了 color 为 green 的样式。

可以看到,组件里写的 color: blue 被覆盖了。

这时候你这样写是没用的:

用 && 才能覆盖:

它通过 .aaa.aaa 这样的方式实现了样式优先级的提升:

那动画怎么写呢?

有单独的 api:

import { styled, keyframes } from "styled-components";

const rotate = keyframes`
  from {
    transform: rotate(0deg);
  }

  to {
    transform: rotate(360deg);
  }
`;

const Rotate = styled.div`
    display: inline-block;
    animation: ${rotate} 2s linear infinite;
    font-size: 50px;
    padding: 30px;
`;

function App() {
    return <Rotate>X</Rotate>;
}

export default App;

通过 keyframes 来编写动画,然后在 animation 里引用。

看下效果:

它为 @keyframes 生成了一个唯一 ID:

这大概就是加一个 keyframes 的 api 的意义。

此外,如果你想复用部分 css,要这样写:

const animation = css`
    animation: ${rotate} 2s linear infinite;
`;

const Rotate = styled.div`
    display: inline-block;
    ${animation}
    font-size: 50px;
    padding: 30px;
`;

不加 css 是不会生效的,你可以试一下。

抽出来的 css 也是可以用 props 的:

import { styled, keyframes, css } from "styled-components";

const rotate = keyframes`
  from {
    transform: rotate(0deg);
  }

  to {
    transform: rotate(360deg);
  }
`;

const animation = css<{ $duration: number }>`
    animation: ${rotate} ${(props) => props.$duration}s linear infinite;
`;

const Rotate = styled.div<{ $duration: number }>`
    display: inline-block;
    ${animation}
    font-size: 50px;
    padding: 30px;
`;

function App() {
    return <Rotate $duration={3}>X</Rotate>;
}

export default App;

但是 css 声明了类型,用到了这部分样式的 styled.xxx 也需要声明类型。

如果你希望样式组件用的时候可以传入一些样式,那可以用 RuleSet:

import { styled, keyframes, css, RuleSet } from "styled-components";

const rotate = keyframes`
  from {
    transform: rotate(0deg);
  }

  to {
    transform: rotate(360deg);
  }
`;

const animation = css<{ $duration: number }>`
    animation: ${rotate} ${(props) => props.$duration}s linear infinite;
`;

const Rotate = styled.div<{ $duration: number; otherStyles: RuleSet }>`
    display: inline-block;
    ${animation}
    font-size: 50px;
    padding: 30px;
    ${(props) => props.otherStyles}
`;

function App() {
    return (
        <Rotate
            $duration={3}
            otherStyles={[
                { border: "1px", background: "pink" },
                { boxShadow: "0 0 3px  blue" },
            ]}>
            X
        </Rotate>
    );
}

export default App;

它是一个样式对象的数组类型:

可以用的时候传入一些样式:

最后,styled-components 还有 theme 的 api。

这个也很简单,你会用 react 的 context 就会用这个:

import { styled, ThemeProvider } from "styled-components";

const Aaa = styled.div`
    width: 100px;
    height: 100px;
    background: ${(props) => (props.theme.dark ? "black" : "#ccc")};
`;
function Content() {
    return <Aaa></Aaa>;
}

function App() {
    return (
        <ThemeProvider theme={{ dark: true }}>
            <Content></Content>
        </ThemeProvider>
    );
}

export default App;

每个样式组件都有 props.theme 可以读取当前 theme 对象,然后这个对象可以通过 useTheme 读取,通过 ThemeProvider 修改。

import { useState } from "react";
import { styled, ThemeProvider, useTheme } from "styled-components";

const Aaa = styled.div`
    width: 100px;
    height: 100px;
    background: ${(props) => (props.theme.dark ? "black" : "#ccc")};
`;
function Content() {
    const theme = useTheme();
    const [dark, setDark] = useState < boolean > theme.dark;

    return (
        <>
            <button onClick={() => setDark(!dark)}>切换</button>
            <ThemeProvider theme={{ dark }}>
                <Aaa></Aaa>
            </ThemeProvider>
        </>
    );
}

function App() {
    return (
        <ThemeProvider theme={{ dark: true }}>
            <Content></Content>
        </ThemeProvider>
    );
}

export default App;

我们用 useTheme 读取了当前 theme,然后点击按钮的时候 setState 触发重新渲染,通过 ThemeProvider 修改了 theme 的值。

这就是 styled-components 的 theme 功能。

上面的过一遍,styled-components 就算掌握的差不多了

那最后我们来思考下,用 styled-components 有啥优缺点呢?

先来看下好处:

用了 styled-components 之后,你的 className 都是这样的:

没有样式冲突问题,不需要类似 CSS Modules 这种方案。

而且你可以用 js 来写样式逻辑,而且封装方式也是 React 组件的方式,这个是挺爽的。

不然你要学 scss 的函数的语法,比如这样:

@function multiple-box-shadow($n) {
    $value: "#{random(2000)}px #{random(2000)}px #FFF";
    @for $i from 2 through $n {
        $value: "#{$value} , #{random(2000)}px #{random(2000)}px #FFF";
    }
    @return unquote($value);
}

#stars {
    width: 1px;
    height: 1px;
    box-shadow: multiple-box-shadow(700);
}

scss 的 for 循环、if else 还有函数等的语法都要单独学习。

相比之下,还是 styled-components 直接用 js 来写样式组件的逻辑更爽。

这就像很多人不喜欢 vue 的 template 写法,更喜欢 React 的 jsx 一样,可以直接用 js 来写逻辑。

当然,styled-components 也有不好的地方,比如:

你的 React 项目里会多出特别多样式组件:

随便找一个组件,一眼望去全是样式组件。

你的 React DevTools 里也是一堆 styled-components 的组件:

当然,这些也不是啥大问题,styled-components 整体还是很好用的。

案例代码上传了小册仓库

总结

CSS in JS 就是用 js 来写 css。

今天我们学习了最流行的 CSS in JS 库 styled-components。

它的特点就是样式组件,用 styled.div、styled() 可以创建样式组件。

样式组件可以传参数,可以通过 attrs() 修改参数。

通过 keyframes 来声明动画样式,通过 css 来复用某段样式,通过 createGlobalStyle 创建全局样式。

写样式的时候,通过 & 代表当前样式组件的实例,当样式和全局样式冲突的时候,还可以 && 提高优先级。

styled-components 还支持 theme,可以通过 ThemeProvider 修改 theme 值,通过 useTheme 来读取,每个样式组件里都可以通过 props.theme 拿到当前 theme,然后展示不同样式。

styled-components 相比 scss 等方案有好有坏:

  • 没有 className 冲突问题,不需要 CSS Modules
  • 用 js 来写逻辑,不需要学习单独的 scss 语法
  • 项目里会多很多的样式组件,和普通组件混在一起
  • React DevTools 里会有很多层的样式组件

总体来说,styled-components 还是很有不错,如果你喜欢通过 React 组件的方式来写样式这种方式,可以考虑使用。

我最近在维护的一个项目,用 styled-components 好多年了,大项目用也没问题。

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
27.用CSSModules避免样式冲突
Next
29.react-spring实现滑入滑出的转场动画