Emotion 是一个专门为使用 JavaScript 编写 CSS 样式而设计的库,适合需要主题能力与运行时动态样式的场景。

它有两种常见的使用方式:框架无关模式,以及与 React 配合的组件化模式。

官网

GitHub

配置css属性

在 Emotion 中,可以为元素添加 css 属性;但默认情况下,框架并不知道如何解析 css 属性,因此需要配置来支持它。

TypeScript 配置 Emotion

"jsxImportSource": "@emotion/react"

否则 TypeScript 无法解析 css 属性,会报以下错误:

TS2322: Type '{ children: string; css: SerializedStyles; }' is not assignable to type 'DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>'.   Property 'css' does not exist on type 'DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>'.

create-react-app(CRA)

npx create-react-app emotion-hello --template typescript
cd emotion-hello
pnpm i @emotion/react

主要依赖版本

{
  "react": "^18.1.0",
  "react-dom": "^18.1.0",
  "react-scripts": "5.0.1",
  "typescript": "^4.6.4",
  "@emotion/react": "^11.9.0"
}

Babel

在 CRA 中,Babel 配置封装得比较死板,可以使用 @craco/craco 进行扩展。

安装@craco/craco
pnpm i @craco/craco -D
创建craco.config.js文件并配置
module.exports = {
  babel: {
    presets: [
      [
        "@babel/preset-react",
        { runtime: "automatic", importSource: "@emotion/react" },
      ],
    ],
    plugins: ["@emotion/babel-plugin"],
  },
};

JSX Pragma

在 import 时候添加注释
/** @jsx jsx */
import { css, jsx } from "@emotion/react";

会有以下错误:

 pragma and pragmaFrag cannot be set when runtime is automatic.
解决方案一
+ /** @jsxRuntime classic */
/** @jsx jsx */
解决方案二
+ /** @jsxImportSource @emotion/react */
- /** @jsx jsx */

结论

推荐使用 Babel 的方式配置 Emotion。

css prop

从类型声明中可以看到函数重载,css 方法有两种调用方式。

export function css(
  template: TemplateStringsArray,
  ...args: Array<CSSInterpolation>
): SerializedStyles;

export function css(...args: Array<CSSInterpolation>): SerializedStyles;

模板字符串

使用 css 方法时,可以直接传入模板字符串。

这里的 style 与 inline-style 或样式文件中写的样式一模一样。

这种方式更符合我们日常编写样式的习惯。

import { css } from "@emotion/react";

<div
  css={css`
    padding: 32px;
    background-color: hotpink;
    font-size: 24px;
    border-radius: 4px;
    &:hover {
      color: ${color};
    }
  `}
/>;

Object

import { css } from "@emotion/react";

const color = "white";

<div
  css={css({
    padding: 20,
    backgroundColor: "hotpink",
    fontSize: 24,
    borderRadius: 4,
    "&:hover": {
      color,
    },
  })}
/>;

可以直接给 css 传递对象,无需调用 css 方法。

import { css } from "@emotion/react";

const color = "white";

<div css={{
    padding: 20,
    backgroundColor: "hotpink",
    fontSize: 24,
    borderRadius: 4,
    "&:hover": {
        color,
    },
}}>

方法返回值

export interface SerializedStyles {
  name: string;
  styles: string;
  map?: string;
  next?: SerializedStyles;
}
  • name 就是 class 名称(默认会添加 css 前缀)
  • styles:真正的样式
  • map:sourcemap

style 优先级

import { CSSObject } from "@emotion/react";

interface PProps {
  css?: CSSObject;
}

function P(props: PProps) {
  return (
    <p
      css={{
        margin: 0,
        fontSize: 12,
        lineHeight: 1.5,
        fontFamily: "sans-serif",
        color: "black",
      }}
      {...props}
    />
  );
}
export default function ArticleText() {
  return (
    <P
      css={{
        fontSize: 14,
        fontFamily: "Georgia, serif",
        color: "darkgray",
      }}
    />
  );
}

ArticleText 结果

相当于合并样式。根据 css 规范的 “Order of Appearance”,后定义的属性值(绿色)会覆盖先定义的属性值(红色)。

.css-kcvhz8-P-ArticleText {
    margin: 0;
-    font-size: 12px;
    line-height: 1.5;
-    font-family: sans-serif;
-    color: black;
+    font-size: 14px;
+    font-family: Georgia,serif;
+    color: darkgray;
}

结论

通过 props 传入的 css 属性优先级高于组件内部的 css 属性,因此在调用组件时可以覆盖组件的默认样式。

Styled Components

样式化组件。

