基础

动机

styled-components 是作者对于如何增强 React 组件中 CSS 表现这个问题的思考结果 通过聚焦于单个用例,设法优化了开发者的体验和面向终端用户的输出.

除了提升开发者体验外, styled-components 同时提供以下特性:

  • Automatic critical CSS: styled-components 持续跟踪页面上渲染的组件,并向自动其注入且仅注入样式. 结合使用代码拆分, 可以实现仅加载所需的最少代码.
  • 解决了 class name 冲突: styled-components 为样式生成唯一的 class name. 开发者不必再担心 class name 重复,覆盖和拼写错误的问题.
  • CSS 更容易移除: 想要确切的知道代码中某个 class 在哪儿用到是很困难的. 使用 styled-components 则很轻松, 因为每个样式都有其关联的组件. 如果检测到某个组件未使用并且被删除,则其所有的样式也都被删除.
  • 简单的动态样式: 可以很简单直观的实现根据组件的 props 或者全局主题适配样式,无需手动管理数十个 classes.
  • 无痛维护: 无需搜索不同的文件来查找影响组件的样式.无论代码多庞大,维护起来都是小菜一碟。
  • 自动提供前缀: 按照当前标准写 CSS,其余的交给 styled-components handle 处理.

通过 styled-components 绑定样式到组件,开发者可以在编写熟知的 CSS 同时也获得上述全部的益处.

安装

从 npm 安装 styled-components :

  1. npm install --save styled-components

强烈推荐使用 styled-components 的 babel 插件 (当然这不是必须的).它提供了许多益处,比如更清晰的类名,SSR 兼容性,更小的包等等.

如果没有使用模块管理工具或者包管理工具,也可以使用官方托管在 unpkg CDN 上的构建版本.只需在HTML文件底部添加以下<script>标签:

  1. <script src="https://unpkg.com/styled-components/dist/styled-components.min.js"></script>

添加 styled-components 之后就可以访问全局的 window.styled 变量.

  1. const Component = window.styled.div`
  2. color: red;
  3. `

注意

这用使用方式需要页面在 styled-components script 之前引入 react CDN bundles

入门

styled-components 通过标记的模板字符来设置组件样式.

它移除了组件和样式之间的映射.当我们通过styled-components定义样式时,我们实际上是创建了一个附加了样式的常规 React 组件.

以下的例子创建了两个简单的附加了样式的组件, 一个Wrapper和一个Title:

  1. // 创建一个 Title 组件,它将渲染一个附加了样式的 <h1> 标签
  2. const Title = styled.h1`
  3. font-size: 1.5em;
  4. text-align: center;
  5. color: palevioletred;
  6. `;
  7. // 创建一个 Wrapper 组件,它将渲染一个附加了样式的 <section> 标签
  8. const Wrapper = styled.section`
  9. padding: 4em;
  10. background: papayawhip;
  11. `;
  12. // 就像使用常规 React 组件一样使用 Title 和 Wrapper
  13. render(
  14. <Wrapper>
  15. <Title>
  16. Hello World!
  17. </Title>
  18. </Wrapper>
  19. );

注意

styled-components 会为我们自动创建 CSS 前缀

基于属性的适配

我们可以将 props 以插值的方式传递给styled component,以调整组件样式.

下面这个 Button 组件持有一个可以改变colorprimary属性. 将其设置为 ture 时,组件的background-colorcolor会交换.

  1. const Button = styled.button`
  2. /* Adapt the colors based on primary prop */
  3. background: ${props => props.primary ? "palevioletred" : "white"};
  4. color: ${props => props.primary ? "white" : "palevioletred"};
  5. font-size: 1em;
  6. margin: 1em;
  7. padding: 0.25em 1em;
  8. border: 2px solid palevioletred;
  9. border-radius: 3px;
  10. `;
  11. render(
  12. <div>
  13. <Button>Normal</Button>
  14. <Button primary>Primary</Button>
  15. </div>
  16. );

样式继承

可能我们希望某个经常使用的组件,在特定场景下可以稍微更改其样式.当然我们可以通过 props 传递插值的方式来实现,但是对于某个只需要重载一次的样式来说这样做的成本还是有点高.

创建一个继承其它组件样式的新组件,最简单的方式就是用构造函数styled()包裹被继承的组件.下面的示例就是通过继承上一节创建的按钮从而实现一些颜色相关样式的扩展:

  1. // 上一节创建的没有插值的 Button 组件
  2. const Button = styled.button`
  3. color: palevioletred;
  4. font-size: 1em;
  5. margin: 1em;
  6. padding: 0.25em 1em;
  7. border: 2px solid palevioletred;
  8. border-radius: 3px;
  9. `;
  10. // 一个继承 Button 的新组件, 重载了一部分样式
  11. const TomatoButton = styled(Button)`
  12. color: tomato;
  13. border-color: tomato;
  14. `;
  15. render(
  16. <div>
  17. <Button>Normal Button</Button>
  18. <TomatoButton>Tomato Button</TomatoButton>
  19. </div>
  20. );

