この記事について
CSS書けないしデザインセンスも皆無な自分が個人開発で画面を作る際、どのようにしてそれなりの見た目のものを作ったかについてまとめた記事
読者は↓のような人を想定
- CSSはmarginくらいしか知らない
- デザインなんてやったことない
- Reactの基本的なこと(コンポーネントの作り方など)はわかる
戦略
最低限のCSSで見栄えのいい画面を作る戦略の紹介
結論から言うとCSSには出来合いのコンポーネントを使えば書かなくていいものと絶対に逃れられないものの2種類あるので、前者はライブラリを使うことで作業量を削減しようという作戦
部品の見た目を決めるCSSと配置を決めるCSS
CSSは大きく分けて2つに分けられる(と思っている)
部品の見た目を決めるCSS
ボタンの角を丸くしたり影をつけたり色を変えたり...といったことをして実装されている
このようなCSS(borderとかbackground-colorとか)がこれに該当する
部品の配置を決めるCSS
Googleのログインフォーム
ロゴやテキストボックス、ボタンなどの各部品をいい感じに並べている
marginやdisplayなどがこれに該当する
ライブラリを使ってCSSの記述量を減らそう
上記のCSSのうち部品の見た目を決めるCSS
に関してはこれらのライブラリを用いることで記述する必要がなくなる
部品の配置を決めるCSS
に関してはどうあがいても逃れられないので記述する必要はあるが、アーキテクチャを工夫したりライブラリを利用することでCSS初心者でも比較的書きやすくしたり見通しをよくしてメンテナンスしやすくしたりはできる
※(詳細は後述)Material-UIやBootstrapなどのライブラリには部品の配置を決めるためのコンポーネントが用意されているのでさらにCSSの記述量を減らすことが可能
ライブラリ/フレームワーク紹介
今回はReactをベースにし、以下の2種類のデザイン用ライブラリを組み合わせて実装する
- おしゃれにスタイリングされたコンポーネントを提供するライブラリ(コンポーネントライブラリ)
-
部品の見た目を決めるCSS
はこれに一任する
-
- ReactのコンポーネントのCSSを書くためのライブラリ(CSSフレームワーク)
-
部品の配置を決めるCSS
はこれを使って記述する
-
それぞれいくつか候補をあげていくので適当に見繕ってインストールしてほしい
ちなみに自分が選択したのはMaterial-UIとMaterial-UIのsx props
コンポーネントライブラリ
ざっと触った感じ機能に大きな差があるわけではないので見た目の好みで決めていいと思う
特にこだわりがなければコミュニティが大きく更新も頻繁なMaterial-UIかBootstrapがよさそう
Material-UI
マテリアルデザインに沿って実装されたコンポーネントを提供
歴史が長くドキュメントが豊富
Bootstrap
フラットデザインのコンポーネントを提供
こちらも古株でドキュメントには困らない
Semantic-UI
Bootstrapと似た感じのコンポーネント
上2つと比べるとマイナーでドキュメントも少ない
CSSフレームワーク
styled-components
Reactのコンポーネント単位でのスタイリング機能を提供するフレームワーク
const Button = styled.button`
background: ${props => props.primary ? "palevioletred" : "white"};
color: ${props => props.primary ? "white" : "palevioletred"};
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid palevioletred;
border-radius: 3px;
`;
render(
<div>
<Button>Normal</Button>
<Button primary>Primary</Button>
</div>
);
CSSを文字列で表現しているためテンプレートリテラルで変数を埋め込むことも可能
コンポーネント単位なのでどうしても↓2つに比べて記述量が多くなってしまうのが玉にキズ
ただしこちらの記事のようにDOM、スタイリング、依存性注入とコンポーネントを細かく分けて実装する方法だとかなり便利
Tailwind CSS
UtilityFirst
(汎用的なCSSのクラスを作りそれらを組み合わせてスタイリングする手法)なCSSフレームワーク
shadow-sm
shadow-lg
のようにスタイルの大きさをenum的に表現してくれているのがgood
ちょっと凝ったデザインにしようものならクラス名が肥大化してしまうのが難点
MUI.sx props
単体のフレームワークではなくMaterial-UIのコンポーネントのsxというpropsに直接CSSを渡す方法
CSSをjavascriptのObjectで表現するので何かと融通が利く
若干話は逸れるがMaterial-UIなどにはこちらのように部品の配置を決めるためのコンポーネントが用意されているため、それらを活用すればsx propsで記述するCSSの量もグッと減らせる
コード例
import * as React from 'react';
import Box from '@mui/material/Box';
export default function BoxSx() {
return (
<Box
sx={{
width: 300,
height: 300,
backgroundColor: 'primary.dark',
'&:hover': {
backgroundColor: 'primary.main',
opacity: [0.9, 0.8, 0.7],
},
}}
/>
);
}
実装方針
ReactのプロジェクトにMaterial-UIをインストールしてコンポーネントを実装する
コンポーネントをどういった粒度で分割していくかについてはAtomic Designに従って決定する
Atomic Design
UI設計の方法論の1つ
詳細についてはわかりやすい記事がたくさんあるので各自参照していただきたい
ここではAtomic Designのざっくりとした解説とCSSのコーティング規約(っぽいもの)を各階層ごとに書き連ねていく
Atoms
Atomic Designの最小単位となる階層
↑のLABEL、INPUT、BUTTONのようにこれ以上細かく分解できないような粒度のコンポーネントをこの階層に実装する
CSSは自身の見た目を決めるもののみ記述するべきで、自身の配置や大きさについては関心を持たないようにするべき
具体的には↓
// OK
.btn {
text-align: center;
background-color: blue;
padding: 8px;
border-width: 2px;
}
// NG
.btn {
margin-top: 8px;
width: 200px;
}
Molecules
Atoms
を組み合わせて実装されるコンポーネント
これも自身の見た目に関するCSSのみ記述可能
Organisms
Atoms
やMolecules
を組み合わせて実装されるコンポーネント
これも自身の見た目(ロゴと検索フォームを横並びにするなど、自身の子コンポーネントの配置が主)に関するCSSのみ記述可能
Templates
Organisms
を組み合わせてページ全体を構成するコンポーネント
ただし実際にTemplates
コンポーネント内でOrganisms
コンポーネントを呼び出すわけではなく、ヘッダーはここで動画があそこで...という風にレイアウトを決めているだけ
CSSは↑にある通りOrganisms
コンポーネントをどのように配置するか、サイズはどれくらいかなどを記述していく
Pages
Templates
コンポーネントに対して必要なOrganisms
コンポーネントを渡すことでページを表現するためのコンポーネント
CSSはこのコンポーネントでは書かない
実践
以下のコマンドでプロジェクト作成
npx create-react-app {プロジェクト名} --template typescript
cd {プロジェクト名}
npm install @mui/material @emotion/react @emotion/styled
ディレクトリ構成
src/
├ components/
│ ├ atoms/
│ ├ molecules/
│ ├ organisms/
│ ├ templates/
│ └ pages/
└ styles/theme.ts
components
以下にAtomicDesignに従ってコンポーネントを実装していく
また、グローバルなスタイル定義はstyles/theme.ts
に書く
ThemeProvider
Material-UIのコンポーネントのCSSをグローバルに管理できるプロバイダ
以下のようにテーマオブジェクトを作成して<ThemeProvider />
に渡してやることで、全子コンポーネントのCSSを変更できる
const theme = createTheme({
status: {
danger: orange[500],
},
});
<ThemeProvider theme={theme}>
...
</ThemeProvider>
以下ではThemeProviderでカスタムできる項目のうちよく使いそうなものだけ抜粋して紹介
Color
コンポーネントの色を変更できる
const theme = createTheme({
palette: {
primary: {
light: '#0066ff',
main: '#0044ff',
dark: '#0022ff',
},
secondary: {
main: '#ff4400',
// light, darkを省略した場合mainの値に応じて自動で決定される
},
},
});
カラーコードを自分で作るのが面倒な場合は↓を使うと便利
Spacing
marginやpaddingなどの大きさをカスタマイズできる
コンポーネント実装
今回はよくあるログインページを実装してみる
Atoms
基本的にMaterial-UIのコンポーネントを少しカスタマイズするだけ
import * as mui from "@mui/material";
type ButtonProps = {
label: string;
type?: "submit" | "button";
color?: "primary" | "secondary";
onClick?: () => void;
};
export function Button(props: ButtonProps) {
return (
<mui.Button
variant="contained"
color={props.color}
onClick={() => props.onClick && props.onClick()}
type={props.type}
fullWidth={true}
>
{props.label}
</mui.Button>
);
}
その他Typography、TextFieldのコンポーネントも作っておく
Molecules
今回は対象となるコンポーネントがないので省略
Organisms
※今回の主題はデザインなので関数などは適当
この階層ではAtomsのコンポーネントを呼び出し、それらの配置を決めるコンポーネントを実装する
以下はログインフォームのコンポーネント
import { Paper, Stack } from "@mui/material";
import { Button } from "../atoms/Button";
import { Textbox } from "../atoms/Textbox";
import { Typography } from "../atoms/Typography";
export function LoginForm() {
return (
<Paper
sx={{
padding: 4,
}}
>
<Stack spacing={4}>
<Typography value="Login" variant="h3" />
<Textbox
value="test@example.com"
onChange={() => null}
label="Email"
type="email"
/>
<Textbox
value="password"
onChange={() => null}
label="Password"
type="password"
/>
<Button label="LOGIN" color="primary" />
</Stack>
</Paper>
);
}
新たに登場した<Paper />
、<Stack />
について簡単に補足
Paper
Stack
コンポーネントを1次元に並べるコンポーネント
direction
をcolumn(デフォルト)にすると垂直方向に、rowにすると水平方向に子コンポーネントを1列に並べる
他にも2次元に並べるための<Grid>
や中央揃えするための<Container>
などが用意されている
ついでにヘッダーのコンポーネントも実装する
import { AppBar } from "@mui/material";
import { Typography } from "../atoms/Typography";
export function Header() {
return (
<AppBar position="relative">
<Typography value="My App" variant="h1" />
</AppBar>
);
}
Templates
Organismsのコンポーネントの配置を決めるためのコンポーネント
import { Box, Container } from "@mui/material";
type AuthenticationTemplateProps = {
header: JSX.Element;
form: JSX.Element;
};
export function AuthenticationTemplate(props: AuthenticationTemplateProps) {
return (
<Box
sx={{
width: "100vw",
height: "100vh",
}}
>
<Box>{props.header}</Box>
<Container
sx={{
marginTop: 10,
}}
>
{props.form}
</Container>
</Box>
);
}
Pages
Templatesに対してOrganismsのコンポーネントを流し込むためのコンポーネント
(今回は省略したものの)各コンポーネントで使用するコールバック関数や引数などもこのコンポーネントで注入する形になる
import { Header } from "../organisms/Header";
import { LoginForm } from "../organisms/LoginForm";
import { AuthenticationTemplate } from "../templates/AuthenticationTemplate";
export function LoginPage() {
return <AuthenticationTemplate header={<Header />} form={<LoginForm />} />;
}
実装完了したらnpm start
で実行