<MultiLevelMenu>

This Enterprise EditionReact Admin Enterprise Edition icon component adds support for nested sub menus in the left navigation bar.

MultiLevelMenu

When a React-admin application grows significantly, the default menu might not be the best solution. The <MultiLevelMenu> can help unclutter the navigation: it renders a menu with an infinite number of levels and sub-menus. Menu Items that are not at the top level are rendered inside a collapsible panel.

When a React-admin application grows significantly, the default <Menu> component might not be the best solution. The <MultiLevelMenu> can help unclutter the navigation: it renders a menu with an infinite number of levels and sub menus. Menu Items that are not at the top level are rendered inside a collapsible panel.

Test it live on the Enterprise Edition Storybook.

Usage

Create a custom Menu component using <MultiLevelMenu> as root instead of <Menu>. Menu entries should be <MultiLevelMenu.Item>, <MultiLevelMenu.ResourceItem> or <MultiLevelMenu.DashboardItem> components. They are very similar to the default <MenuItemLink> from react-admin, except that they accept other <MultiLevelMenu.Item> (or <MultiLevelMenu.ResourceItem> or <MultiLevelMenu.DashboardItem>) as their children.

For instance, here is how to create a menu with sub menus for each artist genre. The menu target is actually the same page - the artists list - but with a different filter:

import { MultiLevelMenu } from '@react-admin/ra-navigation';

import DashboardIcon from '@mui/icons-material/Dashboard';
import MusicIcon from '@mui/icons-material/MusicNote';
import PeopleIcon from '@mui/icons-material/People';

const MyMenu = () => (
    <MultiLevelMenu>
        <MultiLevelMenu.DashboardItem />
        <MultiLevelMenu.ResourceItem resource="songs" icon={<MusicIcon />}  />
        {/* The empty filter is required to avoid falling back to the previously set filter */}
        <MultiLevelMenu.ResourceItem resource="artists" to={'/artists?filter={}'} icon={<PeopleIcon />}>
            <MultiLevelMenu.Item name="artists.rock" to={'/artists?filter={"type":"Rock"}'} label="Rock">
                <MultiLevelMenu.Item name="artists.rock.pop" to={'/artists?filter={"type":"Pop Rock"}'} label="Pop Rock" />
                <MultiLevelMenu.Item name="artists.rock.folk" to={'/artists?filter={"type":"Folk Rock"}'} label="Folk Rock" />
            </MultiLevelMenu.Item>
            <MultiLevelMenu.Item name="artists.jazz" to={'/artists?filter={"type":"Jazz"}'} label="Jazz">
                <MultiLevelMenu.Item name="artists.jazz.rb" to={'/artists?filter={"type":"RB"}'} label="R&B" />
            </MultiLevelMenu.Item>
        </MultiLevelMenu.ResourceItem>
    </MultiLevelMenu>
);

Note that each <MultiLevelMenu.Item> requires a unique name attribute.

Then, create a custom layout using the <Layout> component and pass your custom menu component to it. Make sure you wrap the layout with the <AppLocationContext> component.

// in src/MyLayout.js
import { Layout } from 'react-admin';
import { AppLocationContext } from '@react-admin/ra-navigation';

import { MyMenu } from './MyMenu';

export const MyLayout = ({ children }) => (
  <AppLocationContext>
    <Layout menu={MyMenu}>
        {children}
    </Layout>
  </AppLocationContext>
);

<AppLocationContext> is necessary because ra-navigation doesn’t use the URL to detect the current location. Instead, page components declare their location using a custom hook (useDefineAppLocation()). This allows complex site maps, with multiple levels of nesting. That’s the reason why each <MultiLevelMenu.Item> requires a unique name, that matches a particular page location.

You can set the AppLocation for a given page like so:

import { useDefineAppLocation } from '@react-admin/ra-navigation';

const ArtistRockList = () => {
    useDefineAppLocation('artists.rock');
    return <h1>Artist Rock List</h1>;
};

And then use this AppLocation as name for <MultiLevelMenu.Item>:

<MultiLevelMenu.Item
    name="artists.rock"
    to={'/artists/rock'}
    label="Rock"
>

Check the ra-navigation documentation to learn more about App Location.

Finally, pass this custom layout to the <Admin> component

// in src/App.js
import { Admin, Resource } from "react-admin";

import { MyLayout } from './MyLayout';

const App = () => (
    <Admin
        layout={MyLayout}
        dataProvider={...}
    >
        // ...
    </Admin>
);