可以看到,新的TomatoButton仍然和Button类似,我们只是添加了两条规则.

In some cases you might want to change which tag or component a styled component renders.这在构建导航栏时很常见,例如导航栏中同时存在链接和按钮,但是它们的样式应该相同.

在这种情况下,我们也有替代办法(escape hatch). 我们可以使用多态 “as” polymorphic prop 动态的在不改变样式的情况下改变元素:

  1. const Button = styled.button`
  2. display: inline-block;
  3. color: palevioletred;
  4. font-size: 1em;
  5. margin: 1em;
  6. padding: 0.25em 1em;
  7. border: 2px solid palevioletred;
  8. border-radius: 3px;
  9. `;
  10. const TomatoButton = styled(Button)`
  11. color: tomato;
  12. border-color: tomato;
  13. `;
  14. render(
  15. <div>
  16. <Button>Normal Button</Button>
  17. <Button as="a" href="/">Link with Button styles</Button>
  18. <TomatoButton as="a" href="/">Link with Tomato Button styles</TomatoButton>
  19. </div>
  20. );

这也完美适用于自定义组件:

  1. const Button = styled.button`
  2. display: inline-block;
  3. color: palevioletred;
  4. font-size: 1em;
  5. margin: 1em;
  6. padding: 0.25em 1em;
  7. border: 2px solid palevioletred;
  8. border-radius: 3px;
  9. `;
  10. const ReversedButton = props => <button {...props} children={props.children.split('').reverse()} />
  11. render(
  12. <div>
  13. <Button>Normal Button</Button>
  14. <Button as={ReversedButton}>Custom Button with Normal Button styles</Button>
  15. </div>
  16. );

给任何组件添加样式

styled方法适用于任何最终向 DOM 元素传递 className 属性的组件,当然也包括第三方组件.

注意

在 react-native 中,请使用 style 而不是 className.

  1. // 下面是给 react-router-dom Link 组件添加样式的示例
  2. const Link = ({ className, children }) => (
  3. <a className={className}>
  4. {children}
  5. </a>
  6. );
  7. const StyledLink = styled(Link)`
  8. color: palevioletred;
  9. font-weight: bold;
  10. `;
  11. render(
  12. <div>
  13. <Link>Unstyled, boring Link</Link>
  14. <br />
  15. <StyledLink>Styled, exciting Link</StyledLink>
  16. </div>
  17. );

注意

