Emotion 是一个专门为使用 JavaScript 编写 CSS 样式而设计的库,适合需要主题能力与运行时动态样式的场景。
它有两种常见的使用方式:框架无关模式,以及与 React 配合的组件化模式。
配置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,
);
结论
-
开发体验尚可,即使不安装插件也可能获得代码提示,但配置稍微麻烦一些。
-
感觉上,
Emotion包含了styled-components的功能,而styled-components目前也包含了Emotion的所有功能。 -
React 本身也支持 inline style,
Emotion同样支持,但它扩展了 JSX 的语法,增加了一个css prop,从而支持更复杂的样式。不过这也是它的缺点:它更改了 React。
DISCUSSION
讨论与反馈