はじめに
Reactでスタイルを管理する方法に、一度は悩んだことがあるのではないでしょうか。CSS Modulesはクラス名の衝突こそ防げますが、propsに応じて色やサイズを切り替えたいとなると、classNameをif文で出し分けるコードがすぐに膨らんでしまいます。かといって普通のCSSファイルに戻すと、コンポーネントとスタイルの対応関係が追いにくくなります。
Emotionは、CSSをJavaScriptの中に書き、propsやstateをそのままスタイルに反映できるCSS-in-JSライブラリです。styled関数でコンポーネント化する書き方も、既存のクラスに動的にスタイルを足す書き方もでき、Material UI(MUI)やChakra UIといった有名なコンポーネントライブラリが内部のスタイリングエンジンとして採用していることでも知られています。
とはいえ、読むより触った方が早いと思います。propsで見た目が変わるボタンを実際に動かせるサンプルを用意したので、先に挙動を見たい方はこちらからどうぞ。
Emotionとは
Emotionは、Reactを中心にJavaScript環境全般で使えるCSS-in-JSライブラリです。テンプレートリテラルでCSSを書き、styledコンポーネントとして切り出したり、既存の要素にクラス名として合成したりできます。Material UI(MUI)がv5からデフォルトのスタイリングエンジンとしてEmotionを採用しているほか、Chakra UIやTheme UIといったライブラリの内部実装にも使われており、CSS-in-JS分野で高い採用実績を持つライブラリです。
主な特徴
- propsに応じた動的スタイリング -
styledコンポーネントのテンプレートリテラル内でpropsを参照でき、条件分岐をそのままCSSの値に反映できる - css関数によるスタイルの合成 -
@emotion/cssのcss・cx関数を使うと、複数のスタイル定義を関数呼び出しだけで合成でき、Reactに依存しない場所でも利用できる - テーマ機能(ThemeProvider) -
@emotion/reactのThemeProviderでテーマオブジェクトをコンポーネントツリーに配下し、styledコンポーネントからpropsとして参照できる - 軽量かつ高速 - CSS-in-JSライブラリの中でもランタイムのオーバーヘッドが小さく、ベンチマークでも高いパフォーマンスが報告されている
- SSR(サーバーサイドレンダリング)対応 - Next.jsなどのSSR環境でクリティカルCSSを抽出し、初回描画時のスタイル漏れ(FOUC)を防げる
- TypeScriptとの親和性 - 型定義が標準で提供されており、
styledコンポーネントのprops型もそのまま推論される
インストール
Reactで使う場合は@emotion/reactと@emotion/styledをインストールします。
npm install @emotion/react @emotion/styled
yarnやpnpmを使う場合は以下の通りです。
yarn add @emotion/react @emotion/styled
pnpm add @emotion/react @emotion/styled
Reactに依存しない場所でも使いたい場合は、フレームワーク非依存の@emotion/cssを使います。
npm install @emotion/css
Emotionのサンプルを動かす
下のプレイグラウンドは、@emotion/styledで作ったChipコンポーネントにactiveというpropsを渡し、その値によって背景色と表示テキストを切り替えるサンプルです。ボタンをクリックするとReactのuseStateが更新され、styledのテンプレートリテラル内の条件式がそのまま再評価されます。
要点だけを取り出すと、Emotionのstyledは次のようにpropsを関数として受け取り、CSSの値に埋め込めます。
import styled from '@emotion/styled'
const Chip = styled.button`
padding: 8px 20px;
border: none;
border-radius: 999px;
font-weight: 600;
cursor: pointer;
background: ${props => (props.active ? '#16a34a' : '#e5e7eb')};
color: ${props => (props.active ? '#fff' : '#374151')};
transition: background 0.2s ease;
`
function StatusChip() {
const [active, setActive] = useState(true)
return (
<Chip active={active} onClick={() => setActive(!active)}>
{active ? '公開中' : '非公開'}
</Chip>
)
}
実際にクリックして色が切り替わる様子を確かめられるサンプルが以下です。backgroundや&:activeの中身を書き換えると、その場で見た目が変わります。
props.activeがfalseのときのbackgroundを#e5e7ebから#fca5a5に変えると、非公開状態のChipが赤みがかった色になります。このように、Emotionではif文でclassNameを切り替えるのではなく、CSSの値そのものをJavaScriptの式として書けるのがポイントです。
基本的な使い方
もっともシンプルな使い方は、styledでHTML要素をラップしてコンポーネント化することです。&:hoverのようなネストしたセレクタもそのまま書けます。
import styled from '@emotion/styled'
const Card = styled.div`
padding: 16px;
border-radius: 8px;
background: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
`
function App() {
return <Card>Emotionで書いたカードです</Card>
}
styled.divの代わりにstyled(MyComponent)と書けば、自作コンポーネントに対してもスタイルを追加できます。Emotionが内部でclassNameを生成し、対象のコンポーネントに渡してくれる仕組みです。
実践的なユースケース
サイズとバリアントで出し分けるボタンコンポーネント
デザインシステムでは、同じButtonコンポーネントでもvariant(primary/danger など)やsize(small/large など)によって見た目を切り替えたい場面が多くあります。Emotionのstyledはpropsをそのまま条件式に使えるため、バリアントごとに別コンポーネントを作らずに済みます。
const Button = styled.button`
padding: ${props => (props.size === 'large' ? '12px 24px' : '8px 16px')};
font-size: ${props => (props.size === 'large' ? '16px' : '14px')};
border-radius: 6px;
border: none;
color: #fff;
cursor: pointer;
background: ${props =>
({ primary: '#2563eb', danger: '#dc2626', neutral: '#4b5563' }[props.variant])};
`
下のサンプルでは、variantとsizeをボタンで切り替えるたびに、プレビューのButtonコンポーネントの色とサイズがリアルタイムに変わります。
colorsオブジェクトに新しいキー(例えばsuccess: '#16a34a')を追加し、Pickerの配列にも'success'を足すと、そのままバリアントを1種類増やせます。propsとCSSの値がオブジェクトのマッピングで結びついているため、見た目の追加がJavaScriptのデータ操作だけで完結します。
cxでスタイルを合成する
複数のスタイル定義を条件によって組み合わせたい場合は、フレームワーク非依存の@emotion/cssが便利です。css関数はスタイルからクラス名を生成し、cx関数はそのクラス名同士を合成します。Reactのstyledを使わずに、既存のマークアップへ後からEmotionのスタイルを足したいときに向いています。
import { css, cx } from '@emotion/css'
const base = css`
padding: 16px;
border-radius: 8px;
border: 1px solid #e5e7eb;
`
const highlighted = css`
border-color: #f59e0b;
background: #fffbeb;
`
const className = cx(base, isHighlighted && highlighted)
下のサンプルでは、チェックボックスをオンにするとcx(base, highlighted)が評価され、カードにhighlightedクラスの枠線と背景色が追加されます。
cxの第2引数にはchecked && highlightedのように条件式をそのまま渡せます。checkedがfalseのときはfalseが渡るだけでcxが無視してくれるため、三項演算子で空文字列を渡すような書き方をせずに済みます。
ThemeProviderでライト/ダークテーマを切り替える
複数のコンポーネントで同じ色・余白の値を使い回したい場合は、@emotion/reactのThemeProviderが使えます。テーマオブジェクトをコンポーネントツリーの上位で渡しておくと、配下のstyledコンポーネントはpropsとしてthemeを受け取れます。
import { ThemeProvider } from '@emotion/react'
import styled from '@emotion/styled'
const themes = {
light: { bg: '#ffffff', text: '#111827', border: '#e5e7eb' },
dark: { bg: '#111827', text: '#f9fafb', border: '#374151' },
}
const Panel = styled.div`
background: ${props => props.theme.bg};
color: ${props => props.theme.text};
border: 1px solid ${props => props.theme.border};
`
下のサンプルでは、ボタンを押すたびにThemeProviderに渡すthemeがlightとdarkで切り替わり、配下のPanelとTitleが持つthemepropsも連動して更新されます。
themesオブジェクトにaccentのようなキーを増やし、Panelのテンプレートリテラルからprops.theme.accentを参照すれば、テーマに新しい役割の色を1つ追加できます。デザイントークンをコンポーネント側にハードコードせず、themeオブジェクト側に集約できるのがこのパターンの利点です。
まとめ
Emotionは、propsに応じたスタイルの出し分け、css/cxによるスタイルの合成、ThemeProviderによるテーマ管理という3つの機能を、いずれも小さなAPIだけで実現できるCSS-in-JSライブラリです。CSS ModulesやプレーンなCSSではclassNameの切り替えロジックが増えがちな「propsで見た目が変わるコンポーネント」を、Emotionならstyledのテンプレートリテラルの中に自然に書き込めます。
まずは今回のサンプルのようにstyledで単一のコンポーネントを書くところから始めて、慣れてきたらThemeProviderでデザイントークンを共通化する、という順序で取り入れてみてください。
