この記事は、React Advent Calendar 2021 1 日目の記事です。
定期的に話題になる、UI コンポーネントでの余白 (margin, padding) をどうするか問題。
昨日も Zenn に解説記事が流れてきました。
(参照記事の参照記事)
Zenn の記事の中で、UI ライブラリーにはレイアウト (Box モデル) が当たり前に存在する と記載がされています。
(※一部引用)
例えば、Chakra UIではレイアウト(Boxモデル)に特化したコンポーネントが存在します。
Chakraのレイアウトの責務を持ったコンポーネントの豊富さは大変魅力的ですね。
そう、Chakra UI のレイアウト関連コンポーネントは、大変魅力的なのです・・・!
ということで、マージンを持たない UI コンポーネントを Chakra UI で作ってみようと思います。
保険
React を仕事で使い始めてから 3 ヶ月、Chakra UI は 1 ヶ月程度です。
Case 1. 保存ボタンコンポーネント (SaveButton)
データを保存するアクションを表す、保存ボタン (SaveButton) コンポーネントを作っていきます。
Atomic Design でいう、Molecules に該当するかなと思います。
コンポーネントの要件は次のとおりとします。
- ボタンは緑色
- 左側にチェックアイコンがついている
シンプルな実装
とりあえず要件を満たせるコンポーネントを作ってみます。
※注意: コメントは見やすさを優先して、すべて //
で統一していますが、このままでは動かない箇所がある可能性があります。
import { CheckIcon } from "@chakra-ui/icons";
import { Button, ButtonProps } from "@chakra-ui/react";
export interface SaveButtonProps extends ButtonProps {} // (1)
const SaveButton: React.FC<SaveButtonProps> = (props) => {
const { children, ...buttonProps } = props; // (2)
return (
<Button
colorScheme="green" // (3)
leftIcon={<CheckIcon />} // (4)
{...buttonProps} // (5)
>
{children} // (6)
</Button>
);
};
export default SaveButton;
No | 説明 |
---|---|
(1) | コンポーネント用の Props インターフェースを、ベースとなるコンポーネントの Props 「ButtonProps 」 を継承して定義する。 |
(2) | ボタンのテキスト children とそれ以外の Props (ButtonProps の中身) で分ける。 |
(3) | ボタンの色を緑色にする。 |
(4) | ボタンの左側にチェックアイコンをつける。(アイコンは Chakra UI が提供する @chakra-ui/icons を利用。) |
(5) | ベースコンポーネントの Props をすべて渡す。 |
(6) | ボタンのテキストを指定する。 |
使い方は次のとおりです。
import SaveButton from "./components/SaveButton";
// 中略
<SaveButton>Save</SaveButton>
このように、ボタンの文字を指定するだけでいい保存ボタンコンポーネントができました。
DevTools などで見てみると、この時点でマージンは指定されていません。
このコンポーネントの Props は、ButtonProps
を継承して定義しているため、Button
を拡張するようにデザインを変更することができます。
// 横幅を親要素いっぱいに広げる
<SaveButton w="full">ほぞーーーん</SaveButton>
// 角張ったボタンにする
<SaveButton borderRadius={0}>SAVE</SaveButton>
マージンの指定について
Button には、マージンに関する Props を持っているため、コンポーネントを定義したあとでも自由にマージンを指定することができます。
// 例) 縦並びは margin-top を設定
<SaveButton>あっちに保存</SaveButton>
<SaveButton mt={2}>こっちに保存</SaveButton>
// 例) 横並びは margin-left を設定
<SaveButton>そっちに保存</SaveButton>
<SaveButton ml={2}>どっちに保存?</SaveButton>
制限をつけた実装
マージン関連の話題からそれますが・・・。
Button のもつ Props を使えるため、柔軟である一方、ボタンの色やアイコンも上書きすることができます。
// 赤い色で、ワーニングのアイコン
<SaveButton colorScheme="red" leftIcon={<WarningIcon />}>Save?</SaveButton>
これだとコンポーネントを用意した意味がありません。
ということで、必要な機能に絞ったコンポーネントを作ってみます。
import { CheckIcon } from "@chakra-ui/icons";
import { Button, ButtonProps } from "@chakra-ui/react";
export interface SaveButtonProps {
children: string; // (1)
onClick?: React.MouseEventHandler<HTMLButtonElement>; // (2)
buttonPropsEx?: ButtonProps; // (3)
}
const SaveButton: React.FC<SaveButtonProps> = ({
children,
onClick = () => {},
buttonPropsEx = {}
}) => {
return (
<Button
colorScheme="green"
leftIcon={<CheckIcon />}
onClick={onClick}
{...buttonPropsEx} // (4)
>
{children}
</Button>
);
};
export default SaveButton;
No | 説明 |
---|---|
(1) | ボタンのテキストを指定できるようにする。 |
(2) | ボタンクリック時のイベントを指定できるようにする。 |
(3) | どうしてもボタンを拡張したくなった場合の拡張ポイント。 |
(4) | 緊急ハッチをすべて展開する。 |
制限をかけるとしても、どうしても拡張が必要な場面が出てくるかもしれません。
その場合に備えて、ButtonProps を直接指定する buttonPropsEx
という Props を用意しました。
こうすることで、柔軟性をもたせつつ、ある程度制限をかけたコンポーネントを作ることができます。
もちろん、TypeScript の型も効いています。
また、この例のように xxPropsEx
という名前にすることで、拡張している箇所を PropsEx
で検索することができるため、必要以上に拡張されていないかを調査することもできます。
// こらこら・・・!
<SaveButton
buttonPropsEx={{
colorScheme: "red",
leftIcon: <WarningIcon />
}}
>
Delete
</SaveButton>
Case 2. ヘッダー
次に、ページのヘッダーを作ってみようと思います。
Atomic Design では Organisms 相当でしょうか・・・。
コンポーネントの要件は次のとおりとします。
- 左側にプロダクトアイコンを表示する。(今回は簡略化するため、アイコンを表示する。)
- アイコンの右にメニューを表示する。
- ヘッダーの右に、ログインユーザーの情報を表示する。
よくあるヘッダーですね。
実装
Organisms レベルのコンポーネントを作るときは、基本的に <Box>
で囲ってしまってもいいでしょう。
import { StarIcon } from "@chakra-ui/icons";
import { Box, BoxProps, Heading, HStack, Spacer, Text } from "@chakra-ui/react";
export interface PageHeaderProps extends BoxProps {} // (1)
const PageHeader: React.VFC<PageHeaderProps> = ({ ...boxProps }) => {
return (
<Box px={6} py={4} borderWidth="1px" {...boxProps}> // (2)
<HStack spacing={4}>
<Heading size="md">
<StarIcon />
</Heading>
<Text>メニュー 1</Text>
<Text>メニュー 2</Text>
<Text>メニュー 3</Text>
<Spacer /> // (3)
<Text>あなた 様</Text>
</HStack>
</Box>
);
};
export default PageHeader;
No | 説明 |
---|---|
(1) | BoxProps を拡張した Props インターフェースを定義する。 |
(2) | Props をすべて渡す。 |
(3) | Chakra UI にて、中間を開けることができるコンポーネント。 |
Case 1. のシンプルな実装で説明したとおり、BoxProps を継承した Props を採用することで、Box に指定できるものは何でも指定することができます。
マージンの指定
コンポーネントを配置したあとで、必要に応じてマージンなどを指定することができる ようになります。
import PageHeader from "./components/PageHeader";
// ページヘッダーを、画面上部からちょっと離す。
<PageHeader w="100vw" mt={4} />
まとめ
Chakra UI を使用して、コンポーネントをいくつか定義してみました。
Chakra UI の Style に関する Props を使用することで、コンポーネント自体にマージンなどを定義しなくても、あとから どうにでも 柔軟にレイアウトができることが分かりました。
Chakra UI には、今回紹介した Box の他にも、中央揃えする Center
や、縦並び、横並びにする Stack
, VStack
, HStack
、複雑なレイアウトを実装できる Flex
, Grid
, SimpleGrid
など、レイアウトに関するコンポーネントが多く用意されており、大変魅力的 です!
・・・うっかり Chakra UI はいいぞ みたいな記事になりかけてしまいましたが、Chakra UI はユーティリティベースで 型安全な Tailwind CSS として使えると思うので、まだ触ったことのない方はぜひ試してみてはいかがでしょうか。
明日 12/2 は、 @denkiuo604 さんの MUI (Material-UI) で簡単なクイズアプリを作った話 です。
付録: CodeSandbox に React + TypeScript + Chakra UI の環境を作ったときのメモ
(できあがったテンプレートが こちら (CodeSandbox) です。Fork してお使いください。)
-
CodeSandbox の「Create Sandbox」をクリックし、テンプレート
React TypeScript
をクリックする。 -
Dependencies に、次の 4 つと、アイコン関連のライブラリーを追加する。(2021 年 12 月 1 日時点の Chakra UI 公式サイト より)
-
@chakra-ui/react
->1.7.2
-
@emotion/react@^11
->11.7.0
-
@emotion/styled@^11
->11.6.0
-
framer-motion@^4
->5.3.3
-
@chakra-ui/icons
->1.1.1
-
-
App.tsx
を次のとおり変更する。
import {
Box,
Center,
ChakraProvider,
Heading,
Text,
VStack
} from "@chakra-ui/react";
import "./styles.css";
export default function App() {
return (
<ChakraProvider>
<Box> {/* 外枠 */}
<Center> {/* 子要素を中央に配置する */}
<VStack> {/* 子要素を縦に並べる */}
<Heading>Hello CodeSandbox</Heading>
<Text>Start editing to see some magic happen!</Text>
</VStack>
</Center>
</Box>
</ChakraProvider>
);
}