styled 是一种创建 React 组件、并把样式附加到该组件上的方式,它受到 styled-components 和 glamorous 的启发。

安装依赖

pnpm i @emotion/styled

如何创建样式化组件

模板字符串

创建一个 button 组件

import styled from "@emotion/styled";

const Button = styled.button`
  color: red;
  width: 200px;
  height: 50px;
`;

<Button>Hello styled components</Button>;

Object

创建一个 div 容器组件

import styled from "@emotion/styled";

const Container = styled.div({
  color: "red",
  width: 1200,
  backgroundColor: "blue",
  margin: "0 auto",
});

<Container>
  <Button>Hello styled components</Button>
</Container>;

覆盖样式化组件内部的默认样式

模板字符串

import styled from "@emotion/styled";

const Button = styled.button`
  width: 200px;
  height: 50px;
  color: ${(props) => props.color || "red"};
`;

<Button>默认颜色</Button>
<Button color="green">更改颜色</Button>

Object

将整个参数作为函数传入。

import styled from "@emotion/styled";

export const Container = styled.div((props: { backgroundColor?: string }) => ({
  color: "red",
  width: 1000,
  backgroundColor: props.backgroundColor || "blue",
  margin: "0 auto",
}));

第一个参数传入对象,第二个参数传入函数。

export const Container = styled.div(
  {
    color: "red",
    width: 1000,
    margin: "0 auto",
  },
  (props: { backgroundColor?: string }) => ({
    backgroundColor: props.backgroundColor,
  }),
);

为任意组件添加样式

styled 可以接受任何组件,并为其增加一个 className 属性(实际上就是 css 方法生成的 className)。

const Basic = ({ className }: { className?: string }) => (
  <div className={className}>Some Text</div>
);

模板字符串

import styled from "@emotion/styled";

export const Fancy = styled(Basic)`
  color: hotpink;
`;

<Fancy />;

Object

import styled from "@emotion/styled";

export const Fancy = styled(Basic)({
  color: "hotpink",
});

<Fancy />;

父组件设置子组件样式

模板字符串

import styled from "@emotion/styled";

export const Child = styled.div`
  color: red;
`;

export const Parent = styled.div`
  ${Child} {
    color: green;
  }
`;

<Parent>
  <Child>Green because I am inside a Parent</Child>
</Parent>
<Child>Red because I am not inside a Parent</Child>

只有在 Parent 中的 Child 的颜色才会变成绿色。

Object

import styled from "@emotion/styled";

const Child = styled.div({
  color: "red",
});

// TS2464 a computed property name must be of type 'string', 'number', 'symbol', or 'any'.
// https://github.com/emotion-js/emotion/issues/1275#event-6533934489
const Parent = styled.div({
  [Child as any]: {
    color: "green",
  },
});

<Parent>
  <Child>Green because I am inside a Parent</Child>
</Parent>;
<Child>Red because I am not inside a Parent</Child>;

嵌套组件

使用嵌套选择器 &(表示组件本身):

import styled from "@emotion/styled";

export const Nesting = styled.span`
  color: lightgreen;
  & > a {
    color: hotpink;
  }
`;

<Nesting>
  This is <a>nested</a>
</Nesting>;

有些时候还可以把 & 用在另一个元素的选择器中,这在某些场景下会用到。

const paragraph = css`
  color: turquoise;

  header & {
    color: green;
  }
`;
render(
  <div>
    <header>
      <p css={paragraph}>This is green since it's inside a header</p>
    </header>
    <p css={paragraph}>This is turquoise since it's not inside a header.</p>
  </div>,
);

as

别名

如果想沿用样式组件的样式,同时更改渲染出来的元素标签,可以使用 as 属性。

import styled from "@emotion/styled";

const Button = styled.button`
  color: hotpink;
`;

<Button as="a">Emotion as props</Button>;

样式组合

import { css } from "@emotion/react";

const danger = css`
  color: red;
`;

const base = css`
  background-color: darkgreen;
  color: turquoise;
`;

export const Composition = () => (
  <div>
    <div css={base}>This will be turquoise</div>
    <div css={[danger, base]}>
      This will be also be turquoise since the base styles overwrite the danger
      styles.
    </div>
    <div css={[base, danger]}>This will be red</div>
  </div>
);

<Composition />;

也可以把 css 变量的值直接传给另一个 css。

const base = css`
  color: hotpink;
`

render(
  <div
    css={css`
      ${base};
      background-color: #eee;
    `}
  >
    This is hotpink.
  </div>
)

优先级

在样式组合中,生效顺序取决于样式的调用顺序,而不是声明顺序。