也可以传递标签给styled(), 比如:styled("div"). 实际上styled.tagname的方式就是 styled(tagname)`的别名.

属性传递

如果添加样式的目标是 DOM 元素 (如styled.div), styled-components会传递已知的 HTML 属性给 DOM. 如果是一个自定义的 React 组件 (如styled(MyComponent)), styled-components 会传递全部 props.

以下示例展示如何传递 Input 组件的 props 到已装载的 DOM 节点, as with React elements.

  1. // 创建一个给<input>标签添加若干样式的 Input 组件
  2. const Input = styled.input`
  3. padding: 0.5em;
  4. margin: 0.5em;
  5. color: ${props => props.inputColor || "palevioletred"};
  6. background: papayawhip;
  7. border: none;
  8. border-radius: 3px;
  9. `;
  10. // 渲染两个样式化的 text input,一个标准颜色,一个自定义颜色
  11. render(
  12. <div>
  13. <Input defaultValue="@probablyup" type="text" />
  14. <Input defaultValue="@geelen" type="text" inputColor="rebeccapurple" />
  15. </div>
  16. );

注意, inputColor prop并没有传递给 DOM, 但是typedefaultValue 都传递了. styled-components足够智能,会自动过滤掉所有非标准 attribute.

Coming from CSS

styled-components 如何在组件中工作?

如果你熟悉在组件中导入 CSS(例如 CSSModules),那么下面的写法你一定不陌生:

  1. import React from 'react'
  2. import styles from './styles.css'
  3. export default class Counter extends React.Component {
  4. state = { count: 0 }
  5. increment = () => this.setState({ count: this.state.count + 1 })
  6. decrement = () => this.setState({ count: this.state.count - 1 })
  7. render() {
  8. return (
  9. <div className={styles.counter}>
  10. <p className={styles.paragraph}>{this.state.count}</p>
  11. <button className={styles.button} onClick={this.increment}>
  12. +
  13. </button>
  14. <button className={styles.button} onClick={this.decrement}>
  15. -
  16. </button>
  17. </div>
  18. )
  19. }
  20. }

由于 Styled Component 是 HTML 元素和作用在元素上的样式规则的组合, 我们可以这样编写Counter:

  1. import React from 'react'
  2. import styled from 'styled-components'
  3. const StyledCounter = styled.div`
  4. /* ... */
  5. `
  6. const Paragraph = styled.p`
  7. /* ... */
  8. `
  9. const Button = styled.button`
  10. /* ... */
  11. `
  12. export default class Counter extends React.Component {
  13. state = { count: 0 }
  14. increment = () => this.setState({ count: this.state.count + 1 })
  15. decrement = () => this.setState({ count: this.state.count - 1 })
  16. render() {
  17. return (
  18. <StyledCounter>
  19. <Paragraph>{this.state.count}</Paragraph>
  20. <Button onClick={this.increment}>+</Button>
  21. <Button onClick={this.decrement}>-</Button>
  22. </StyledCounter>
  23. )
  24. }
  25. }

注意,我们在StyledCounter添加了”Styled”前缀,这样组件CounterStyledCounter 不会明明冲突,而且可以在 React Developer Tools 和 Web Inspector 中轻松识别.

在 render 方法之外定义 Styled Components

在 render 方法之外定义 styled component 很重要, 不然 styled component 会在每个渲染过程中重新创建. 这将阻止缓存生效并且大大降低了渲染速度,所以尽量避免这种情况.

推荐通过以下方式创建 styled components :

  1. const StyledWrapper = styled.div`
  2. /* ... */
  3. `
  4. const Wrapper = ({ message }) => {
  5. return <StyledWrapper>{message}</StyledWrapper>
  6. }

而不是:

  1. const Wrapper = ({ message }) => {
  2. // WARNING: 别这么干,会很慢!!!
  3. const StyledWrapper = styled.div`
  4. /* ... */
  5. `
  6. return <StyledWrapper>{message}</StyledWrapper>
  7. }

推荐阅读:Talia Marcassa 写了一篇很精彩的有关styled-components实际应用的文章,包含许多实用的见解以及与其它方案的比较Styled Components: To Use or Not to Use?

伪元素,伪类选择器和嵌套

styled-component 所使用的预处理器stylis支持自动嵌套的 scss-like 语法,示例如下:

  1. const Thing = styled.div`
  2. color: blue;
  3. `

伪元素和伪类无需进一步细化,而是自动附加到了组件:

  1. const Thing = styled.button`
  2. color: blue;
  3. ::before {
  4. content: '🚀';
  5. }
  6. :hover {
  7. color: red;
  8. }
  9. `
  10. render(
  11. <Thing>Hello world!</Thing>
  12. )

对于更复杂的选择器,可以使用与号(&)来指向主组件.以下是一些示例:

  1. const Thing = styled.div.attrs({ tabIndex: 0 })`
  2. color: blue;
  3. &:hover {
  4. color: red; // <Thing> when hovered
  5. }
  6. & ~ & {
  7. background: tomato; // <Thing> as a sibling of <Thing>, but maybe not directly next to it
  8. }
  9. & + & {
  10. background: lime; // <Thing> next to <Thing>
  11. }
  12. &.something {
  13. background: orange; // <Thing> tagged with an additional CSS class ".something"
  14. }
  15. .something-else & {
  16. border: 1px solid; // <Thing> inside another element labeled ".something-else"
  17. }
  18. `
  19. render(
  20. <React.Fragment>
  21. <Thing>Hello world!</Thing>
  22. <Thing>How ya doing?</Thing>
  23. <Thing className="something">The sun is shining...</Thing>
  24. <div>Pretty nice day today.</div>
  25. <Thing>Don't you think?</Thing>
  26. <div className="something-else">
  27. <Thing>Splendid.</Thing>
  28. </div>
  29. </React.Fragment>
  30. )

如果只写选择器而不带&,则指向组件的子节点.

  1. const Thing = styled.div`
  2. color: blue;
  3. .something {
  4. border: 1px solid; // an element labeled ".something" inside <Thing>
  5. display: block;
  6. }
  7. `
  8. render(
  9. <Thing>
  10. <label htmlFor="foo-button" className="something">Mystery button</label>
  11. <button id="foo-button">What do I do?</button>
  12. </Thing>
  13. )

最后,&可以用于增加组件的差异性;在处理混用 styled-components 和纯 CSS 导致的样式冲突时这将会非常有用:

  1. const Thing = styled.div`
  2. && {
  3. color: blue;
  4. }
  5. `
  6. const GlobalStyle = createGlobalStyle`
  7. div${Thing} {
  8. color: red;
  9. }
  10. `
  11. render(
  12. <React.Fragment>
  13. <GlobalStyle />
  14. <Thing>
  15. I'm blue, da ba dee da ba daa
  16. </Thing>
  17. </React.Fragment>
  18. )

附加额外的属性 (v2)

为了避免仅为传递一些props来渲染组件或元素而使用不必要的wrapper, 可以使用 .attrs constructor. 通过它可以添加额外的 props 或 attributes 到组件.

举例来说,可以通过这种方式给元素添加静态 props,或者传递第三方 prop 给组件(比如传递activeClassName给 React Router 的 Link). 此外也可以将dynamic props 添加到组件. .attrs 对象也接收函数,返回值也将合并进 props.

示例如下:

  1. const Input = styled.input.attrs({
  2. // static props
  3. type: "password",
  4. // dynamic props
  5. margin: props => props.size || "1em",
  6. padding: props => props.size || "1em"
  7. })`
  8. color: palevioletred;
  9. font-size: 1em;
  10. border: 2px solid palevioletred;
  11. border-radius: 3px;
  12. /* dynamically computed props */
  13. margin: ${props => props.margin};
  14. padding: ${props => props.padding};
  15. `;
  16. render(
  17. <div>
  18. <Input placeholder="A small text input" size="1em" />
  19. <br />
  20. <Input placeholder="A bigger text input" size="2em" />
  21. </div>
  22. );

正如所见,我们可以在插值中访问新创建的 props,type attribute也正确的传递给了元素.

动画

虽然使用@keyframes的 CSS 动画不限于单个组件,但我们仍希望它们不是全局的(以避免冲突). 这就是为什么 styled-components 导出 keyframes helper 的原因: 它将生成一个可以在 APP 应用的唯一实例:

  1. // Create the keyframes
  2. const rotate = keyframes`
  3. from {
  4. transform: rotate(0deg);
  5. }
  6. to {
  7. transform: rotate(360deg);
  8. }
  9. `;
  10. // Here we create a component that will rotate everything we pass in over two seconds
  11. const Rotate = styled.div`
  12. display: inline-block;
  13. animation: ${rotate} 2s linear infinite;
  14. padding: 2rem 1rem;
  15. font-size: 1.2rem;
  16. `;

注意

react-native不支持 keyframes. 请参考ReactNative.Animated API.

Keyframes are lazily injected when they’re used, which is how they can be code-splitted, so you have to use the css helper for shared style fragments:

  1. const rotate = keyframes``
  2. // ❌ This will throw an error!
  3. const styles = `
  4. animation: ${rotate} 2s linear infinite;
  5. `;
  6. // ✅ This will work as intended
  7. const styles = css`
  8. animation: ${rotate} 2s linear infinite;
  9. `

NOTE

This used to work in v3 and below where we didn’t code-split keyframes. If you’re upgrading from v3, make sure that all your shared style fragments are using the css helper!

ReactNative

styled-components 可以在 React-Native 中以同样的方式使用. 示例: Snack by Expo.

  1. import React from 'react'
  2. import styled from 'styled-components/native'
  3. const StyledView = styled.View`
  4. background-color: papayawhip;
  5. `
  6. const StyledText = styled.Text`
  7. color: palevioletred;
  8. `
  9. class MyReactNativeComponent extends React.Component {
  10. render() {
  11. return (
  12. <StyledView>
  13. <StyledText>Hello World!</StyledText>
  14. </StyledView>
  15. )
  16. }
  17. }

同时也支持复杂样式 (like transform)和简写(如 margin) 感谢 css-to-react-native !

注意

flex的工作方式类似于 CSS 简写, 而不是 React Native 中的flex用法. 设置 flex: 1 则会设置 flexShrink为1.

Imagine how you’d write the property in React Native, guess how you’d transfer it to CSS, and you’re probably right:

  1. const RotatedBox = styled.View`
  2. transform: rotate(90deg);
  3. text-shadow-offset: 10px 5px;
  4. font-variant: small-caps;
  5. margin: 5px 7px 2px;
  6. `

与 web-version 不同, React Native 不支持 keyframescreateGlobalStyle .使用媒体查询或是嵌套 CSS 也会报警.

NOTE

v2 支持百分比. 为了实现这一目标,需要为所有简写强制指定单位. 如果要迁移到v2, a codemod is available.

Simpler usage with the metro bundler

If you’d prefer to just import styled-components instead of styled-components/native, you can add a resolverMainFields configuration that includes “react-native“. This used to be supported in metro by default (and currently does work in haul) but appears to have been removed at some point.