Chakra UIでレスポンシブレイアウト
まだReact.jsは初心者ですが、人気のUIライブラリであるChakra UIを使って、よくあるヘッダーとサイドメニューからなるレスポンシブレイアウトを作ってみた。
ChakraUIとは?
Chakra UIとは、UIコンポーネントライブラリの1つで、自前でCSSを書かなくてもパラメータ指定でスタイルを記述でき、デザインに一貫性を持たせることができる。
再利用可能なコンポーネントも多く用意されていて、アラートダイアログやドロワーメニュー、トーストなど自前で作ろうとすると大変なものも簡単に実現できる。また、breakpointsを指定することでレスポンシブスタイルも簡単に対応できる(こちらを参照)。
Chakra UIの導入
既存のReactプロジェクトに追加する場合はyarn addでできるが、今回はReact App構築時にテンプレートを指定して作る。また、ルーティングやアイコンで必要なパッケージもインストールしておく。
# 既存プロジェクトに追加
yarn add @chakra-ui/react @emotion/react @emotion/styled framer-motion
# React App構築時にChakra UI, TypeScriptを指定
yarn create react-app my-app --template @chakra-ui/typescript
# Reactにルーティング機能を追加するパッケージ
yarn add react-router-dom
# React用のアイコンを利用できるパッケージ
yarn add react-icons
インストールできたら、yarn startでローカルサーバーが立ち上がり、localhost:3000でアクセスできる。
構成
今回作るWebアプリケーションでは、以下のようなレスポンシブデザインを採用。必要なコンポーネントを切り出す。
PCの場合:トップヘッダー + 左サイドメニュー + 右コンテンツ
スマホの場合:トップヘッダー + 左ドロワーメニュー + コンテンツ
src
└ components
└ CommonButton.tsx
└ DrawerMenu.tsx
└ SideMenu.tsx
└ TopHeader.tsx
└ routes
└ Page1.tsx
└ Page2.tsx
└ Page3.tsx
└ App.tsx
サンプルコード
App.tsx
export const App = () => {
// ドロワーメニューのフック
const { isOpen, onOpen, onClose } = useDisclosure();
return (
// アプリケーションのトップに置く必要がある
<ChakraProvider theme={theme}>
<Box h={{ base: "80px", lg: "100px" }}>
{/* ヘッダーコンポーネント */}
<TopHeader onOpen={onOpen} />
</Box>
<Box w="100vw" h={{ base: "calc(100vh - 80px)", lg: "calc(100vh - 100px)" }}>
<Flex w="100%" h="100%">
<BrowserRouter>
{/* ドロワーメニューコンポーネント */}
<DrawerMenu isOpen={isOpen} onClose={onClose} />
<Show above="lg">
{/* サイドメニューコンポーネント */}
<SideMenu width="20vw" />
</Show>
<Box w={{ base: "100vw", lg: "80vw" }}>
{/* ルーティング設定 */}
<Routes>
<Route path="/" element={<Page1 />} />
<Route path="/page2" element={<Page2 />} />
<Route path="/page3" element={<Page3 />} />
</Routes>
</Box>
</BrowserRouter>
</Flex>
</Box>
</ChakraProvider>
);
}
ヘッダーコンポーネント
type Props = {
onOpen: () => void;
}
export const TopHeader = (props: Props) => (
<Flex
as="header"
top={0}
width="100%"
height="100%"
bg="blue.100"
shadow="sm"
align="center"
px={{ base: 2, lg: 4 }}
>
{/* large以上で表示されるハンバーガーボタン */}
<Hide above="lg">
<Button
variant="ghost"
fontSize={{ base: "xl", lg: "3xl" }}
boxSize={{ base: 8, lg: 16 }}
onClick={() => props.onOpen()}
>
<HamburgerIcon />
</Button>
</Hide>
<Box
ml={{ base: 2, lg: 4 }}
fontSize={{ base: "xl", lg: "3xl" }}
fontWeight="bold"
>
Top Header
</Box>
</Flex>
);
サイドメニューコンポーネント
type Props = {
width: string
}
export const SideMenu = (props: Props) => {
// React Routerのページ遷移フック
const navigate = useNavigate();
return (
<Box
w={props.width}
py={3}
bg="gray.100"
>
{MenuItems.map((item) => (
<Box>
{/* 共通のメニューボタンコンポーネント */}
<CommonMenuButton
iconType={item.icon}
title={item.name}
onClick={() => navigate(item.path)}
/>
</Box>
))}
</Box>
);
}
ドロワーメニューコンポーネント
type Props = {
isOpen: boolean;
onClose: () => void;
}
export const DrawerMenu = (props: Props) => {
// React Routerのページ遷移フック
const navigate = useNavigate();
const onClickMenu = (path: string) => {
// ページ遷移
navigate(path);
// ドロワーメニューを閉じる
props.onClose();
}
return (
<Drawer
placement="left"
isOpen={props.isOpen}
onClose={props.onClose}
>
<DrawerOverlay />
<DrawerContent bg="gray.100">
<DrawerBody px={0} py={6}>
<Flex direction="column">
{MenuItems.map((item) => (
<Box>
{/* 共通のメニューボタンコンポーネント */}
<CommonMenuButton
iconType={item.icon}
title={item.name}
onClick={() => onClickMenu(item.path)}
/>
</Box>
))}
</Flex>
</DrawerBody>
</DrawerContent>
</Drawer>
);
}
共通メニューボタンコンポーネント
type Props = {
iconType: IconType;
title: string;
onClick: () => void;
}
// 共通のメニューボタン
export const CommonMenuButton = (props: Props) => (
<Button
w="100%"
h="auto"
px={6}
py={3}
variant="ghost"
onClick={props.onClick}
>
<Spacer />
{/* アイコン */}
<Icon
as={props.iconType}
w={7}
h={7}
mr={2}
/>
{/* ボタンタイトル */}
{props.title}
<Spacer />
</Button>
)
ページコンポーネント
export const Page1 = () => (
<Box h="100%">
<Center h="100%">
<h1>Page1</h1>
</Center>
</Box>
);