Props

Prop Required Type Default Description
children Optional ReactNode - The Menu Items to be rendered.
initialOpen Optional boolean false Whether the menu items with sub menus should be open initially. Has no effect if using the categories variant.
openItemList Optional string[] - List of names of menu items that should be opened by default.
sx Optional SxProps - Style overrides, powered by MUI System

Additional props are passed down to the root <div> component.

children

The menu items to render:

// in src/MyMenu.js
import { MultiLevelMenu } from "@react-admin/ra-navigation";

import MusicIcon from '@mui/icons-material/MusicNote';
import PeopleIcon from '@mui/icons-material/People';

const MyMenu = () => (
    <MultiLevelMenu>
        <MultiLevelMenu.DashboardItem />
        <MultiLevelMenu.ResourceItem resource="songs" icon={<MusicIcon />}  />
        {/* The empty filter is required to avoid falling back to the previously set filter */}
        <MultiLevelMenu.ResourceItem
            resource="artists"
            to={'/artists?filter={}'}
            icon={<PeopleIcon />}
        >
            <MultiLevelMenu.Item
                name="artists.rock"
                to={'/artists?filter={"type":"Rock"}'}
                label="Rock"
            >
                <MultiLevelMenu.Item
                    name="artists.rock.pop"
                    to={'/artists?filter={"type":"Pop Rock"}'}
                    label="Pop Rock"
                />
                <MultiLevelMenu.Item
                    name="artists.rock.folk"
                    to={'/artists?filter={"type":"Folk Rock"}'}
                    label="Folk Rock"
                />
            </MultiLevelMenu.Item>
            <MultiLevelMenu.Item
                name="artists.jazz"
                to={'/artists?filter={"type":"Jazz"}'}
                label="Jazz"
            >
                <MultiLevelMenu.Item
                    name="artists.jazz.rb"
                    to={'/artists?filter={"type":"RB"}'}
                    label="R&B"
                />
            </MultiLevelMenu.Item>
        </MultiLevelMenu.ResourceItem>
    </MultiLevelMenu>
);

Check the <MultiLevelMenu.Item> section for more information.

initialOpen

Whether the menu items with sub menus should be open initially. Has no effect if using the categories variant. Defaults to false.

export const MyMenu = () => (
    <MultiLevelMenu initialOpen>
        // ...
    </MultiLevelMenu>
);

openItemList

List of names of menu items that should be opened by default. If the menu item to be opened is nested, you have to fill in the name of all the parent items. Ex: ['artists', 'artists.rock', 'artists.rock.pop']

import { MultiLevelMenu } from '@react-admin/ra-navigation';
const MyMenu = () => (
    <MultiLevelMenu openItemList={['artists', 'artists.rock', 'artists.rock.pop']}>
        <MultiLevelMenu.DashboardItem />
        <MultiLevelMenu.ResourceItem resource="songs" />
        {/* The empty filter is required to avoid falling back to the previously set filter */}
        <MultiLevelMenu.ResourceItem
            resource="artists"
            to={'/artists?filter={}'}
        >
            <MultiLevelMenu.Item
                name="artists.rock"
                to={'/artists?filter={"type":"Rock"}'}
                label="Rock"
            >
                <MultiLevelMenu.Item
                    name="artists.rock.pop"
                    to={'/artists?filter={"type":"Pop Rock"}'}
                    label="Pop Rock"
                />
                <MultiLevelMenu.Item
                    name="artists.rock.folk"
                    to={'/artists?filter={"type":"Folk Rock"}'}
                    label="Folk Rock"
                />
            </MultiLevelMenu.Item>
            <MultiLevelMenu.Item
                name="artists.jazz"
                to={'/artists?filter={"type":"Jazz"}'}
                label="Jazz"
            >
                <MultiLevelMenu.Item
                    name="artists.jazz.rb"
                    to={'/artists?filter={"type":"RB"}'}
                    label="R&B"
                />
            </MultiLevelMenu.Item>
        </MultiLevelMenu.ResourceItem>
    </MultiLevelMenu>
);

sx: CSS API

Pass an sx prop to customize the style of the main component and the underlying elements.

export const MyMenu = () => (
    <MultiLevelMenu sx={{ marginTop: 0 }}>
        // ...
    </MultiLevelMenu>
);

To override the style of <MultiLevelMenu> using the application-wide style overrides, use the RaMenuRoot key.

Style overrides, powered by MUI System:

