emotionを使ったCSS-in-JS

styled-components一強という流れの中、
地味に注目を集めているCSS-in-JSライブラリemotionを使ってみた。

注意点

emotionはReact向けに@emotion/coreや@emotion/styledパッケージが用意されていて、今回の記事はそちらを対象にしています。
通常のemotionとは異なる場合があります。

また現在@emotion/coreは、後述するPragmaの仕様により、v2.1以降のcreate-react-app --typescriptにおいて正常に使用できないことがあります。一応の対処法は記載していますが、根本的な解決に至っていないので注意してください。

emotionとは

公式ページ
GitHub

CSS-in-JSライブラリ。
後発ならではのいいとこ取りとパフォーマンスがウリ。
deprecatedになったglamorousというライブラリも移行先として推奨していて、事実上の後継ライブラリになっている。
Gatsbyのチュートリアルでも、このライブラリが使用されている。
そして、このブログのCSSはemotionで書いている。

JSXの中でインライン記法のように書けるCSS propが特徴。
glamというライブラリにだいぶ似てる。

/** @jsx jsx */
import * as React from 'react';
import { css, jsx } from '@emotion/core';

const ComponentEmotion: React.FC = children => {
  return (
    <div
      css={css({
        backgroundColor: '#000',
        color: '#fff',
        fontSize: 14,
      })}
    >
      {children}
    </div>
  );
};

export default ComponentEmotion;

中にcss関数を置いて、オブジェクト記法やタグ付きテンプレートリテラルで書く(後述)。

処理は独特で、特にv10になってから大きく変わっている。
emotion独自のJSX関数でstyle要素を生成し、クラスを付けて書き出してる。
propsにclassNameを渡さないで書けるのと、style当てるためにコンポーネントを分けなくて済むのが利点。
また、@emotion/styledを入れれば、styled-componentsのように書くこともできる。

styled-componentsもv4で一応CSS propに対応し、パフォーマンスが改善されている。
emotionもまだ不安定なところが多くて、styled-componentsからの乗り換え対象になるわけではない。

導入

yarn add @emotion/core @emotion/styled

emotionは、デフォルトでTypeScriptをサポートしている。
また、@emotion/coreと@emotion/styledはSSRに対応している。

基本形

CSS propで指定する。

/** @jsx jsx */
import * as React from 'react';
import { jsx, css } from '@emotion/core';

// cssを書く
const style = css({
  backgroundColor: '#000',
  color: '#fff',
  fontSize: 24,
  '&:hover': {
    color: '#000',
  },
});

const ComponentEmotion: React.FC = ({ children }) => {
  return (
    // css propにstyleを適用
    <div css={style}>{children}</div>
  );
};

export default ComponentEmotion;

タグ付きテンプレートリテラルを使用すれば、CSSと同じように書ける。

// cssをcssっぽく書く
const style = css`
  background-color: #000;
  color: #fff;
  font-size: 24px;
  &:hover {
    color: #000;
}`;

オブジェクト記法で書いた方が、CSSTypeの恩恵を受けられて良さげ。

JSX pragma

冒頭のコメントがポイント。

/** @jsx jsx */
import { jsx, css } from '@emotion/core'; // jsxをimport

これはplugin-transform-react-jsxのpragmaと言って、JSXを変換する際に呼ばれる関数を指定するもの。
このコメントを付けておくと、@emotion/coreが持つjsx関数を通して、JSXを変換する。

// pragmaなし
__WEBPACK_IMPORTED_MODULE_1_react__["createElement"]("div", { css: style }, children));

// pragmaあり
Object(__WEBPACK_IMPORTED_MODULE_1__emotion_core__["b" /* jsx */])("div", { css: style }, children));

@emotion/babel-preset-css-propをインストールして、.bablrcのpresetsを追加することでも対応できる。
(create-react-appでは、ejectしないと不可能)

yarn add @emotion/babel-preset-css-prop
.bablrc
{
  "presets": ["@emotion/babel-preset-css-prop"]
}

設定を忘れると、css部分が変換されずスタイルが当たらない。

jsx is not defined

JSX Pragmaは@emotion/coreのjsx関数を読み込むため、importする必要がある。
ただ、plugin-transform-typescriptが明示されていないimportを消してしまう現象があるようで、jsxのimportが消えてしまい、ReferenceError: jsx is not definedエラーが出る。

https://github.com/babel/babel/pull/9095
https://github.com/emotion-js/emotion/issues/1046

これは、適当なところでimportしたjsxを宣言する、という方法で無理やり対処できる。
1つならいいけど、複数のコンポーネントで無意味に宣言するのは色々とエモい。
というか、CSS書くためにpragmaを置くのは保守的にもエモすぎる。
大人しく@emotion/babel-preset-css-propの設定を書くのが良さそう。

babel-plugin-emotionについて

babel-plugin-emotionを使えば、babelのプラグインとしてコンパイルすることもできる。
その場合は、以下の機能も使用できるようになる。

  • components as selectors(@emotion/styledでcomponent selectorができる)
  • Minification
  • Dead Code Elimination
  • Source Maps
  • Contextual Class Names(変数名などがクラス名の末尾に付与される)
yarn add -D babel-plugin-emotion

Macroもあって、これをimportすればcreate-react-appでも使用できる。

