Reactで固定長の静的なサイドメニューをMUIを使って作る

目次

はじめに

サイドメニューは、今やWEBアプリを実装する人にとっては避けて通れないものとなっています。
大抵のWEBサイトには、サイドメニューがあるのではないでしょうか。

今回はそんなサイドメニューを実装してみたいと思います。
サイドメニューの実装担当になる前に、ここで押さえておきましょう。

実物はこちら

あわせて読みたい
React Recipe Reactアプリのレシピ集

リポジトリはこちら

GitHub
GitHub - hakoratory/react-recipe Contribute to hakoratory/react-recipe development by creating an account on GitHub.

概要

今回は、ハンバーガーメニューやアコーディオンのようなギミックはなく、4つの項目を持つ最もシンプルな形のサイドメニューとなります。

以下のケースを想定して作成しています。

  • ロゴと4つの項目があり、今後増えたり減ったりすることはない(動的にしない)
  • 項目を選択すると、対応する画面が右エリアに表示される

実装

いつものように、先にコード全容です。

なお、今回はスタイルの勝手が効くため MUI のコンポーネントを多用しています。
慣れない方は Box は div、◯◯アイコンは svg、Typography は span に読み替えていただければイメージしやすいかと思います。

'use client'
import {Box, Typography} from "@mui/material";
import LocalConvenienceStoreIcon from '@mui/icons-material/LocalConvenienceStore';
import HomeRepairServiceIcon from '@mui/icons-material/HomeRepairService';
import CalendarMonthIcon from '@mui/icons-material/CalendarMonth';
import PeopleIcon from '@mui/icons-material/People';
import BuildIcon from '@mui/icons-material/Build';
//import {usePathname} from "next/navigation";
import {useState} from "react";

const Logo = () => (
    <Box
        p={2}
        display="flex"
        alignItems="center"
        sx={{
            backgroundColor: "#6573c3",
            textWrap: "nowrap"
        }}
    >
        <LocalConvenienceStoreIcon
            fontSize="large"
            sx={{
                color: "white",
                marginRight: "0.5rem"
            }}
        />
        <Typography
            variant="h5"
            component="span"
            sx={{
                color: "white"
            }}
        >
            Shop System
        </Typography>
    </Box>
)