Rule name Description
RaMultiLevelMenu Applied to the root div element
& .RaMultiLevelMenu-nav Applied to the nav element
& .RaMultiLevelMenu-navWithCategories Applied to the nav element when using the categories variant
& .RaMultiLevelMenu-list Applied to the MUI <List>

<MultiLevelMenu.Item>

The <MultiLevelMenu.Item> component displays a menu item with a label and an icon.

<MultiLevelMenu.Item
    name="songs"
    to="/songs"
    label="Songs"
    icon={<MusicIcon />}
/>

Usage

import { MultiLevelMenu } from '@react-admin/ra-navigation';
const MyMenu = () => (
    <MultiLevelMenu>
        {/* The empty filter is required to avoid falling back to the previously set filter */}
        <MultiLevelMenu.Item
            name="artists"
            to={'/artists?filter={}'}
            label="artists"
        >
            <MultiLevelMenu.Item
                name="artists.rock"
                to={'/artists?filter={"type":"Rock"}'}
                label="Rock"
            >
                <MultiLevelMenu.Item
                    name="artists.rock.pop"
                    to={'/artists?filter={"type":"Pop Rock"}'}
                    label="Pop Rock"
                />
                <MultiLevelMenu.Item
                    name="artists.rock.folk"
                    to={'/artists?filter={"type":"Folk Rock"}'}
                    label="Folk Rock"
                />
            </MultiLevelMenu.Item>
            <MultiLevelMenu.Item
                name="artists.jazz"
                to={'/artists?filter={"type":"Jazz"}'}
                label="Jazz"
            >
                <MultiLevelMenu.Item
                    name="artists.jazz.rb"
                    to={'/artists?filter={"type":"RB"}'}
                    label="R&B"
                />
            </MultiLevelMenu.Item>
        </MultiLevelMenu.Item>
    </MultiLevelMenu>
);

Props

In addition to the props of react-router <NavLink> and those of material-ui <ListItem>.

Prop Required Type Default Description
children Optional ReactNode   The sub-menu items to be rendered.
name Required string   The name of the item. Used to manage its open/closed state.
icon Optional ReactNode - An icon element to display in front of the item.
label Optional string - The label to display for this item. Accepts translation keys.
to Required string - The location to link to.
sx Optional SxProps - Style overrides, powered by MUI System.

Additional props are passed down to the underling Material UI <listItem> component.

Tip: You can omit the to from <NavLink> property for <MultiLevelMenu.Item> elements that have a child menu item.

children

Pass a children to <MultiLevelMenu.Item> to define the sub-menu entries:

import { MultiLevelMenu } from "@react-admin/ra-navigation";
import PeopleIcon from '@mui/icons-material/People';

const MyMenu = () => (
    <MultiLevelMenu>
        <MultiLevelMenu.Item
            name="artists"
            to={'/artists?filter={}'}
            label="Artists"
            icon={<PeopleIcon />}
        >
            <MultiLevelMenu.Item
                name="artists.rock"
                to={'/artists?filter={"type":"Rock"}'}
                label="Rock"
            >
                <MultiLevelMenu.Item
                    name="artists.rock.pop"
                    to={'/artists?filter={"type":"Pop Rock"}'}
                    label="Pop Rock"
                />
                <MultiLevelMenu.Item
                    name="artists.rock.folk"
                    to={'/artists?filter={"type":"Folk Rock"}'}
                    label="Folk Rock"
                />
            </MultiLevelMenu.Item>
            <MultiLevelMenu.Item
                name="artists.jazz"
                to={'/artists?filter={"type":"Jazz"}'}
                label="Jazz"
            >
                <MultiLevelMenu.Item
                    name="artists.jazz.rb"
                    to={'/artists?filter={"type":"RB"}'}
                    label="R&B"
                />
            </MultiLevelMenu.Item>
        </MultiLevelMenu.Item>
    </MultiLevelMenu>
);

name

The name of the item. Used to manage its open/closed state.

import { MultiLevelMenu } from '@react-admin/ra-navigation';
const MyMenu = () => (
    <MultiLevelMenu>
        <MultiLevelMenu.Item name="artists" to={'/artists?filter={}'} />
    </MultiLevelMenu>
);

icon

An icon element to display in front of the item.

import { MultiLevelMenu } from '@react-admin/ra-navigation';
import PeopleIcon from '@mui/icons-material/People';
const MyMenu = () => (
    <MultiLevelMenu>
        <MultiLevelMenu.Item name="artists" to={'/artists?filter={}'} icon={<PeopleIcon />} />
    </MultiLevelMenu>
);