Media Queries

在 Emotion 中使用 media queries 与在 CSS 中完全一样。

模板字符串

<p
  css={css`
    font-size: 30px;
    @media (min-width: 420px) {
      font-size: 50px;
    }
  `}
>
  Some text!
</p>

Object

<p
  css={css({
    fontSize: 30,
    "@media (min-width: 420px)": {
      fontSize: 50,
    },
  })}
>
  Some text!
</p>

可重用的媒体查询

根据设定的 breakpoints,可以很容易地创建响应式布局。

import { jsx, css } from "@emotion/react";

const breakpoints = [576, 768, 992, 1200];

const mediaQueries = breakpoints.map((bp) => `@media (min-width: ${bp}px)`);

render(
  <div>
    <div
      css={{
        color: "green",
        [mediaQueries[0]]: {
          color: "gray",
        },
        [mediaQueries[1]]: {
          color: "hotpink",
        },
      }}
    >
      Some text!
    </div>
    <p
      css={css`
        color: green;
        ${mediaQueries[0]} {
          color: gray;
        }
        ${mediaQueries[1]} {
          color: hotpink;
        }
      `}
    >
      Some other text!
    </p>
  </div>,
);

借助 facepaint 可以简化媒体查询

pnpm i facepaint
pnpm i @types/facepaint
const breakpoints = [576, 768, 992, 1200];
const mq = facepaint(breakpoints.map((bp) => `@media (min-width: ${bp}px)`));
<div
  css={mq({
    color: ["green", "gray", "hotpink"],
  })}
>
  Some text.
</div>;

Global

全局样式

可以编写多个 Global 样式。

styles 可以是对象,也可以是模板字符串。

import { css, Global } from "@emotion/react";

<Global
  styles={css`
    .some-class {
      color: hotpink;
    }
  `}
/>
<Global
  styles={{
    body: { margin: 0 },
    a: { textDecoration: "none", color: "red" },
    ".some-class": {
      fontSize: 50,
      textAlign: "center",
    },
  }}
/>
<a>This is red</a>
<div className="some-class">This classname is .some-class</div>

Keyframes

可以使用 keyframes 定义动画。

import styled from "@emotion/styled";
import { keyframes } from "@emotion/react";

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

export const Rotate = styled.div`
  display: inline-block;
  animation: ${rotate} 2s linear infinite;
  padding: 2rem 1rem;
  font-size: 1.2rem;
`;
<Rotate>😄</Rotate>

theming

基于上下文的主题配置,所有的子组件都可以获取到主题配置。

import { ThemeProvider } from "@emotion/react";

const theme = {
  colors: {
    primary: "hotpink",
  },
};

声明 Theme 类型

默认情况下,theme 是一个空对象,所以在 TypeScript 环境下找不到对应属性时会报错。

创建 emotion.d.ts 文件:

import "@emotion/react";

declare module "@emotion/react" {
  export interface Theme {
    color: {
      primary: string;
      positive: string;
      negative: string;
    };
  }
}

修改tsconfig.json

  "include": [
    "src",
+    "emotion.d.ts"
  ]

css prop

从 css prop 中获取上下文中的 theme。

import { ThemeProvider } from "@emotion/react";

const CSSProp = () => {
  return (
    <div css={(theme) => ({ color: theme.colors.primary })}>
      some other text
    </div>
  );
};

const theme = {
  colors: {
    primary: "hotpink",
  },
};

<ThemeProvider theme={theme}>
  <CSSProp />
</ThemeProvider>;

styled

import styled from "@emotion/styled";

const Styled = styled.div`
  color: ${(props) => props.theme.colors.primary};
`;

<ThemeProvider theme={theme}>
  <Styled>some other text</Styled>
</ThemeProvider>;

useTheme

import { ThemeProvider, useTheme } from "@emotion/react";

export const Hook = () => {
  const theme = useTheme();
  return <div css={{ color: theme.colors.primary }}>some other text</div>;
};

const theme = {
  colors: {
    primary: "hotpink",
  },
};

<ThemeProvider theme={theme}>
  <Hook />
</ThemeProvider>;

多个 theme 合并

import { ThemeProvider, withTheme } from "@emotion/react";

const theme = {
  backgroundColor: "green",
  color: "red",
};

const adjustedTheme = (ancestorTheme) => ({ ...ancestorTheme, color: "blue" });

<ThemeProvider theme={theme}>
  <ThemeProvider theme={adjustedTheme}>
    <Text>Boom shaka laka!</Text>
  </ThemeProvider>
</ThemeProvider>;

Label

