Theme UI で Theme Driven な快適 React スタイリング生活

こんにちは。YAMAP でフロントエンドエンジニアをしている @SotaSuzuki と申します。
この記事は YAMAP エンジニア Advent Calendar 2019 の 9日目の記事となります。
拙い文章で恐縮ですが何卒よろしくお願いいたします。

## tl;dr

- Styled System の Theming は Emotion の Theming と比べて 3倍便利
- Theme UI の Theming はさらにその 2倍高機能
- Styled System の Style Prop API と TypeScript の併せ技で 3倍高速なマークアップ
- Theme UI はいいぞ

## [Styled System]([https://styled-system.com/](https://styled-system.com/)) とは

本題の Theme UI に入る前に、Theme UI のベースとなるライブラリ Styled System について簡単に説明します。

Styled System は [Theme Specification](https://github.com/system-ui/theme-specification) に基づく theming と、[Style Prop API](https://styled-system.com/api) 、Variants 機構などを備えた UI 構築用ライブラリです。
これらの機能はあくまでオリジナルの UI コンポーネントを構築するための low-level な API として提供されており、たとえば `Button` などの UI コンポーネント自体は Styled Stystem からは提供されていません。

いうなれば Styled System はメタ UI ライブラリともいうことができそうです。

Styled System の機能で個人的に強力だと思うのが [Style Prop API](https://styled-system.com/api) で、これと TypeScript を併用することでコード補完も効くようになるため、UI のスタイリングが体感で通常の3倍（当社比）になります。

#### 参考
- [System UI Theme Specification](https://system-ui.com/theme/)
- [API - Styled System](https://styled-system.com/api)
- [Variants - Styled System](https://styled-system.com/variants)
- [Ecosystem - Styled System](https://styled-system.com/ecosystem)

## Theming

Theme は Styled System 以外のライブラリでも実装されている概念です。たとえば Emotion や Material UI でも実装されています。

Styled System との比較のために、[Emotion の Theming](https://emotion.sh/docs/theming) を取り上げます。 

### emotion-theming を使った Theming

Emotion で Theming する場合、まず `ThemeProvider` に `theme` を設定します。

```js:theme.js
import React from 'react';
import styled from '@emotion/styled';
import { ThemeProvider } from 'emotion-theming';

const theme = {
  colors: {
    primary: '#3cf',
  },
};

export default () => {
  return (
    <ThemeProvider theme={theme}>
      <Main />
    </ThemeProvider>
  );
};

```
    
そして、`theme` の値を使用するコンポーネントでは `props` 、または `useTheme` hook を使い `theme` の値を取り出します。

```js:Main.js
import React from 'react';
/** @jsx jsx */
import { jsx } from '@emotion/core';
import styled from '@emotion/styled';
import { useTheme } from 'emotion-theming';

const Main = () => {
  const theme = useTheme();

  return (
    <>
      {/* with css prop */}
      <h1 css={theme => ({ color: theme.colors.primary })}>Emotion Theming</h1>

      {/* with Styled */}
      <StyledH1>Emotion Theming</StyledH1>

      {/* with useTheme hook */}
      <h1 css={{ color: theme.colors.primary }}>Emotion Theming</h1>
    </>
  );
};

const StyledH1 = styled({
  color: theme => theme.colors.primary
});

export default Main;

```

### Styled System の Theming

まず Styled System の theming は [System UI Theme Specification](https://system-ui.com/theme/) に基づいて実装されています。emotion-theming との最大の違いはこれと言ってよいでしょう。

System UI Theme Specification はその名の通り Theming の仕様です。ここでは Theme key と対応する CSS プロパティが定義されており、各CSS プロパティでは対応する Theme key に設定している値のみ使用できます。

たとえば、Theme key `colors` は `color`, `background-color`, `border-color` と、Theme key `space` は `margin`,  `padding` などと対応しています。

下記の例は、padding は `space` Theme key に対応しているため `space` の値を使えていますが、transform は `space` に対応していないため `space` の値が使えていません。

```tsx
// Theme
const theme = {
  // space が Theme key にあたる
  space: [0, 4, 8, 12]
};

// p={2} -> padding: 8px
// transform={translateY(2)} -> transform: translateY(2);
render(<Box p={2} transform={trnaslateY(2)}></Box>);
```

また、特殊な Theme key として breakpoints が存在します。これは mediaQuery のショートハンドのようなもので非常に便利です。

以下は Styled System で Theming を使用する例です。

```ts:theme.ts
const theme = {
  colors: {
    text: '#333',
    background: '#fff',
    primary: '#48f',
    secondary: '#37e',
    muted: '#e4e4e4',
  },
  space: [0, 4, 8, 12, 16, 20, 24, 32],
  fontSizes: [12, 14, 16, 18, 20, 24, 32, 48, 64, 80],
  breakpoints: ['480px', '768px', '1024px'],
  buttons: {
    primary: {
      color: 'white',
      backgroundColor: '#48f',
    },
  },
};
```

```tsx:App.tsx
import React from 'react';
import { ThemeProvider } from 'styled-components';
import {
  buttonStyle,
  ButtonStyleProps,
  color,
  ColorProps,
  background,
  backgroundProps,
  border,
  borderProps,
  display,
  DisplayProps,
  space,
  SpaceProps,
} from 'styled-system';
import theme from './theme'

export default () => {
  return (
    <ThemeProvider theme={theme}>
      <>
        {/*
        - ~479px -> padding: 8px
        - 480px~ -> padding: 12px
        - 768px~ -> padding: 16px
        */}
        <Box p={[2, 3, 4]} bg="muted">
          {/* using theme values */}
          <Button color="text" bg="background">Normal Button</Button>

          {/* using variant prop */}
          <Button variant="primary">Primary Button</Button>
        </Box>
      </>
    </ThemeProvider>
  );
}

type BoxProps = SpaceProps &
  ColorProps &
  DisplayProps &
  BorderProps;
const Box = styled.div<BoxProps>(
  space,
  color,
  display,
  border,
);

type ButtonProps = ButtonStyleProps & ColorProps & BackgroundProps;
const Button = styled.button<ButtonProps>(
  buttonStyle,
  color,
  background,
);
```

または [@styled-system/css](https://styled-system.com/css) の `css` 関数を使えば  styled-component で `theme` の値を扱うこともできます。

```tsx:FlexColorBox.tsx
import React from 'react';
import styled from 'styled-components';
import { display, DisplayProps } from 'styled-system';
import { css } from '@styled-system/css';
import { Box } from './Box'; // 先の例の Box コンポーネントを import しています

export const FlexColorBox: React.FC = styled<BoxProps & DisplayProps>(Box)(
  display,
  css({
    display: 'flex',
    alignItems: 'center',
    color: 'background',
    bg: 'primary',
    border: '1px solid',
    borderColor: 'secondary',
    p: [2, 3, 4],
  })
);
```

このように、StyledSystem の Theming は variants や breakpoints などの便利機能を多数取り揃えています。

また、System UI Theme Specification に制約ベースの仕様のため、必然的に Theme ファイルが混沌となりづらくなるのもメリットといえるでしょう。

とはいえ、Styled System 自体ははじめに説明したとおり、UI コンポーネントを構築するためのシステムであり、UI ライブラリではありません。
実際の現場におけるアプリケーション開発で Styled System をそのまま使うのは、ちょっと悠長な気もします。たとえるなら、create-react-app や next.js を使わずに react アプリをイチから構築するのに近いかもしれません。

#### 参考
- [Theming - Material-UI ](https://material-ui.com/customization/theming/)
- [Theming - Emotion](https://emotion.sh/docs/theming)

## Theme UI

> One of the primary motivations behind Theme UI is to make building themeable, constraint-based user interfaces in React as simple and as interoperable as possible.

[ThemeUI](https://theme-ui.com/) は、UI コンポーネントカタログや Styled System Theming の拡張、MDX、Color Mode、Gatsby 向けのプラグインまで、様々な要素を携えた **React 用に作られた** ライブラリです。

Theme UI 自体は Theming に特化しており、`sx` prop と MDX 向けの Theming ができるのが特徴です。他にもサブディレクトリに様々なパッケージが存在し、それぞれ開発が進められているようです。
Chrome Extension の [Theme UI DevTools](https://github.com/system-ui/theme-ui/tree/master/packages/chrome) や、アプリケーション上で Theme を編集するための UI を取り揃えている [@theme-ui/editor](https://github.com/system-ui/theme-ui/tree/master/packages/editor) といった面白いパッケージもあるので、Theme UI を使うなら目を通しておくとよいでしょう。

https://github.com/system-ui/theme-ui/tree/master/packages

ちなみに Theme UI の作者は Styled System の作者と同じ [@jxnblk](https://github.com/jxnblk) 氏（Gatsby 社勤務）です。彼は [Rebass](https://rebassjs.org/) という Styled System に基づいて構築された UI ライブラリも公開しています。

本人の Twitter や GitHub の Activity を見たところ、現在は ThemeUI のほうに注力しているようなので、これから導入するなら Rebass よりも Theme UI がよいでしょう。

ただし、ThemeUI は比較的新しいプロジェクト（2019年4月頃〜）で未だバージョン 0.2.49 です。
これからどんどん API の追加/削除/変更はされていくことが予想されるため、導入する際そのあたりは注意しておきたいところです。

## [sx prop](https://theme-ui.com/sx-prop) と theme.styles

本記事では Theme UI への Deep Dive はしません。筆者も Theme UI について、ドキュメントを通したくらいなので、まだ Theme UI について多くを語ることができないからです。

そのかわりといってはなんですが、Theme UI のさわり、**sx prop** と **theme.styles** について紹介し、本記事の締めとしたいと思います。

### sx prop

sx prop は Emotion の [css prop](https://emotion.sh/docs/css-prop) と似ていますが下記のような拡張がなされているようです。

- `theme` で設定した値が使える
- Style Prop API のショートハンドが使える

つまり Style Prop と同じ感覚でスタイルを書くことができるのです。ただし、sx props はオブジェクトリテラルしか許容しません。

sx prop を使用するにあたっては、 Emotion の css prop 同様 [JSX pragma](https://theme-ui.com/jsx-pragma)  を記述する必要があります。

```tsx:ThreeBoxes.tsx
import React from 'react';
/** @jsx jsx */ // <- これが JSX Pragma
imprt { jsx } from 'theme-ui';

const ThreeBoxes: React.FC = () => {
  return (
    <div sx={{
      display: 'flex',
      p: 2,
    }}>
      {[1, 2, 3].map(n => (
        <div key={n} sx={{
          bg: 'accent',
          color: 'background',
          p: 4,
          ml: n !== 1 ? 2 : 0,
        }}>BOX{n}</div>
      ))}
    </div>
  );
};

export default ThreeBoxes;
```

#### 参考

- [https://theme-ui.com/sx-prop](https://theme-ui.com/sx-prop)
- [https://theme-ui.com/motivation#the-css-and-sx-props](https://theme-ui.com/motivation#the-css-and-sx-props)

### theme.styles

theme.styles の `styles` は `theme` オブジェクトの key です。これの役割は以下の２つです。

- MDX のスタイリング
- グローバルなスタイリング

ひとつめの MDX のスタイリングについては、まだ筆者が試せていないため言及はしません。
なので、ふたつめのグローバルなスタイリングについて、簡単な例を出し説明したいと思います。

まずは Theme ファイルを作成します。

```ts:theme.ts

import { Theme } from 'theme-ui';
import { invert } from '@theme-ui/color'

const theme: Theme = {
  breakpoints: ['480px', '768px', '1024px'],
  colors: {
    text: '#fff',
    primary: '#48f',
    accent: 'f72',
  },
  fonts: {
    body: 'system-ui, sans-serif',
    heading: '"Avenir Next", sans-serif',
    monospace: 'Menlo, monospace',
  },
  fontSizes: [10, 12, 14, 16, 20, 24, 30, 48, 64],
  space: [0, 4, 8, 12, 16, 20, 24, 30, 60, 100],
  borders: {
    base: '1px solid #444',
    accent: '1px solid #f72',
  },
};

theme.styles = {
  h1: {
    fontSize: [5, 6, 7],
    fontFamily: 'heading',
    mt: [2, 3, 4],
    mb: ['2px', 1, 2],
  },
  button: {
    bg: 'accent',
    color: invert('accent'),
    border: 'accent',
    p: 1,
  },
};

export default theme;
```

次に、Theme ファイルで定義した theme.styles のスタイルをコンポーネントに適用します。

theme-ui から `Styled` モジュールを import して使います。これは Styled Components の `styled` に似た API です。

```tsx:App.tsx
import * as React from 'react';
/** @jsx jsx */
import { Styled, jsx, ThemeProvider } from 'theme-ui';
import theme from './theme';

const App: React.FC = () => {
  return (
    <ThemeProvider theme={theme}>
      <Styled.h1>Changing the HTML element or component</Styled.h1>
      <p>The rendered HTML element can be changed for each Styled component. This can be useful for ensuring accessible, SEO-friendly, and semantically-correct HTML is rendered while leveraging styles from the theme. This API originated in Rebass and is similar to the <Styled.button as="a" href="https://emotion.sh/docs/styled">@emotion/styled</Styled.button> API.
      </p>

      <Styled.h1 as="h2">H2 Styled as h1</Styled.h1>
      <p>blah blah blah</p>
    </ThemeProvider>
  );
};

export default App;
```

### 参考

- [https://theme-ui.com/styling-mdx](https://theme-ui.com/styling-mdx)
- [https://theme-ui.com/styled](https://theme-ui.com/styled)

## おわります

Theme UI はあんまり日本語の情報が落ちていませんでした。追いかけるなら、英語の情報を・・というより作者の Twitter とか GitHub を watch する必要がありそうです。

筆者は MDX も気になっているので、今度は Theme UI + MDX コンポーネントを試してみようかしらん、と画策しています。

以上です。

ちなみに Styled System ベースのライブラリを使うときは TypeScript を使うと、CSS  Property と Style Prop API の補完が非常に気持ちよいです。鬼に金棒コンビでおすすめです :japanese_ogre: 

## 付録

### その他の Styled System ベースのライブラリ

#### Chakra UI

[https://chakra-ui.com/](https://chakra-ui.com/)

#### GitHub Primer Components

[https://primer.style/components/](https://primer.style/components/)

### sx prop VS built-in style prop

build-in style prop というのはスタイル属性のことです。

#### sx のメリット
- ランダムな class 名を当ててくれる
- media-query を書ける
- 疑似セレクタ/疑似要素を書ける
- Theme が使える

#### style 属性のメリット
- library を必要としない
- 強い（CSS詳細度が）

### Example で使っているパッケージ

```sh
yarn add theme-ui @types/theme-ui @emotion/core @mdx-js/react styled-system styled-components @types/styled-system @/types/styled-components @styled-system/css @types/styled-system__css
```

### 今回書いた Examples 全貌（ちょっと変わっているかも）

Code Sandbox で用意しています。

#### example1: UI built with Styled System

[https://codesandbox.io/embed/styled-system-ui-j17bu?fontsize=14&hidenavigation=1&theme=dark](https://codesandbox.io/embed/styled-system-ui-j17bu?fontsize=14&hidenavigation=1&theme=dark)

[![Edit styled-system-ui](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/styled-system-ui-j17bu?fontsize=14&hidenavigation=1&theme=dark)

#### example2: Theme UI の sx prop と theme.styles

[https://codesandbox.io/embed/sx-prop-ivin7?fontsize=14&hidenavigation=1&theme=dark](https://codesandbox.io/embed/sx-prop-ivin7?fontsize=14&hidenavigation=1&theme=dark)

[![Edit sx-prop](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/sx-prop-ivin7?fontsize=14&hidenavigation=1&theme=dark)