export default function SideMenu() {
    //const pathname = usePathname()
    const [pathname, setPathname] = useState<string>('/');

    function getPageName() {
        switch (pathname) {
            case '/product':
                return 'Product'
            case '/work-schedule':
                return 'Work Schedule'
            case '/staff':
                return 'Staff'
            case '/system-setting':
                return 'System Setting'
            default:
                return ''
        }
    }

    return (
        <Box display="flex">
            <Box
                component="nav"
                height="100vh"
                bgcolor="primary"
                style={{
                    backgroundColor: "#7c88cc"
                }}
            >
                <Logo />
                <ul>
                    <Box
                        component="li"
                        pl={5}
                        py={2}
                        sx={{
                            borderBottom: '1px solid white',
                            '&:hover': {
                                opacity: 0.7
                            },
                            backgroundColor: pathname === '/product' ? '#afaffa' : 'transparent'
                        }}
                        onClick={() => setPathname('/product')}
                    >
                        <HomeRepairServiceIcon
                            fontSize="large"
                            sx={{
                                color: "white",
                                marginRight: "0.5rem",
                                position: 'relative',
                                top: -3
                            }}
                        />
                        <Typography
                            variant="subtitle1"
                            component="span"
                            sx={{
                                color: "white"
                            }}
                        >
                            Product
                        </Typography>
                    </Box>
                    <Box
                        component="li"
                        pl={5}
                        py={2}
                        sx={{
                            borderBottom: '1px solid white',
                            '&:hover': {
                                opacity: 0.7
                            },
                            backgroundColor: pathname === '/work-schedule' ? '#afaffa' : 'transparent'
                        }}
                        onClick={() => setPathname('/work-schedule')}
                    >
                        <CalendarMonthIcon
                            fontSize="large"
                            sx={{
                                color: "white",
                                marginRight: "0.5rem",
                                position: 'relative',
                                top: -3
                            }}
                        />
                        <Typography
                            variant="subtitle1"
                            component="span"
                            sx={{
                                color: "white"
                            }}
                        >
                            Work Schedule
                        </Typography>
                    </Box>
                    <Box
                        component="li"
                        pl={5}
                        py={2}
                        sx={{
                            borderBottom: '1px solid white',
                            '&:hover': {
                                opacity: 0.7
                            },
                            backgroundColor: pathname === '/staff' ? '#afaffa' : 'transparent'
                        }}
                        onClick={() => setPathname('/staff')}
                    >
                        <PeopleIcon
                            fontSize="large"
                            sx={{
                                color: "white",
                                marginRight: "0.5rem",
                                position: 'relative',
                                top: -3
                            }}
                        />
                        <Typography
                            variant="subtitle1"
                            component="span"
                            sx={{
                                color: "white"
                            }}
                        >
                            Staff
                        </Typography>
                    </Box>
                    <Box
                        component="li"
                        pl={5}
                        py={2}
                        sx={{
                            borderBottom: '1px solid white',
                            '&:hover': {
                                opacity: 0.7
                            },
                            backgroundColor: pathname === '/system-setting' ? '#afaffa' : 'transparent'
                        }}
                        onClick={() => setPathname('/system-setting')}
                    >
                        <BuildIcon
                            fontSize="large"
                            sx={{
                                color: "white",
                                marginRight: "0.5rem",
                                position: 'relative',
                                top: -3
                            }}
                        />
                        <Typography
                            variant="subtitle1"
                            component="span"
                            sx={{
                                color: "white"
                            }}
                        >
                            System Setting
                        </Typography>
                    </Box>
                </ul>
            </Box>
            <Box
                component="main"
                sx={{
                    backgroundColor: "#fffafa"
                }}
                width="100%"
                m={2}
            >
                <Typography variant="h3" sx={{ color: "#6573c3"}}>
                    {
                        getPageName()
                    }
                </Typography>
            </Box>
        </Box>

    )
}

各部分の解説

現在のページのURLを取得する

サイドメニューは各ページへのリンクであることが多いと思います。
あるページに遷移したとき、いま表示されている画面がわかるようサイドメニューの対応する項目をハイライトするとユーザーに親切ですね。
そのためには現在のページのURLが必要になります。

Next.js の App Router を使用している場合は、next/navigationusePathname フックを使用します。
今回は画面遷移しないので state で代用しています。

//import {usePathname} from "next/navigation";