label

The label to display for this item. Accepts translation keys.

import { MultiLevelMenu } from '@react-admin/ra-navigation';
import PeopleIcon from '@mui/icons-material/People';
const MyMenu = () => (
    <MultiLevelMenu>
        <MultiLevelMenu.Item name="artists" to={'/artists?filter={}'} label="Artists" />
        <MultiLevelMenu.Item name="settings" to={'/artists?filter={}'} label="myapp.menu.settings" />
    </MultiLevelMenu>
);

to

The location to go to for this item. Accepts a path.

import { MultiLevelMenu } from '@react-admin/ra-navigation';

const MyMenu = () => (
    <MultiLevelMenu>
        <MultiLevelMenu.Item name="artists" to="/artists" />
        <MultiLevelMenu.Item name="settings" to="/settings" />
    </MultiLevelMenu>
);

sx

Style overrides, powered by MUI System:

Rule name Description
RaMenuItem Applied to the root div element
& .RaMenuItem-container Applied to the MUI <ListItem> element
& .RaMenuItem-link Applied to the text of the NavLink
& .RaMenuItem-active Applied to the text of the NavLink when active (this item is the current page)
& .RaMenuItem-menuIcon Applied to the MUI <ListItemIcon> element
& .RaMenuItem-icon Applied to the item icon element
& .RaMenuItem-button Applied to the MUI <IconButton> that open/closes the item children list
& .RaMenuItem-nestedList Applied to the MUI <List> element that contains the item children
& .RaMenuItem-hiddenNestedList Applied to the MUI <List> element that contains the item children when collapsed
& .RaMenuItem-itemButton Applied to the MUI <ListItemButton>
import { MultiLevelMenu } from '@react-admin/ra-navigation';
const MyMenu = () => (
    <MultiLevelMenu>
        <MultiLevelMenu.Item name="artists" to={'/artists?filter={}'} sx={{ border: 'solid 1px #000' }} />
    </MultiLevelMenu>
);

<MultiLevelMenu.DashboardItem>

A <MultiLevelMenu.Item> for the dashboard.

Usage

import { MultiLevelMenu } from '@react-admin/ra-navigation';

const MyMenu = () => (
    <MultiLevelMenu>
        <MultiLevelMenu.DashboardItem />
    </MultiLevelMenu>
);

It accepts all props from <MultiLevelMenu.Item>

<MultiLevelMenu.ResourceItem>

A <MultiLevelMenu.Item> for resources. It only needs the resource name and automatically applies access control if your authProvider implements canAccess by calling it with the following parameters:

{
    resource: "posts",
    action: "list",
}

Usage

import { MultiLevelMenu } from '@react-admin/ra-navigation';

const MyMenu = () => (
    <MultiLevelMenu>
        <MultiLevelMenu.ResourceItem resource="artists" />
    </MultiLevelMenu>
);

Props

In addition to the props of <MultiLevelMenu.Item>, it accepts the following props

Prop Required Type Default Description
resource Required string   The resource this item refers to.

Tip: Note that the name prop from <MultiLevelMenu.Item> is optional for <MultiLevelMenu.ResourceItem>.

resource

The resource this item refers to

import { MultiLevelMenu } from '@react-admin/ra-navigation';

const MyMenu = () => (
    <MultiLevelMenu>
        <MultiLevelMenu.ResourceItem resource="artists" />
    </MultiLevelMenu>
);

Creating Menu Items For Resources

If you want to render a custom menu item and the default resource menu items, use the useResourceDefinitions hook to retrieve the list of resources and create one menu item per resource.

// in src/MyMenu.js
import { createElement } from 'react';
import { useResourceDefinitions } from 'react-admin';
import { MultiLevelMenu } from "@react-admin/ra-navigation";
import LabelIcon from '@mui/icons-material/Label';

export const MyMenu = () => {
    const resources = useResourceDefinitions();

    return (
        <MultiLevelMenu>
            {Object.keys(resources).map(name => (
                <MultiLevelMenu.ResourceItem
                    key={name}
                    resource={name}
                    label={resources[name].options && resources[name].options.label || name}
                    icon={createElement(resources[name].icon)}
                />
            ))}
            <MultiLevelMenu.Item name="custom.route" to="/custom-route" label="Miscellaneous" icon={<LabelIcon />} />
        </MultiLevelMenu>
    );
};