Skip to main content

5.Emotionとstyled-system

Emotion とは

React は HTLM + CSS とは違った方法で DOM にスタイルを当てています。 標準的なスタイルの適応方法は、スタイルを定義したJSONを、VirtualDOMに割り当てることで反映させます。 この方法は CSS in JS と呼ばれています。 React以外のライブラリを使わなくても良いので、ちょっとしたデザインを当てたい時は便利です。

CSS in JSは大規模なweb サービス、レスポンシブデザインを考慮すると、 多くの記述が必要となり現実的ではなくなります。

EmotionはReactで利用するVirtualDOMにcssライクなスタイルを適応できるライブラリです。 例えば、webサイトで扱うテキストのスタイルを以下のように定義しておきます。

const Text = styled.p`
font-size: 12px;
font-color: #003300;
line-height: 16px;
letter-spacing: 1.1;
`

VirtualDOM 上ではこのように使うことでスタイルを使い回すことができます。

render(
<div>
<Text>Hello</Text>
</div>,
)

CSS in JS のような記述にも対応しており、個別にフォントカラーを赤色にしたい場合はこの様に上書きができる。

render(
<div>
<Text style={{ color: 'red' }}>Hello</Text>
</div>,
)

Emotionは他にもいくつか便利な機能があり、テーマ化や、VDOM から引数を受け取り動的にcssの値を変更できます。

基本的な利用方法については公式サイトを参照してください。

https://emotion.sh/docs/styled

Emotionの設計思想はstyled-componentsをベースにしています。 使い方もstyled-componentsと同じように利用できます。

https://emotion.sh/docs/styled

styld-system とは

styled-systemはEmotionを拡張して、より動的にスタイルを適用できるライブラリです。 styled-system はレスポンシブに対応しており、記法もより簡素です。 例えば、styled-system が提供する typography をEmotionの関数に渡すことで、fontSizeを動的変更できます。

const Text = styled.p`
font-size: 12px;
font-color: #003300;
line-height: 16px;
letter-spacing: 1.1;
${typography}
`
// smでは10, mdでは11, lgでは12を指定できる
<Text fontSize={[10, 11, 12]}>Hello</Text>

styled-system のドキュメントはこちら

https://styled-system.com/getting-started

インストール

Emotionとstyles-sytemをインストールします。

pnpm add @emotion/react @emotion/styled @emotion/cache styled-system

設定

next.config.tsを開いて、Emotionのcompilerを加えます。

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
/* config options here */
compiler: {
emotion: true
}
};

export default nextConfig;

テーミング

Emotionはテーマの定義が可能です。

このテーマを定義することで、簡単にスタイルを使い回すことができます。 イメージとしては、Material UIの色定義primarysecondaryのようなものです。 同じように、Emotionのテーマで border のスタイルを1px solid #33ab8fと宣言しておけば、 インデックス番号から簡単に利用できます。

ただ、この方法は動的に引数をVDOM に渡しスタイルを当てるstyled-system とは相性が悪いため利用する場面は少ないです。 レスポンシブのブレークスポイントの定義をテーマに宣言し使います。

まずは、libフォルダを作成し、theme.tsファイルを作成します。

mkdir lib
touch lib/theme.ts

中身をこの様にします。

lib/theme.ts
import { Theme } from '@emotion/react'

const breakpoints = {
md: '560px',
lg: '960px',
}

export const colors = {} as const

export const theme: Theme = {
breakpoints: [breakpoints.md, breakpoints.lg],

mediaQueries: {
nonMobile: '@media not all and (hover: none)',
md: `@media screen and (min-width: ${breakpoints.md})`,
lg: `@media screen and (min-width: ${breakpoints.lg})`,
},
space: [],
fontSizes: [],
fontWeights: [],
lineHeights: {
solid: 1,
title: 1.25,
copy: 1.5,
},
colors: colors,
rgbaColors: {
black: (opacity: number = 1) => `rgba(0, 0, 0, ${opacity})`,
},
letterSpacings: {},
fonts: {
serif: 'athelas, georgia, times, serif',
sansSerif:
'-apple-system, BlinkMacSystemFont, "avenir next", avenir, "helvetica neue", helvetica, ubuntu, roboto, noto, "segoe ui", arial, sans-serif',
},
borders: [],
radii: [],
width: [],
heights: [],
maxWidths: [],
}

