Emotion是一个专门为使用 JavaScript 编写 CSS 样式而设计的库

使用Emotion有两种主要的方法,一种是框架无关的;另一种是与 React 一起使用。

官网

GitHub

配置css属性

Emotion中,为元素添加了css属性,默认情况下,不知道如何解析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
npm 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中,封装得比较死,使用@craco/craco对其进行扩展.

安装@craco/craco
npm 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 或 style file 中写的样式是一模一样的。

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

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 soucemap

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 components 的方式,并把样式添加到该组件上。受到 styled-components and glamorous启发。

安装依赖

npm 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 可以简化媒体查询

npm i facepaint
npm 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可以是Object模板字符串

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 props 属性中获取上下文的 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

安装依赖

npm i @emotion/cache stylis
npm 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-componentsstyled-components目前也包含了Emotion所有的功能。

  3. React 本身也支持 inline style。Emotion也支持 inline style,它扩展了 jsx 的语法增加了一个css prop,可以支持复杂的样式。但这也是缺点,更改了 React。

源码