👩‍🎤

Emotion Best Practice(TypeScript+object styles)

2023/05/15に公開

Emotionとは何か。

css in JSと言われるJavaScriptでcssスタイルを書くためのライブラリの1つ。他に有名なcss in JSはstyled-component。

https://emotion.sh/docs/introduction

Emotionの基本的な使い方

Emotionでは一般的にcss propsを使います。

render(
  <div
    css={{
      backgroundColor: 'hotpink',
      '&:hover': {
        color: 'lightgreen'
      }
    }}
  >
    This has a hotpink background.
  </div>
)

しかし、jsxにスタイルを含めてしまうと可読性を大きく損ねてしまいます。なので、cssを使ってスタイルをjsxから分離させましょう。

import { css } from '@emotion/react'

const style = css`
  backgrond-color: hotpink;
  &:hover {
    color:lightgreen;
  }
`

render(
  <div css={style}>
    This has a hotpink background.
  </div>
)

cssやstyled-componentに慣れている人とは上記のテンプレートリテラルの方が使いやすいかも。
vs codeの拡張機能vscode-styled-componentを追加すれば、補完も効いて書きやすいと思います。

なぜTypeScript+object stylesなのか

TypeScript+object stylesはEmotionのBest Practiceにて言及されています。

Recommendations

Use TypeScript and object styles

You don't get Intellisense or type checking when using CSS strings, e.g. csscolor: blue. You can improve the developer experience and prevent style bugs by using TypeScript and object styles, which enable Intellisense and some static type checking:
https://emotion.sh/docs/best-practices

要約すると、

テンプレートリテラルだとintellisenseと静的チェックが機能しないけど、TypeScriptとobject stylesなら機能するからバグを防げるよ〜

って書いてある。

というわけで、TypeScript+object stylesで書いていきます。

TypeScript+object stylesの基本

上記の例をTypeScript+object stylesに変更していきます。

import { css } from '@emotion/react'

-const style = css`
-  backgrond-color: hotpink;
-  &:hover {
-    color:lightgreen;
-  }
-`
+const style = css({
+  backgroundColor: 'hotpink',
+  '&:hover': {
+    color: 'lightgreen'
+  }
+})

render(
  <div css={style}>
    This has a hotpink background.
  </div>
)

ざっとこんな感じ。そこまで難しくない。

cssの仕様がわからなかったら、MDNなどで調べればだいたいわかります。

ここからはEmotionの公式ドキュメントで記載されている順序に従って、自分が実際につまずいたところを中心に紹介します。

Nested Selectors

import { css } from '@emotion/react'

const breakpoint = '@media (min-width: 1000px)'

const style = css({
  backgroundColor: 'hotpink',
  h1: {
    color: 'red'
  },
  '&:hover': {
    color: 'lightgreen'
  },
  [breakpoint]:{
    backgrounrdColor: 'blue'
  }
})

render(
  <div css={style}>
    This has a hotpink background.
    <h1>red text</h1>
  </div>
)
  • 単一の要素セレクタの場合はSassなどと同様。
  • それ以外の場合はセレクタをstringにする。(''で囲む)
  • 変数の場合は[ ]で囲む。

Media Queries

import { css } from '@emotion/react'

const breakpoints = {
  sm: '576px',
  md: '768px',
  lg: '992px',
  xl: '1200px',
}

const mq = (bp: keyof typeof breakpoints) =>
  `@media (min-width: ${breakpoints[bp]})`

const style = css({
  color: 'green',
  [mq('md')]: {
    color: 'gray'
  },
  [mq('lg')]: {
    color: 'hotpink'
  }
})

render(
  <div css={style}>
    Some text!
  </div>
)

Global Styles

import { Global, css } from '@emotion/react'

const globalStyles = css({
  h1: {
    fontSize: 50,
    textAlign: 'center'
  }
})

render(
  <>
    <Global styles={globalStyles}/>
    <h1>Text</h1>
  </>
)

Keyframes

import { css, keyframes } from '@emotion/react'

const bounce = keyframes({
  'from, 20%, 53%, 80%, to': {
    transform: 'translate3d(0,0,0)'
  },
  '40%, 43%': {
    transform: 'translate3d(0, -30px, 0)'
  },
  '70%': {
    transform: 'translate3d(0, -15px, 0)'
  },
  '90%': {
    transform: 'translate3d(0,-4px,0)'
  }
})

const style = css({
  animation: `${bounce} 1s ease infinite`
})

render(
  <div css={style}>
    some bouncing text!
  </div>
)

Attaching Props

import { css } from '@emotion/react'
import { ReactNode } from 'react'

const TextWrap = (props: {
  children: ReactNode;
  isRed?: boolean;
}) => {
  return(
    <div css={textWrap({props.isRed})}>
      {props.children}
    </div>
  )
}

const textWrap = (props: { isRed?: boolean }) =>
  css({
    color: props.isRed && "red"
  })

render(
  <>
    <TextWrap>text</TextWrap>
    <TextWrap isRed >red text</TextWrap>
  </>
)

Theming

theme.d.ts
import '@emotion/react'

declare module '@emotion/react' {
  interface Theme {
    colors: Colors
  }
}

interface Colors {
  mainColor: string
  backgroundColor: string
}
theme.ts
export const theme = {
  colors: {
    mainColor: "#64363C",
    backgroundColor: "#F8C3CD",
  },
};
app.ts
import { css, ThemeProvider } from "@emotion/react";
import { theme } from "./theme";

function App() {
  return (
    <ThemeProvider theme={theme}>
      <div css={style}>Hoge</div>
    </ThemeProvider>
  );
}

const style = css({
  color: theme.color.mainColor,
  backgroundColor: theme.color.backgroundColor
})

export default App;

theme.d.tsを作ることでthemeの補完が効きます。

実装で困ったcssプロパティ

fontFamily

css
.style {
  font-family: "Gill Sans Extrabold", sans-serif;
}

Emotion
import { css } from '@emotion/react'

const style = css({
  fontFamily: ['Gill Sans Extrabold', 'sans-serif'].join(, )
})

配列で書く場合は以上の通り。stringも可。

lineHeight

css
.style {
  font-size: 40px;
  line-height: 50px;
}

Emotion
import { css } from '@emotion/react'

const style = css({
  fontSize: 40,
- lineHeight: 50,
+ lineHeight: '50px',
})

numberだと(fontSize*lineHeight)pxになってしまうため、pxの場合はstringで書かなければない。
https://developer.mozilla.org/ja/docs/Web/CSS/line-height

まとめ

Emotionの推奨するTypeScript+object stylesについて書きました。最後はコード多めだったけど説明に関しては公式ドキュメントや他の記事を見てほしい。そういう記事はそれなりにあるから。でも、Emotionの記事でobject styelsで書いているのって少ないんだよね。ということで、この記事を書きました。MUIを使う人にはobject stylesに馴染みがあるけど、cssやstyled-componentを使ってる人には馴染みがないんだよな。

https://dev.classmethod.jp/articles/react-typescript-emotion-theming/
https://chocolat5.com/ja/tips/pass-props-in-emotion/
https://mui.com/material-ui/customization/typography/

Discussion