/** @jsx jsx */
import React from 'react';
import { jsx } from '@emotion/core';
import css from '@emotion/css/macro';

jsxは@emotion/core、cssは@emotion/css/macroをimportすることで、sourcemapなどの恩恵を受けることができるようになる。

Emotion Create-React-App 実装メモ - UncleJavascript

ただ、TypeScriptだと型情報がなくなってちょっと辛い。

機能

styled-components

@emotion/styledをimportすることで、styled-componentsの機能が使える。
babel-plugin-emotionを入れないと、component selectorなど一部の機能が使えない。
styled-componentsで間に合っているため割愛します。

labels

labelプロパティを書くことで、classNameの末尾に任意の名前をつけられる。

const style = css({
  backgroundColor: '#000',
  color: '#fff',
  fontSize: 24,
  label: 'emotionで作ったCSS'
});

Composition

extendみたいに、既存のCSSをくっつけて新しいCSSを作れる。

// baseのcssを作成
const base = css`
  display: inline-block;
  padding: 10px;
`

// baseのcssを合成
const style = css`
  ${base};
  padding: 12px;
  background-color: #000;
  color: #fff;
  font-size: 24px;
`;

オブジェクト記法の場合は、カンマで区切る。

const style = css({
  display: 'inline-block',
  fontSize: 14,
  padding: 20,
});

const styleIsLoading = css(style, {
  pointerEvents: 'none',
});

css関数は可変長引数で、複数のオブジェクトを入れられる。
定義した順番に上書きされていく。

/** @jsx jsx */
import * as React from 'react';
import { css, jsx } from '@emotion/core';

const style = css({
  paddingTop: 50,
});

// styleにpadding-top: 20px;を上書き
const extendedStyle = css(style, {
  paddingTop: 20,
});

// padding-top: 20px;にextendedStyleを上書き
// さらにstyleで上書き
const finalExtendedStyle = css(
  {
    paddingTop: 20,
  },
  extendedStyle,
  style
);

const App: React.FC = () => {
  return (
    <div>
      <p css={style}>padding-topは50px</p>
      <p css={extendedStyle}>padding-topは50pxから上書きされて20px</p>
      <p css={finalExtendedStyle}>padding-topは結局styleの値に上書きされて50px</p>
    </div>
  );
};

export default App;

!importantなども使用可能。
ただ、CSSTypeではstringでなく、文字列リテラル型が指定されているプロパティ(positionとか)もあるので、その場合はキャストする必要がある。

Media Queries

const style = css`
  background-color: #000;
  color: #fff;
  font-size: 24px;
  @media (max-width: 340px) {
    font-size: 12px;
  }
`;

Global Styles

グローバルCSSを作成する。
styled-componentsのcreateGlobalStyle(旧:injectGlobal)とほぼ同じ。

/** @jsx jsx */
import * as React from 'react';
import { Global, jsx, css } from '@emotion/core';

// グローバルCSS
const globalStyle = css`
  html {
    display: none;
  }
`;

const ComponentEmotion: React.FC = ({ children }) => {
  return (
    <div>
      <Global styles={globalStyle} />
      {children}
    </div>
  );
};

export default ComponentEmotion;

keyframes

これもstyled-componentsとほぼ同じ。
一応アニメーション名にもlabelがつけられる。

/** @jsx jsx */
import * as React from 'react';
import { jsx, css, keyframes } from '@emotion/core';

// keyframesを作成
const anim = keyframes({
  from: {
    transform: 'scale(0)',
  },
  to: {
    transform: 'scale(1)',
  },
  label: 'emotionで作ったアニメーション',
});

// styleを作成、animを読み込む
const style = css({
  animation: `${anim} .4s ease infinite`,
  fontSize: '24px',
  textAlign: 'center',
});

const ComponentEmotion: React.FC = ({ children }) => {
  return <div css={style}>{children}</div>;
};

export default ComponentEmotion;

Theming

styled-componentsとほぼ同じ。
というかサンプルコードがほとんどstyled-components。

エモくするなら、withThemeというHOCで、themeの値をpropsに渡すことができる。

yarn add emotion-theming
src/components/content.tsx
/** @jsx jsx */
import * as React from 'react';
import { jsx, css } from '@emotion/core';
import { withTheme } from 'emotion-theming';

interface IProps {
  theme: any;
}

const Content: React.FC<IProps> = ({ theme, children }) => {
  return (
    <div
      css={css({
        backgroundColor: theme.backgroundColor,
        color: theme.color,
      })}
    >
      {children}
    </div>
  );
};

export default withTheme(Content);
src/components/componentEmotion.tsx
/** @jsx jsx */
import * as React from 'react';
import { jsx } from '@emotion/core';
import { ThemeProvider } from 'emotion-theming';
import ContentWithTheme from './content';

const theme = {
  backgroundColor: '#000',
  color: '#fff',
};

const ComponentEmotion: React.FC = () => {
  return (
    <ThemeProvider theme={theme}>
      <ContentWithTheme>こんにちは!</ContentWithTheme>
    </ThemeProvider>
  );
};

export default ComponentEmotion;

感想

emotionを使用するだけのために、JSX Pragmaを入れるのはエモい…。
babel-plugin-emotionを噛ませた時の挙動の方が、まともに思える。
今は感情的に突っ走っているけど、基本的には使いやすくて期待できるライブラリ。