見出し画像

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の場合:トップヘッダー + 左サイドメニュー + 右コンテンツ

  • スマホの場合:トップヘッダー + 左ドロワーメニュー + コンテンツ

srccomponentsCommonButton.tsxDrawerMenu.tsxSideMenu.tsxTopHeader.tsxroutesPage1.tsxPage2.tsxPage3.tsxApp.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>
);

リポジトリ

参考


いいなと思ったら応援しよう!