ReactでTheme UI Part1
目次
Theme UIをReactで使うチュートリアル記事です。
この記事では簡易なWebページを作成します。超シンプルですが、実際にページを組むときに直面するあれこれ(グローバルスタイルやフォントなど)はカバーしてあります。
SandBoxに実行可能なDEMOを用意しましたが、以下の解説はローカル開発を想定しています。また、最低限Reactが動作する段階からの解説になります。
依存関係のインストール
まずは最低限必要なパッケージをインストールします。
yarn add theme-ui @emotion/core @mdx-js/react
ちなみにTheme UIは、以下を組み合わせたライブラリです。
Theme Object
まずテーマオブジェクトを作成する必要があります。これはスタイルが定義されているオブジェクトファイルです。大雑把に分別すると、最上位プロパティは以下の3つになります。
- あらかじめ用意されているもの(spaceやfontsなど)
- 自分で定義したもの(今回の例だとflexなど)
- styles以下のオブジェクト(MDXで使用。次回の記事で解説)
以下のようなテーマオブジェクトを作成しました。
src/theme.js
export default {
space: [0, 4, 8, 16, 32, 64, 128, 256, 512],
fonts: {
body:
'"メイリオ", Meiryo, "MS Pゴシック", "MS PGothic", sans-serif',
heading: '"Segoe UI", Roboto, sans-sefig',
monospace: 'Menlo, monospace',
},
fontSizes: [12, 14, 16, 20, 24, 32, 48, 64, 96],
fontWeights: {
body: 400,
heading: 700,
bold: 700,
},
lineHeights: {
body: 1.5,
heading: 1.125,
},
colors: {
text: '#000',
background: '#fff',
primary: '#07c',
secondary: '#30c',
muted: '#f6f6f6',
},
sizes: {
container: 768,
},
breakpoints: ['40em', '56em', '64em'],
flex: {
header: {
flexDirection: ['column', 'row'],
textAlign: 'center',
},
},
styles: {
root: {
fontFamily: 'body',
lineHeight: 'body',
fontWeight: 'body',
},
h1: {
color: 'text',
fontFamily: 'heading',
lineHeight: 'heading',
fontWeight: 'heading',
fontSize: 5,
},
h2: {
color: 'text',
fontFamily: 'heading',
lineHeight: 'heading',
fontWeight: 'heading',
fontSize: 4,
},
h3: {
color: 'text',
fontFamily: 'heading',
lineHeight: 'heading',
fontWeight: 'heading',
fontSize: 3,
},
h4: {
color: 'text',
fontFamily: 'heading',
lineHeight: 'heading',
fontWeight: 'heading',
fontSize: 2,
},
h5: {
color: 'text',
fontFamily: 'heading',
lineHeight: 'heading',
fontWeight: 'heading',
fontSize: 1,
},
h6: {
color: 'text',
fontFamily: 'heading',
lineHeight: 'heading',
fontWeight: 'heading',
fontSize: 0,
},
p: {
color: 'text',
fontFamily: 'body',
fontWeight: 'body',
lineHeight: 'body',
fontSize: [0, 1, 2],
},
a: {
color: 'primary',
},
pre: {
fontFamily: 'monospace',
overflowX: 'auto',
code: {
color: 'inherit',
},
},
code: {
fontFamily: "monospace",
fontSize: "inherit"
},
blockquote: {
mx: 0,
px: [0, 2, 3],
},
table: {
width: '100%',
borderCollapse: 'separate',
borderSpacing: 0,
},
th: {
textAlign: 'left',
borderBottomStyle: 'solid',
},
td: {
textAlign: 'left',
borderBottomStyle: 'solid',
},
img: {
maxWidth: '100%',
},
},
};
最上位のプロパティ名はscale、Theme Key、Varinat Groupなどと呼ばれています。これらの中で、予め用意されているプロパティ名はCSSプロパティと対応しています。例えばtheme.colors
はCSSのcolor
, background-color
, border-color
に対応しているといった具合です。
対応するキーは参照時にTheme Keyを指定する必要はありません。
colors: {
primary: '#333'
}
styles: {
h1: {
color: 'primary' // colors.primaryと記述しなくていい
}
}
ThemeProvider
テーマオブジェクトは、ThemeProvider
のtheme
propに渡すことで配下のコンポーネントで利用できます。このコンポーネントをApp.jsに定義します。
import React from 'react';
import { ThemeProvider, Styled } from 'theme-ui';
import Global from './components/Global';
import theme from './theme';
import Layout from './components/Layout';
const App = () => (
<ThemeProvider theme={theme}>
<Styled.root>
<Global />
<Layout />
</Styled.root>
</ThemeProvider>
);
export default App;
未定義コンポーネントもありますが、以降で触れていきます。また、テーマオブジェクトを渡しただけでは、配下のコンポーネントに即スタイルが適用されるわけではありません。
Global Fonts
Theme UIはグローバルのフォントの設定を行わないため、特に指定がなければ要素単位でフォントの設定が必要になります。これは面倒なので、Styles.root
というコンポーネントでラップしてフォントを設定します。
テーマオブジェクトにstyles.root
という項目があったのを確認してください。これがマッピングされます。
styles: {
root: {
fontFamily: 'body',
lineHeight: 'body',
fontWeight: 'body',
},
...
}
Global Style
Theme UIではグローバルスコープにCSSを定義しませんが、必要なケースもあります。ThemeProvider
より上位…例えばbody
の余白はこの方法でしか設定ができません。
グローバルスタイルはEmotionのGlobal
コンポーネントを利用します。
// src/components/Global.js
import React from 'react';
import { Global } from '@emotion/core';
export default props => (
<Global
styles={theme => ({
body: {
margin: 0,
padding: 0,
},
})}
/>
);
組み込みコンポーネント
@theme-ui/components
というパッケージをインストールすると、定義済みコンポーネントが使用できます。これらはBox
など一部コンポーネントを除いて、デフォルトのスタイルがセットされています(正しくは、後述のdefault variantが用意されているため)。
パッケージをインストールしておきます。
yarn add @theme-ui/components
レイアウトコンポーネント
画面に表示されるコンポーネントをラップするレイアウトコンポーネントを作成します。
/src/components/layout.js
/** @jsx jsx */
import { jsx } from 'theme-ui';
import {
Container,
Heading,
Flex,
NavLink,
Link,
} from '@theme-ui/components';
const Layout = ({ children }) => (
<div
sx={{
display: 'grid',
gridTempleteColumns: '1fr',
gridTemplateRows: 'auto 1fr auto',
height: '100vh',
}}
>
<header
sx={{
px: [0, 3, 4],
py: [0, 3, 4],
display: 'flex',
flexDirection: ['column', 'row'],
justifyContent: ['flex-start', 'space-between'],
}}
>
<Heading sx={{ fontSize: 2 }} as="h1" p={2}>
MyPage
</Heading>
<Flex as="nav" sx={{ variant: 'flex.header' }}>
<NavLink href="#" p={2}>
Blog
</NavLink>
<NavLink href="#" p={2}>
About
</NavLink>
</Flex>
</header>
<main>
<Container bg="muted" p={4}>
Test
</Container>
</main>
<footer
sx={{
px: 4,
py: 4,
mx: 'auto',
}}
>
Theme UI Sample
<Link href="https://twitter.com/__maeda">@mda</Link>
</footer>
</div>
);
export default Layout;
あえて統一感のない色々な方法でスタイルを定義しています。順番に見ていきましょう。
Custom Pragma
馴染みのない人は、1行目が不思議に思えるでしょう。
/** @jsx */
import { jsx } from 'theme-ui';
これは、JSXのコンパイル時にReact.createElement
の代わりにEmotionのjsx関数を使用することをBabelに伝達する記述です。このあと解説するsx
propを使用する場合は、これを1行目に必ず入れる必要があります。
sx prop
<div
sx={{
display: 'grid',
gridTempleteColumns: '1fr',
gridTemplateRows: 'auto 1fr auto',
height: '100vh',
}}
>
全体をラップするdiv
はグリッドレイアウトでページを組んでいます。このとき利用するのが、sx
propです。sx
はinline styleで定義するのとほぼ同じです。しかし、ハードコーティングするだけではメリットを感じられないはずです。
多少恩恵を感じられるのは以下でしょうか。
<Heading sx={{ fontSize: 2 }} as="h1" p={2}>
MyPage
</Heading>
fontSize: 2
はfont-size: 2px
ではありません。テーマオブジェクトのfontSize
が対応しています。
fontSizes: [12, 14, 16, 20, 24, 32, 48, 64, 96]
配列fontSizes[2]
は16
を指すため、fontSize: 16px
ということです。
このHeading
は前述した組み込みコンポーネントです。p
という独自のpropsがあり、これはpadding
を意味します(例えばdiv
でp
は機能しません)。このときの数値はテーマオブジェクトのspace
が該当します。
space: [0, 4, 8, 16, 32, 64, 128, 256, 512],
つまり上記ではpadding: 8px
というわけです。as
はstyles.h1
で描画するという意味です。
他にもNavLink
、Link
などの独自コンポーネントを利用しています。各々の仕様は、ドキュメントに記載されています。
Breakpoints
Flex
も独自コンポーネントです。これはvariant
という機能で、テーマオブジェクトのflex.header
を参照してスタイルを定義しています。このオブジェクト自体のネーミングは開発者が自由に定義できます。
<Flex as="nav" sx={{ variant: 'flex.header' }}>
<NavLink href="#" p={2}>
Blog
</NavLink>
<NavLink href="#" p={2}>
About
</NavLink>
</Flex>
ここではFlex Layoutでアイテムの主軸方向を設定しています。breakpoints
は、レスポンシブデザインのブレークポイントを意味します。特定のプロパティ値に配列を使うと、ブレークポイントが参照されます。
// theme object
breakpoints: ['40em', '56em', '64em'],
...
flex: {
header: {
flexDirection: ['column', 'row'],
textAlign: 'center',
},
},
主軸方向を40emまでは行、56emからは列方向にスイッチさせています。ブレークポイントは3つありますが、今回は3つ目を定義していません。この場合は直前のスタイルが維持されます。つまり横幅が64emになっても主軸は列です。
尚、1-2つ目のブレークポイントは同じで3つ目のみ変更したいときは、2つ目を空にします。
flex: {
header: {
flexDirection: ['column', ,'row'],
},
},
ブレークポイントはテーマオブジェクト内だけではなくsx
prop内でも利用できます。
<header
sx={{
px: [0, 3, 4],
py: [0, 3, 4],
display: 'flex',
flexDirection: ['column', 'row'],
justifyContent: ['flex-start', 'space-between'],
}}
>
...
</header>
px
はpadding-left
とpadding-right
の短縮表記です。こういったショートハンドがいくつか用意されています。
ちなみにsx
の中でvariant
を指定する方法は、レイアウトでよく使用されるパターンです。Gatsby公式テーマにあるスタイルのShadowingは、この機能を利用しています。
Container
Container
も独自コンポーネントで、これはコンテナをラップするのに適しています。テーマオブジェクトのsizes.contaeiner
に定義した値は、Containerの最大横幅に設定されます。
sizes: {
container: 768,
},
尚、Container自体のスタイルはtheme.layout.container
で定義することもできます。デフォルトで定義されているContainerはtheme.layout.container
を参照するので、ここにスタイルを定義してすればすべてのContainerはそれを参照します。
variant
上記然り、組み込みコンポーネントはTheme Keyに対応するvariant group
が用意されているものがあります。例えば、Button
コンポーネントはtheme.buttons
というVariant Groupに対応しています。尚、Button
がデフォルトで使用するスタイルはbuttons.primary
です。
variant
は言ってしまえば、スタイルのグループ化です。あらかじめ用意されているFonts
などは汎用的なVariant Groupとも言えるかもしれません。
組み込みコンポーネントはすべてvariant
というpropを持っているため、theme.buttons.secondary
を定義しておけば、以下のようにsx
を使用しなくてもスタイルを指定できます。
<Button variant="secondary">
Button
</Button>
variant="buttons.secondary"
と指定しなくていい点に注目してください。例えばFlex
も組み込みコンポーネントですが、Varinat Groupを持っていないため、先のレイアウトコンポーネントで<Flex varinat="header">
は機能しません。
実行
この時点で実行すると、超シンプルなWebページが確認できるはずです。
おわりに
時間をかけて記事を書いたのですが、Theme UIの良さは伝わらなかったでしょう(笑)。あれやこれやとスタイル定義方法に尺を取ったため、そこまで手が回りませんでした。
Theme UIのテーマオブジェクトはSystem UIという仕様に基づいており、できるだけ他のライブラリと併用できるように設計されています。これは少ない労力で一貫性のあるスタイルを維持するのに役立ちます。今回だと、例えばフォントやスペース、ブレークポイントなどでその一面が垣間見えたかもしれません。
制約ベースのデザイン、もしくはスタイルガイド駆動型開発とも言うようですね。CSS-in-JSの中でも面白いアプローチに思えます。
次回は今回の完成品にMDXを埋め込み、スタイリングまで行います。