この設定は基本的にEmotionのデフォルトテーマをリセットしています。

fontsは利用する font-family をデフォルトで定義しています。 breakspointssm < 640 < md < 820 < lg、左からスマホ、タブレット、PC の3つの区分を定義しています。 利用するプロジェクトに応じて分割単位を変えてください。 breakspointsを変数で切り出し、mdlgのエイリアスを作成し、mediaQueriesで利用しています。 公式サイトのこのページを参照しました。

https://styled-system.com/theme-specification/#breakpoints

mediaQueriesは Emotionで作られるコンポーネントの中で固有のレスポンシブデザインを反映するときに利用します。 nonMobileはちょっと特殊な css の指定で、スマホ、タブレット等のタップできる端末ではないことを判定しています。 hover みたいにカーソルがある場合に利用したいcss はこの nonMobile で囲み利用します。

続いて、拡張したスタイルの型定義ファイルを作成します。 emotionのテーマではmediaQueriescolorsrgbaColorsの型定義が用意されていません。

テーマの型を定義するファイルを作成し、コードを追加します。

mkdir types
touch types/theme.d.ts
import '@emotion/react'
import { colors } from '../lib/theme'

declare module '@emotion/react' {
export interface Theme {
breakpoints: string[]
mediaQueries: {
nonMobile: string
md: string
lg: string
}
space: any[]
fontSizes: any[]
fontWeights: any[]
lineHeights: {
solid: number
title: number
copy: number
}
colors: typeof colors
rgbaColors:{
black: (opacity?: number) => string
}
letterSpacings: Record<string, any>
fonts: {
serif: string
sansSerif: string
}
borders: any[]
radii: any[]
width: any[]
heights: any[]
maxWidths: any[]
}
}

レジストリの作成

Next.jsでは、すべてのスタイルを収集し、<head>タグに適用するグローバルスタイルレジストリコンポーネントの実装を提案しています。

https://nextjs.org/docs/app/building-your-application/styling/css-in-js#styled-components

ラップするコードを作成します。 lib/registry.tsxファイルを用意して以下のコードを追加します。

lib/registry.tsx
'use client'

import { useState, ReactNode, JSX } from 'react'
import { useServerInsertedHTML } from 'next/navigation'
import createCache, { EmotionCache } from '@emotion/cache'
import { CacheProvider } from '@emotion/react'

interface EmotionRegistryProps {
children: ReactNode
}

export default function EmotionRegistry({ children }: EmotionRegistryProps): JSX.Element {
// Emotionのキャッシュを一度だけ作成
const [emotionCache] = useState<EmotionCache>(() =>
createCache({
key: 'emotion-css',
prepend: true, // スタイルをheadの先頭に追加
}),
)

// サーバーサイドレンダリング時にスタイルを挿入
useServerInsertedHTML(() => {
if (emotionCache.sheet.tags.length === 0) return null // スタイルがない場合は何もしない

const styles = emotionCache.sheet.tags.map((tag) => (
<style
key={tag.id}
data-emotion={`${emotionCache.key} ${emotionCache.key}`}
dangerouslySetInnerHTML={{ __html: tag.textContent || '' }}
/>
))

emotionCache.sheet.flush() // キャッシュをクリア
return <>{styles}</>
})

// CacheProviderで子コンポーネントをラップ
return <CacheProvider value={emotionCache}>{children}</CacheProvider>
}

Providerを作成し、クライアント側で利用する諸々のスタイルを定義します。 ついでに、destyleもこちらに持ってきます。

app/Providers.tsxを作成し、以下を追加します。

app/Providers.tsx
'use client'
import StyledComponentsRegistry from '../lib/registry'
import { ThemeProvider } from '@emotion/react'
import { theme } from '../lib/theme'

const Providers = (props: React.PropsWithChildren) => {
return (
<StyledComponentsRegistry>
<ThemeProvider theme={theme}>{props.children}</ThemeProvider>
</StyledComponentsRegistry>
)
}

export default Providers

スタイルをNext.jsのページ全体で共通化するために、app/layout.tsxを開きProvidersを追加します。

app/layout.tsx
import 'destyle.css'

import Providers from './Providers'

export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}

export default function RootLayout({ children }) {
return (
<html lang="ja">
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}

以上でemotionとstyled-systemの準備が完了しました。

git add .
git commit -m "Setup emotion and styled-system"