给 className 添加一个后缀,提高可读性。

import { css, jsx } from "@emotion/react";

let style = css`
  color: hotpink;
  label: some-name;
`;

let anotherStyle = css({
  color: "lightgreen",
  label: "another-name",
});

let ShowClassName = ({ className }) => (
  <div className={className}>{className}</div>
);

<div>
  <ShowClassName css={style} />
  <ShowClassName css={anotherStyle} />
</div>;

从测试结果看,只是多了一个 label 名称

// 默认情况
css-1gfxf27-style
css-5pgj1t-anotherStyle
// 添加label
css-wxo79j-some-name-style
css-1i0m745-another-name-anotherStyle

ClassNames

创建一个 className 并传递给子组件,这在 React 中是一种 render props 模式。

import { ClassNames } from "@emotion/react";

let SomeComponent = (props: any) => (
  <div className={props.wrapperClassName}>
    in the wrapper!
    <div className={props.className}>{props.children}</div>
  </div>
);

<ClassNames>
  {({ css }) => (
    <SomeComponent
      wrapperClassName={css({ color: "green" })}
      className={css`
        color: hotpink;
      `}
    >
      from children!!
    </SomeComponent>
  )}
</ClassNames>;

只有通过 ClassNames 获取到的 css 才会生成一个 className。

Attaching Props

把 css 附加到一个常规的 React 组件上后,props 中的 css 优先级将高于组件内部的 css。

这与 style 优先级一节中的例子相同。

CacheProvider

安装依赖

pnpm i @emotion/cache stylis
pnpm i @types/stylis -D
import { CacheProvider, css } from "@emotion/react";
import createCache from "@emotion/cache";
import { prefixer } from "stylis";

const customPlugin = () => {};

const myCache = createCache({
  key: "my-prefix-key",
  stylisPlugins: [
    customPlugin,
    // has to be included manually when customizing `stylisPlugins` if you want to have vendor prefixes added automatically
    prefixer,
  ],
});

<CacheProvider value={myCache}>
  <div
    css={css`
      display: flex;
      width: 80px;
    `}
  >
    <div
      css={css`
        flex: 1;
        transform: scale(1.1);
        color: hotpink;
      `}
    >
      Some text
    </div>
  </div>
</CacheProvider>;

这时就会以 my-prefix-key 作为 css class 前缀,替换掉默认的 css 前缀。

Object Styles

css prop

<div
  css={{
    color: "darkorchid",
    backgroundColor: "lightgray",
  }}
>
  This is darkorchid.
</div>
const hotpink = css({
  color: "hotpink",
});

styled

import styled from "@emotion/styled";

const Button = styled.button(
  {
    color: "darkorchid",
  },
  (props) => ({
    fontSize: props.fontSize,
  }),
);

子选择器

<div
  css={{
    color: "darkorchid",
    "& .name": {
      color: "orange",
    },
  }}
>
  This is darkorchid.
  <div className="name">This is orange</div>
</div>

Media Queries

<div
  css={{
    color: "darkorchid",
    "@media(min-width: 420px)": {
      color: "orange",
    },
  }}
>
  This is orange on a big screen and darkorchid on a small screen.
</div>

Numbers

默认情况下,px 会自动添加到数字后面,除非该属性本身是无单位的 CSS 属性。

<div
  css={{
    padding: 8,
    zIndex: 200,
  }}
>
  This has 8px of padding and a z-index of 200.
</div>

数组

最后还是会平铺成一个对象。

<div
  css={[
    { color: "darkorchid" },
    { backgroundColor: "hotpink" },
    { padding: 8 },
  ]}
/>

Fallback

降级机制

将值定义成数组:如果浏览器不支持 linear-gradient,就会回退使用 red。

<div
  css={{
    background: ["red", "linear-gradient(#e66465, #9198e5)"],
    height: 100,
  }}
/>

组合

css 可以相互传递。

const hotpink = css({
  color: "hotpink",
});

const hotpinkHoverOrFocus = css({
  "&:hover,&:focus": hotpink,
});

const hotpinkWithBlackBackground = css(
  {
    backgroundColor: "black",
    color: "green",
  },
  hotpink,
);

结论

  1. 开发体验尚可,即使不安装插件也可能获得代码提示,但配置稍微麻烦一些。

  2. 感觉上,Emotion 包含了 styled-components 的功能,而 styled-components 目前也包含了 Emotion 的所有功能。

  3. React 本身也支持 inline style,Emotion 同样支持,但它扩展了 JSX 的语法,增加了一个 css prop,从而支持更复杂的样式。不过这也是它的缺点:它更改了 React。

源码