export default function SideMenu() {
    //const pathname = usePathname()
    const [pathname, setPathname] = useState<string>('/');

サイドメニューとメイン画面を横並びに配置する

メニューを左側に、メイン画面を右側に配置するために1行目の display="flex" で横並びにしています。

サイドメニューは height: 100vh で縦幅いっぱいに、メイン画面は width: 100% で横幅いっぱいになるよう指定しています。

        <Box display="flex">
            <Box
                component="nav"
                height="100vh"
                bgcolor="primary"
                style={{
                    backgroundColor: "#7c88cc"
                }}
            >
                <Logo />
                <ul>
============ 中略 ============
                </ul>
            </Box>
            <Box
                component="main"
                sx={{
                    backgroundColor: "#fffafa"
                }}
                width="100%"
                m={2}
            >
                <Typography variant="h3" sx={{ color: "#6573c3"}}>
                    {
                        getPageName()
                    }
                </Typography>
            </Box>

サイドメニュー

サイドメニューの4つの項目は MUI の Box コンポーネントで出力しています。
component="li" と指定することで、<li> として出力することができます。

ポイントはハイライトした 15〜22行目の箇所です。
sx プロパティで CSS を指定しているのと、サイドメニューの項目がクリックされたときの処理を指定しています。
以下の指定がされていることを確認してください。

  • マウスホバー &:hoveropacity を変更している
  • 現在の URL とサイドメニューの項目が同じ場合はハイライトする
  • サイドメニューの項目をクリックすると、その画面に遷移する(今回はサンプルなので state を更新する)
            <Box
                component="nav"
                height="100vh"
                bgcolor="primary"
                style={{
                    backgroundColor: "#7c88cc"
                }}
            >
                <Logo />
                <ul>
                    <Box
                        component="li"
                        pl={5}
                        py={2}
                        sx={{
                            borderBottom: '1px solid white',
                            '&:hover': {
                                opacity: 0.7
                            },
                            backgroundColor: pathname === '/product' ? '#afaffa' : 'transparent'
                        }}
                        onClick={() => setPathname('/product')}
                    >
                        <HomeRepairServiceIcon
                            fontSize="large"
                            sx={{
                                color: "white",
                                marginRight: "0.5rem",
                                position: 'relative',
                                top: -3
                            }}
                        />
                        <Typography
                            variant="subtitle1"
                            component="span"
                            sx={{
                                color: "white"
                            }}
                        >
                            Product
                        </Typography>
                    </Box>
                    <Box
                        component="li"
                        pl={5}
                        py={2}
                        sx={{
                            borderBottom: '1px solid white',
                            '&:hover': {
                                opacity: 0.7
                            },
                            backgroundColor: pathname === '/work-schedule' ? '#afaffa' : 'transparent'
                        }}
                        onClick={() => setPathname('/work-schedule')}
                    >
                        <CalendarMonthIcon
                            fontSize="large"
                            sx={{
                                color: "white",
                                marginRight: "0.5rem",
                                position: 'relative',
                                top: -3
                            }}
                        />
                        <Typography
                            variant="subtitle1"
                            component="span"
                            sx={{
                                color: "white"
                            }}
                        >
                            Work Schedule
                        </Typography>
                    </Box>
                    <Box
                        component="li"
                        pl={5}
                        py={2}
                        sx={{
                            borderBottom: '1px solid white',
                            '&:hover': {
                                opacity: 0.7
                            },
                            backgroundColor: pathname === '/staff' ? '#afaffa' : 'transparent'
                        }}
                        onClick={() => setPathname('/staff')}
                    >
                        <PeopleIcon
                            fontSize="large"
                            sx={{
                                color: "white",
                                marginRight: "0.5rem",
                                position: 'relative',
                                top: -3
                            }}
                        />
                        <Typography
                            variant="subtitle1"
                            component="span"
                            sx={{
                                color: "white"
                            }}
                        >
                            Staff
                        </Typography>
                    </Box>
                    <Box
                        component="li"
                        pl={5}
                        py={2}
                        sx={{
                            borderBottom: '1px solid white',
                            '&:hover': {
                                opacity: 0.7
                            },
                            backgroundColor: pathname === '/system-setting' ? '#afaffa' : 'transparent'
                        }}
                        onClick={() => setPathname('/system-setting')}
                    >
                        <BuildIcon
                            fontSize="large"
                            sx={{
                                color: "white",
                                marginRight: "0.5rem",
                                position: 'relative',
                                top: -3
                            }}
                        />
                        <Typography
                            variant="subtitle1"
                            component="span"
                            sx={{
                                color: "white"
                            }}
                        >
                            System Setting
                        </Typography>
                    </Box>
                </ul>
            </Box>

おわりに

さて、いかがだったでしょうか?
今回は、WEB画面で見ない日はないサイドメニューを実装してみました。

この他にも、権限によって非活性になったり非表示になったり、アコーディオンで小項目を出したりなど、様々なパターンが考えられますね。
今回のコードもそうですが、サイドメニューの項目は同じコードなので、コンポーネント化もしていきたいところです。

ハコラトリは、Reactを習得したい新米エンジニアを応援しています。

Reactレシピでは、これからもReactで色々なUIを作って紹介していきますので、「こんなUIを作ってみてほしい」「こんな場合はどうすれば?」といったアイディアを募集中です!
コメントお待ちしております。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

28歳のときに音楽家からエンジニアに転向。
WEB系のシステム開発(Java, C#, Vue, React など)に携わっています。

自分らしく力を発揮できて、自信を持って生きられる人を増やすための活動をしています。

コメント

コメントする

目次