메뉴

메뉴 (Menu)

좋은 내비게이션 구성으로 사용자가 사이트를 빠르고 효율적으로 이동하게 해 주는 컴포넌트예요. 상단과 사이드 두 가지 내비게이션 옵션을 제공해요.

출처: 문서

본문

언제 사용하나요 (When To Use)

내비게이션은 모든 웹사이트에서 중요한 부분이에요. 좋은 내비게이션 구성은 사용자가 사이트를 빠르고 효율적으로 이동하게 해 주거든요. Ant Design은 상단과 사이드 두 가지 내비게이션 옵션을 제공해요. 상단 내비게이션은 웹사이트의 모든 카테고리와 기능을 제공하고, 사이드 내비게이션은 웹사이트의 다단계 구조를 제공해요.

내비게이션이 있는 더 많은 레이아웃: Layout.

개발자 참고 (Notes for developers)

  • Menu는 ul 요소로 렌더링되므로 자식 노드로 li와 script-supporting 요소만 지원해요. 커스터마이즈된 노드는 Menu.Item으로 감싸야 해요.
  • Menu는 노드 구조를 수집해야 하므로 자식은 Menu.* 또는 이를 캡슐화한 HOC여야 해요.

예시 (Examples)

상단 내비게이션 (Top Navigation)

가로 상단 내비게이션 메뉴예요.

import React, { useState } from 'react';
import { AppstoreOutlined, MailOutlined, SettingOutlined } from '@ant-design/icons';
import type { MenuProps } from 'antd';
import { Menu } from 'antd';

type MenuItem = Required<MenuProps>['items'][number];

const items: MenuItem[] = [
  {
    label: 'Navigation One',
    key: 'mail',
    icon: <MailOutlined />,
  },
  {
    label: 'Navigation Two',
    key: 'app',
    icon: <AppstoreOutlined />,
    disabled: true,
  },
  {
    label: 'Navigation Three - Submenu',
    key: 'SubMenu',
    icon: <SettingOutlined />,
    children: [
      {
        type: 'group',
        label: 'Item 1',
        children: [
          { label: 'Option 1', key: 'setting:1' },
          { label: 'Option 2', key: 'setting:2' },
        ],
      },
      {
        type: 'group',
        label: 'Item 2',
        children: [
          { label: 'Option 3', key: 'setting:3' },
          { label: 'Option 4', key: 'setting:4' },
        ],
      },
    ],
  },
  {
    key: 'alipay',
    label: (
      <a href="https://ant.design" target="_blank" rel="noopener noreferrer">
        Navigation Four - Link
      </a>
    ),
  },
];

const App: React.FC = () => {
  const [current, setCurrent] = useState('mail');

  const onClick: MenuProps['onClick'] = (e) => {
    console.log('click ', e);
    setCurrent(e.key);
  };

  return <Menu onClick={onClick} selectedKeys={[current]} mode="horizontal" items={items} />;
};

export default App;

인라인 메뉴 (Inline menu)

인라인 서브메뉴가 있는 세로 메뉴예요.

import React from 'react';
import { AppstoreOutlined, MailOutlined, SettingOutlined } from '@ant-design/icons';
import type { MenuProps } from 'antd';
import { Menu } from 'antd';

type MenuItem = Required<MenuProps>['items'][number];

const items: MenuItem[] = [
  {
    key: 'sub1',
    label: 'Navigation One',
    icon: <MailOutlined />,
    children: [
      {
        key: 'g1',
        label: 'Item 1',
        type: 'group',
        children: [
          { key: '1', label: 'Option 1' },
          { key: '2', label: 'Option 2' },
        ],
      },
      {
        key: 'g2',
        label: 'Item 2',
        type: 'group',
        children: [
          { key: '3', label: 'Option 3' },
          { key: '4', label: 'Option 4' },
        ],
      },
    ],
  },
  {
    key: 'sub2',
    label: 'Navigation Two',
    icon: <AppstoreOutlined />,
    children: [
      { key: '5', label: 'Option 5' },
      { key: '6', label: 'Option 6' },
      {
        key: 'sub3',
        label: 'Submenu',
        children: [
          { key: '7', label: 'Option 7' },
          { key: '8', label: 'Option 8' },
        ],
      },
    ],
  },
  {
    type: 'divider',
  },
  {
    key: 'sub4',
    label: 'Navigation Three',
    icon: <SettingOutlined />,
    children: [
      { key: '9', label: 'Option 9' },
      { key: '10', label: 'Option 10' },
      { key: '11', label: 'Option 11' },
      { key: '12', label: 'Option 12' },
    ],
  },
  {
    key: 'grp',
    label: 'Group',
    type: 'group',
    children: [
      { key: '13', label: 'Option 13' },
      { key: '14', label: 'Option 14' },
    ],
  },
];

const App: React.FC = () => {
  const onClick: MenuProps['onClick'] = (e) => {
    console.log('click ', e);
  };

  return (
    <Menu
      onClick={onClick}
      style={{ width: 256 }}
      defaultSelectedKeys={['1']}
      defaultOpenKeys={['sub1']}
      mode="inline"
      items={items}
    />
  );
};

export default App;

접히는 인라인 메뉴 (Collapsed inline menu)

인라인 메뉴는 접을 수 있어요.

여기 sider 레이아웃이 있는 완전한 데모가 있어요.

import React, { useState } from 'react';
import {
  AppstoreOutlined,
  ContainerOutlined,
  DesktopOutlined,
  MailOutlined,
  MenuFoldOutlined,
  MenuUnfoldOutlined,
  PieChartOutlined,
} from '@ant-design/icons';
import type { MenuProps } from 'antd';
import { Button, Menu } from 'antd';

type MenuItem = Required<MenuProps>['items'][number];

const items: MenuItem[] = [
  { key: '1', icon: <PieChartOutlined />, label: 'Option 1' },
  { key: '2', icon: <DesktopOutlined />, label: 'Option 2' },
  { key: '3', icon: <ContainerOutlined />, label: 'Option 3' },
  {
    key: 'sub1',
    label: 'Navigation One',
    icon: <MailOutlined />,
    children: [
      { key: '5', label: 'Option 5' },
      { key: '6', label: 'Option 6' },
      { key: '7', label: 'Option 7' },
      { key: '8', label: 'Option 8' },
    ],
  },
  {
    key: 'sub2',
    label: 'Navigation Two',
    icon: <AppstoreOutlined />,
    children: [
      { key: '9', label: 'Option 9' },
      { key: '10', label: 'Option 10' },
      {
        key: 'sub3',
        label: 'Submenu',
        children: [
          { key: '11', label: 'Option 11' },
          { key: '12', label: 'Option 12' },
        ],
      },
    ],
  },
];

const App: React.FC = () => {
  const [collapsed, setCollapsed] = useState(false);

  const toggleCollapsed = () => {
    setCollapsed(!collapsed);
  };

  return (
    <div style={{ width: 256 }}>
      <Button type="primary" onClick={toggleCollapsed} style={{ marginBottom: 16 }}>
        {collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
      </Button>
      <Menu
        defaultSelectedKeys={['1']}
        defaultOpenKeys={['sub1']}
        mode="inline"
        theme="dark"
        inlineCollapsed={collapsed}
        items={items}
      />
    </div>
  );
};

export default App;

메뉴 툴팁 (Menu tooltip)

인라인 접힘 모드에서 tooltip을 설정하거나 비활성화해요.

import React, { useState } from 'react';
import {
  AppstoreOutlined,
  ContainerOutlined,
  DesktopOutlined,
  MailOutlined,
  MenuFoldOutlined,
  MenuUnfoldOutlined,
  PieChartOutlined,
} from '@ant-design/icons';
import type { MenuProps } from 'antd';
import { Button, Menu, Space, Switch } from 'antd';

type MenuItem = Required<MenuProps>['items'][number];

const items: MenuItem[] = [
  { key: '1', icon: <PieChartOutlined />, label: 'Option 1' },
  { key: '2', icon: <DesktopOutlined />, label: 'Option 2' },
  { key: '3', icon: <ContainerOutlined />, label: 'Option 3' },
  {
    key: 'sub1',
    label: 'Navigation One',
    icon: <MailOutlined />,
    children: [
      { key: '5', label: 'Option 5' },
      { key: '6', label: 'Option 6' },
      { key: '7', label: 'Option 7' },
      { key: '8', label: 'Option 8' },
    ],
  },
  {
    key: 'sub2',
    label: 'Navigation Two',
    icon: <AppstoreOutlined />,
    children: [
      { key: '9', label: 'Option 9' },
      { key: '10', label: 'Option 10' },
      {
        key: 'sub3',
        label: 'Submenu',
        children: [
          { key: '11', label: 'Option 11' },
          { key: '12', label: 'Option 12' },
        ],
      },
    ],
  },
];

const App: React.FC = () => {
  const [collapsed, setCollapsed] = useState(false);
  const [tooltipEnabled, setTooltipEnabled] = useState(true);

  return (
    <div style={{ width: 256 }}>
      <Space style={{ marginBottom: 16 }}>
        <Button
          type="primary"
          onClick={() => setCollapsed((prev) => !prev)}
          icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
        />
        <Switch
          checked={tooltipEnabled}
          onChange={setTooltipEnabled}
          checkedChildren="Tooltip On"
          unCheckedChildren="Tooltip Off"
        />
      </Space>
      <Menu
        defaultSelectedKeys={['1']}
        defaultOpenKeys={['sub1']}
        mode="inline"
        theme="dark"
        inlineCollapsed={collapsed}
        tooltip={tooltipEnabled ? { placement: 'left' } : false}
        items={items}
      />
    </div>
  );
};

export default App;

현재 하위 메뉴만 열기 (Open current submenu only)

메뉴를 클릭하면 다른 모든 메뉴가 접혀 전체 메뉴가 컴팩트하게 유지되는 걸 볼 수 있어요.

import React, { useState } from 'react';
import { AppstoreOutlined, MailOutlined, SettingOutlined } from '@ant-design/icons';
import type { MenuProps } from 'antd';
import { Menu } from 'antd';

type MenuItem = Required<MenuProps>['items'][number];

const items: MenuItem[] = [
  {
    key: '1',
    icon: <MailOutlined />,
    label: 'Navigation One',
    children: [
      { key: '11', label: 'Option 1' },
      { key: '12', label: 'Option 2' },
      { key: '13', label: 'Option 3' },
      { key: '14', label: 'Option 4' },
    ],
  },
  {
    key: '2',
    icon: <AppstoreOutlined />,
    label: 'Navigation Two',
    children: [
      { key: '21', label: 'Option 1' },
      { key: '22', label: 'Option 2' },
      {
        key: '23',
        label: 'Submenu',
        children: [
          { key: '231', label: 'Option 1' },
          { key: '232', label: 'Option 2' },
          { key: '233', label: 'Option 3' },
        ],
      },
      {
        key: '24',
        label: 'Submenu 2',
        children: [
          { key: '241', label: 'Option 1' },
          { key: '242', label: 'Option 2' },
          { key: '243', label: 'Option 3' },
        ],
      },
    ],
  },
  {
    key: '3',
    icon: <SettingOutlined />,
    label: 'Navigation Three',
    children: [
      { key: '31', label: 'Option 1' },
      { key: '32', label: 'Option 2' },
      { key: '33', label: 'Option 3' },
      { key: '34', label: 'Option 4' },
    ],
  },
];

interface LevelKeysProps {
  key?: string;
  children?: LevelKeysProps[];
}

const getLevelKeys = (items1: LevelKeysProps[]) => {
  const key: Record<string, number> = {};
  const func = (items2: LevelKeysProps[], level = 1) => {
    items2.forEach((item) => {
      if (item.key) {
        key[item.key] = level;
      }
      if (item.children) {
        func(item.children, level + 1);
      }
    });
  };
  func(items1);
  return key;
};

const levelKeys = getLevelKeys(items as LevelKeysProps[]);

const App: React.FC = () => {
  const [stateOpenKeys, setStateOpenKeys] = useState(['2', '23']);

  const onOpenChange: MenuProps['onOpenChange'] = (openKeys) => {
    const currentOpenKey = openKeys.find((key) => !stateOpenKeys.includes(key));
    // open
    if (currentOpenKey !== undefined) {
      const repeatIndex = openKeys
        .filter((key) => key !== currentOpenKey)
        .findIndex((key) => levelKeys[key] === levelKeys[currentOpenKey]);

      setStateOpenKeys(
        openKeys
          // remove repeat key
          .filter((_, index) => index !== repeatIndex)
          // remove current level all child
          .filter((key) => levelKeys[key] <= levelKeys[currentOpenKey]),
      );
    } else {
      // close
      setStateOpenKeys(openKeys);
    }
  };

  return (
    <Menu
      mode="inline"
      defaultSelectedKeys={['231']}
      openKeys={stateOpenKeys}
      onOpenChange={onOpenChange}
      style={{ width: 256 }}
      items={items}
    />
  );
};

export default App;

세로 메뉴 (Vertical menu)

서브메뉴가 팝업으로 열려요.

import React from 'react';
import { AppstoreOutlined, MailOutlined, SettingOutlined } from '@ant-design/icons';
import type { MenuProps } from 'antd';
import { Menu } from 'antd';

type MenuItem = Required<MenuProps>['items'][number];

const items: MenuItem[] = [
  {
    key: 'sub1',
    icon: <MailOutlined />,
    label: 'Navigation One',
    children: [
      {
        key: '1-1',
        label: 'Item 1',
        type: 'group',
        children: [
          { key: '1', label: 'Option 1' },
          { key: '2', label: 'Option 2' },
        ],
      },
      {
        key: '1-2',
        label: 'Item 2',
        type: 'group',
        children: [
          { key: '3', label: 'Option 3' },
          { key: '4', label: 'Option 4' },
        ],
      },
    ],
  },
  {
    key: 'sub2',
    icon: <AppstoreOutlined />,
    label: 'Navigation Two',
    children: [
      { key: '5', label: 'Option 5' },
      { key: '6', label: 'Option 6' },
      {
        key: 'sub3',
        label: 'Submenu',
        children: [
          { key: '7', label: 'Option 7' },
          { key: '8', label: 'Option 8' },
        ],
      },
    ],
  },
  {
    key: 'sub4',
    label: 'Navigation Three',
    icon: <SettingOutlined />,
    children: [
      { key: '9', label: 'Option 9' },
      { key: '10', label: 'Option 10' },
      { key: '11', label: 'Option 11' },
      { key: '12', label: 'Option 12' },
    ],
  },
];

const onClick: MenuProps['onClick'] = (e) => {
  console.log('click', e);
};

const App: React.FC = () => (
  <Menu onClick={onClick} style={{ width: 256 }} mode="vertical" items={items} />
);

export default App;

메뉴 테마 (Menu Themes)

light와 dark 두 가지 내장 테마가 있어요. 기본값은 light예요.

import React, { useState } from 'react';
import { AppstoreOutlined, MailOutlined, SettingOutlined } from '@ant-design/icons';
import type { MenuProps, MenuTheme } from 'antd';
import { Menu, Switch } from 'antd';

type MenuItem = Required<MenuProps>['items'][number];

const items: MenuItem[] = [
  {
    key: 'sub1',
    label: 'Navigation One',
    icon: <MailOutlined />,
    children: [
      { key: '1', label: 'Option 1' },
      { key: '2', label: 'Option 2' },
      { key: '3', label: 'Option 3' },
      { key: '4', label: 'Option 4' },
    ],
  },
  {
    key: 'sub2',
    label: 'Navigation Two',
    icon: <AppstoreOutlined />,
    children: [
      { key: '5', label: 'Option 5' },
      { key: '6', label: 'Option 6' },
      {
        key: 'sub3',
        label: 'Submenu',
        children: [
          { key: '7', label: 'Option 7' },
          { key: '8', label: 'Option 8' },
        ],
      },
    ],
  },
  {
    key: 'sub4',
    label: 'Navigation Three',
    icon: <SettingOutlined />,
    children: [
      { key: '9', label: 'Option 9' },
      { key: '10', label: 'Option 10' },
      { key: '11', label: 'Option 11' },
      { key: '12', label: 'Option 12' },
    ],
  },
];

const App: React.FC = () => {
  const [theme, setTheme] = useState<MenuTheme>('dark');
  const [current, setCurrent] = useState('1');

  const changeTheme = (value: boolean) => {
    setTheme(value ? 'dark' : 'light');
  };

  const onClick: MenuProps['onClick'] = (e) => {
    console.log('click ', e);
    setCurrent(e.key);
  };

  return (
    <>
      <Switch
        checked={theme === 'dark'}
        onChange={changeTheme}
        checkedChildren="Dark"
        unCheckedChildren="Light"
      />
      <br />
      <br />
      <Menu
        theme={theme}
        onClick={onClick}
        style={{ width: 256 }}
        defaultOpenKeys={['sub1']}
        selectedKeys={[current]}
        mode="inline"
        items={items}
      />
    </>
  );
};

export default App;

하위 메뉴 테마 (Sub-menu theme)

theme prop으로 SubMenu 테마를 설정해 서로 다른 테마 색 효과를 낼 수 있어요. 이 예시는 루트는 dark, SubMenu는 light예요.

import React, { useState } from 'react';
import { MailOutlined } from '@ant-design/icons';
import type { MenuProps, MenuTheme } from 'antd';
import { Menu, Switch } from 'antd';

type MenuItem = Required<MenuProps>['items'][number];

const App: React.FC = () => {
  const [menuTheme, setMenuTheme] = useState<MenuTheme>('light');
  const [current, setCurrent] = useState('1');

  const changeTheme = (value: boolean) => {
    setMenuTheme(value ? 'dark' : 'light');
  };

  const onClick: MenuProps['onClick'] = (e) => {
    setCurrent(e.key);
  };

  const items: MenuItem[] = [
    {
      key: 'sub1',
      icon: <MailOutlined />,
      label: 'Navigation One',
      theme: menuTheme,
      children: [
        { key: '1', label: 'Option 1' },
        { key: '2', label: 'Option 2' },
        { key: '3', label: 'Option 3' },
      ],
    },
    { key: '5', label: 'Option 5' },
    { key: '6', label: 'Option 6' },
  ];

  return (
    <>
      <Switch
        checked={menuTheme === 'dark'}
        onChange={changeTheme}
        checkedChildren="Dark"
        unCheckedChildren="Light"
      />
      <br />
      <br />
      <Menu
        onClick={onClick}
        style={{ width: 256 }}
        openKeys={['sub1']}
        selectedKeys={[current]}
        mode="vertical"
        theme="dark"
        items={items}
        getPopupContainer={(node) => node.parentNode as HTMLElement}
      />
    </>
  );
};

export default App;

메뉴 타입 전환 (Switch the menu type)

동적 전환 모드(inline과 vertical 사이)를 보여 줘요.

import React, { useState } from 'react';
import {
  AppstoreOutlined,
  CalendarOutlined,
  LinkOutlined,
  MailOutlined,
  SettingOutlined,
} from '@ant-design/icons';
import { Divider, Menu, Switch } from 'antd';
import type { GetProp, MenuProps } from 'antd';

type MenuTheme = GetProp<MenuProps, 'theme'>;

type MenuItem = GetProp<MenuProps, 'items'>[number];

const items: MenuItem[] = [
  {
    key: '1',
    icon: <MailOutlined />,
    label: 'Navigation One',
  },
  {
    key: '2',
    icon: <CalendarOutlined />,
    label: 'Navigation Two',
  },
  {
    key: 'sub1',
    label: 'Navigation Two',
    icon: <AppstoreOutlined />,
    children: [
      { key: '3', label: 'Option 3' },
      { key: '4', label: 'Option 4' },
      {
        key: 'sub1-2',
        label: 'Submenu',
        children: [
          { key: '5', label: 'Option 5' },
          { key: '6', label: 'Option 6' },
        ],
      },
    ],
  },
  {
    key: 'sub2',
    label: 'Navigation Three',
    icon: <SettingOutlined />,
    children: [
      { key: '7', label: 'Option 7' },
      { key: '8', label: 'Option 8' },
      { key: '9', label: 'Option 9' },
      { key: '10', label: 'Option 10' },
    ],
  },
  {
    key: 'link',
    icon: <LinkOutlined />,
    label: (
      <a href="https://ant.design" target="_blank" rel="noopener noreferrer">
        Ant Design
      </a>
    ),
  },
];

const App: React.FC = () => {
  const [mode, setMode] = useState<'vertical' | 'inline'>('inline');
  const [theme, setTheme] = useState<MenuTheme>('light');

  const changeMode = (value: boolean) => {
    setMode(value ? 'vertical' : 'inline');
  };

  const changeTheme = (value: boolean) => {
    setTheme(value ? 'dark' : 'light');
  };

  return (
    <>
      <Switch onChange={changeMode} /> Change Mode
      <Divider vertical />
      <Switch onChange={changeTheme} /> Change Style
      <br />
      <br />
      <Menu
        style={{ width: 256 }}
        defaultSelectedKeys={['1']}
        defaultOpenKeys={['sub1']}
        mode={mode}
        theme={theme}
        items={items}
      />
    </>
  );
};

export default App;

시맨틱 DOM 스타일링 (Custom semantic dom styling)

classNames와 styles로 객체나 함수를 전달해 Menu의 시맨틱 DOM 스타일을 커스터마이즈할 수 있어요.

import React from 'react';
import { Flex, Menu } from 'antd';
import type { GetProp, MenuProps } from 'antd';
import { createStaticStyles } from 'antd-style';

const classNames = createStaticStyles(({ css }) => ({
  root: css`
    border: 1px solid #f0f0f0;
    max-width: 600px;
    padding: 8px;
    border-radius: 4px;
  `,
  item: css`
    color: #1677ff;
  `,
}));

const items: Required<MenuProps>['items'] = [
  {
    key: 'SubMenu',
    label: 'Navigation One',
    children: [
      {
        key: 'g1',
        label: 'Item 1',
        type: 'group',
        children: [
          { key: '1', label: 'Option 1' },
          { key: '2', label: 'Option 2' },
        ],
      },
    ],
  },
  { key: 'mail', label: 'Navigation Two' },
];

const styles: MenuProps['styles'] = {
  root: { border: '1px solid #f0f0f0', padding: 8, borderRadius: 4 },
  item: { color: '#1677ff' },
  subMenu: { list: { color: '#fa541c' } },
};

const stylesFn: MenuProps['styles'] = (info): GetProp<MenuProps, 'styles', 'Return'> => {
  const hasSub = info.props.items?.[0];
  return {
    root: {
      backgroundColor: hasSub ? 'rgba(240,249,255, 0.6)' : 'rgba(255,255,255)',
    },
  };
};

const App: React.FC = () => {
  const shareProps: MenuProps = {
    classNames,
    items,
  };

  return (
    <Flex vertical gap="medium">
      <Menu {...shareProps} styles={styles} />
      <Menu mode="inline" {...shareProps} styles={stylesFn} />
    </Flex>
  );
};

export default App;

커스텀 하위 메뉴 렌더 (Custom Submenu Render)

popupRender prop으로 하위 메뉴 팝업 렌더링을 커스터마이즈해요.

import React from 'react';
import type { MenuProps } from 'antd';
import { Col, ConfigProvider, Flex, Menu, Row, Space, Typography } from 'antd';
import { createStyles } from 'antd-style';

const { Title, Paragraph } = Typography;

const useStyles = createStyles(({ token }) => ({
  navigationPopup: {
    padding: token.padding,
    minWidth: 480,
    background: token.colorBgElevated,
    borderRadius: token.borderRadiusLG,
    boxShadow: token.boxShadowSecondary,
  },
  menuItem: {
    borderRadius: token.borderRadius,
    transition: `all ${token.motionDurationSlow}`,
    cursor: 'pointer',
    '&:hover': {
      background: 'rgba(0, 0, 0, 0.02)',
    },
  },
  menuItemSpace: {
    padding: token.paddingSM,
  },
  leadingHeader: {
    margin: '0 !important',
    paddingBottom: token.paddingXS,
    borderBottom: `${token.lineWidth}px ${token.lineType} ${token.colorSplit}`,
  },
  marginLess: {
    margin: '0 !important',
  },
}));

const MenuItem = ({ title, description }: { title: string; description: string }) => {
  const { styles } = useStyles();
  return (
    <div className={styles.menuItem}>
      <Space vertical size={4} className={styles.menuItemSpace}>
        <Title level={5} className={styles.marginLess}>
          {title}
        </Title>
        <Paragraph type="secondary" className={styles.marginLess}>
          {description}
        </Paragraph>
      </Space>
    </div>
  );
};

const menuItems = [
  {
    key: 'home',
    label: 'Home',
  },
  {
    key: 'features',
    label: 'Features',
    children: [
      {
        key: 'getting-started',
        label: (
          <MenuItem title="Getting Started" description="Quick start guide and learn the basics." />
        ),
      },
      {
        key: 'components',
        label: <MenuItem title="Components" description="Explore our component library." />,
      },
      {
        key: 'templates',
        label: <MenuItem title="Templates" description="Ready-to-use template designs." />,
      },
    ],
  },
  {
    key: 'resources',
    label: 'Resources',
    children: [
      {
        key: 'blog',
        label: <MenuItem title="Blog" description="Latest updates and articles." />,
      },
      {
        key: 'community',
        label: <MenuItem title="Community" description="Join our developer community." />,
      },
    ],
  },
];

const App: React.FC = () => {
  const { styles } = useStyles();
  const popupRender: MenuProps['popupRender'] = (_, { item }) => {
    return (
      <Flex className={styles.navigationPopup} vertical gap="medium">
        <Typography.Title level={3} className={styles.leadingHeader}>
          {item.title}
        </Typography.Title>
        <Row gutter={16}>
          {React.Children.map(item.children as React.ReactNode, (child) => {
            if (!React.isValidElement(child)) {
              return null;
            }
            return (
              <Col span={12} key={child.key}>
                {child}
              </Col>
            );
          })}
        </Row>
      </Flex>
    );
  };

  return (
    <ConfigProvider
      theme={{
        components: {
          Menu: {
            popupBg: '#fff',
            horizontalItemSelectedColor: '#1677ff',
            horizontalItemHoverColor: '#1677ff',
          },
          Typography: {
            titleMarginBottom: 0,
            titleMarginTop: 0,
          },
        },
      }}
    >
      <Menu mode="horizontal" items={menuItems} popupRender={popupRender} />
    </ConfigProvider>
  );
};

export default App;

API

공통 props는 Common props를 참고해요.

속성 (Property) 설명 (Description) 타입 (Type) 기본값 (Default) 버전 (Version) 글로벌 설정
classNames 컴포넌트 내부의 각 시맨틱 구조에 대한 class를 지정해요. 객체 또는 함수를 지원해요. Record<SemanticDOM, string> | (info: { props }) => Record<SemanticDOM, string> - 6.0.0
defaultOpenKeys 기본으로 열린 하위 메뉴 키 배열 string[] - ×
defaultSelectedKeys 기본 선택된 메뉴 항목 키 배열 string[] - ×
expandIcon 하위 메뉴의 커스텀 확장 아이콘 ReactNode | (props: SubMenuProps & { isSubMenu: boolean }) => ReactNode - 4.9.0 5.15.0
forceSubMenuRender 하위 메뉴를 보이기 전에 DOM으로 렌더링해요. boolean false ×
inlineCollapsed 메뉴가 인라인 모드일 때 접힘 상태를 지정해요. boolean - ×
inlineIndent 각 단계의 인라인 메뉴 항목 들여쓰기(픽셀) number 24 ×
items 메뉴 항목 콘텐츠 ItemType[] - 4.20.0 ×
mode 메뉴 타입 vertical | horizontal | inline vertical ×
multiple 여러 항목 선택 허용 boolean false ×
openKeys 현재 열린 하위 메뉴 키 배열 string[] - ×
overflowedIndicator 메뉴가 가로로 접히면 생략 부호 아이콘 커스터마이즈 ReactNode <EllipsisOutlined /> ×
selectable 메뉴 항목 선택 허용 boolean true ×
selectedKeys 현재 선택된 메뉴 항목 키 배열 string[] - ×
styles 컴포넌트 내부의 각 시맨틱 구조에 대한 인라인 스타일을 지정해요. 객체 또는 함수를 지원해요. Record<SemanticDOM, CSSProperties> | (info: { props }) => Record<SemanticDOM, CSSProperties> - 6.0.0
subMenuCloseDelay 마우스가 떠날 때 하위 메뉴를 숨기는 지연 시간(초) number 0.1 ×
subMenuOpenDelay 마우스가 들어올 때 하위 메뉴를 보여 주는 지연 시간(초) number 0 ×
tooltip 인라인 접힘 모드의 메뉴 항목에 대한 tooltip props 설정. false로 비활성화. false | TooltipProps - 6.3.0 ×
theme 메뉴의 색 테마 light | dark light ×
triggerSubMenuAction 하위 메뉴 열기/닫기를 트리거하는 동작 hover | click hover ×
onClick 메뉴 항목이 클릭될 때 호출 function({ key, keyPath, domEvent, itemData }) - ×
onDeselect 메뉴 항목이 선택 해제될 때 호출 (multiple 모드만) function({ key, keyPath, selectedKeys, domEvent, itemData }) - ×
onOpenChange 하위 메뉴가 열리거나 닫힐 때 호출 function(openKeys: string[]) - ×
onSelect 메뉴 항목이 선택될 때 호출 function({ key, keyPath, selectedKeys, domEvent, itemData }) - ×
popupRender 하위 메뉴의 커스텀 팝업 렌더러 (node: ReactElement, props: { item: SubMenuProps; keys: string[] }) => ReactNode - ×

더 많은 옵션은 @rc-component/menu 참고.

ItemType

type ItemType = MenuItemType | SubMenuType | MenuItemGroupType | MenuDividerType;

MenuItemType

속성 (Property) 설명 (Description) 타입 (Type) 기본값 (Default) 버전 (Version)
danger 위험 스타일 표시 boolean false
disabled 메뉴 항목 비활성화 여부 boolean false
extra 메뉴 항목의 추가 요소 ReactNode - 5.21.0
icon 메뉴 항목의 아이콘 ReactNode -
key 메뉴 항목의 고유 ID string -
label 메뉴 라벨 ReactNode -
title 접힌 항목의 표시 title 설정 string -

SubMenuType

속성 (Property) 설명 (Description) 타입 (Type) 기본값 (Default) 버전 (Version)
children 하위 메뉴 또는 하위 메뉴 항목 ItemType[] -
disabled 하위 메뉴 비활성화 여부 boolean false
icon 하위 메뉴의 아이콘 ReactNode -
key 하위 메뉴의 고유 ID string -
label 메뉴 라벨 ReactNode -
popupClassName 하위 메뉴 class 이름. mode="inline"에서는 동작하지 않아요. string -
popupOffset 하위 메뉴 오프셋. mode="inline"에서는 동작하지 않아요. [number, number] -
theme SubMenu의 색 테마 (기본적으로 Menu에서 상속) light | dark -
onTitleClick 하위 메뉴 제목이 클릭될 때 실행되는 콜백 function({ key, domEvent }) -
popupRender 현재 하위 메뉴의 커스텀 팝업 렌더러 (node: ReactElement, props: { item: SubMenuProps; keys: string[] }) => ReactNode -

MenuItemGroupType

type을 group으로 정의해 그룹으로 만들어요.

const groupItem = {
  type: 'group', // Must have
  label: 'My Group',
  children: [],
};
속성 (Property) 설명 (Description) 타입 (Type) 기본값 (Default) 버전 (Version)
children 하위 메뉴 항목 MenuItemType[] -
label 그룹의 제목 ReactNode -

MenuDividerType

메뉴 항목 사이의 구분선이에요. 세로 팝업 Menu나 Dropdown Menu에서만 사용해요. type을 divider로 정의해야 해요.

const dividerItem = {
  type: 'divider', // Must have
};
속성 (Property) 설명 (Description) 타입 (Type) 기본값 (Default) 버전 (Version)
dashed 선이 점선인지 여부 boolean false

FAQ

왜 Menu의 children이 두 번 렌더링되나요? {#faq-render-twice}

Menu는 twice-render로 구조 정보를 수집해 HOC 사용을 지원해요. 단일 렌더링으로 합치면 로직이 훨씬 복잡해질 수 있어요. 수집 로직 개선에 기여하는 것을 환영해요.

왜 Menu가 Flex 레이아웃에서 반응형 접힘을 하지 않나요? {#faq-flex-layout}

Menu는 flex 레이아웃에서 항목을 완전히 렌더링한 다음 접어요. 반응형을 활성화하려면 flex가 Menu 너비를 고려하지 않도록 해야 해요. (온라인 데모)

<div style={{ flex }}>
  <div style={{ ... }}>Some Content</div>
  <Menu style={{ minWidth: 0, flex: "auto" }} />
</div>

시맨틱 DOM (Semantic DOM)

시맨틱 DOM 구조는 https://ant.design/components/menu/semantic.md 에서 확인할 수 있어요.

디자인 토큰 (Design Token)

컴포넌트 토큰 (Menu) (Component Token)

토큰 이름 (Token Name) 설명 (Description) 타입 (Type) 기본값 (Default Value)
activeBarBorderWidth 메뉴 항목 활성 바의 테두리 너비 string | number 1
activeBarHeight 메뉴 항목 활성 바의 높이 number 2
activeBarWidth 메뉴 항목 활성 바의 너비 string | number 0
collapsedIconSize 접힘 시 아이콘 크기 number 16
collapsedWidth 접힘 시 너비 string | number 80
dangerItemActiveBg 활성 위험 메뉴 항목의 배경색 string #fff2f0
dangerItemColor 위험 메뉴 항목 텍스트의 색 string #ff4d4f
dangerItemHoverColor 위험 메뉴 항목 텍스트의 hover 색 string #ff4d4f
dangerItemSelectedBg 선택된 위험 메뉴 항목의 배경색 string #fff2f0
dangerItemSelectedColor 선택된 위험 메뉴 항목 텍스트의 색 string #ff4d4f
darkDangerItemActiveBg 다크 모드에서 활성 위험 메뉴 항목의 배경 string #ff4d4f
darkDangerItemColor 다크 모드에서 위험 메뉴 항목 텍스트의 색 string #ff4d4f
darkDangerItemHoverColor 다크 모드에서 hover된 위험 메뉴 항목의 배경 string #ff7875
darkDangerItemSelectedBg 다크 모드에서 활성 위험 메뉴 항목의 배경 string #ff4d4f
darkDangerItemSelectedColor 다크 모드에서 선택된 위험 메뉴 항목의 색 string #fff
darkGroupTitleColor 다크 모드에서 그룹 제목 텍스트의 색 string rgba(255,255,255,0.65)
darkItemBg 다크 모드에서 메뉴 항목의 배경 string #001529
darkItemColor 다크 모드에서 메뉴 항목 텍스트의 색 string rgba(255,255,255,0.65)
darkItemDisabledColor 다크 모드에서 비활성 메뉴 항목의 색 string rgba(255,255,255,0.25)
darkItemHoverBg 다크 모드에서 hover된 메뉴 항목의 배경 string transparent
darkItemHoverColor 다크 모드에서 hover된 메뉴 항목의 색 string #fff
darkItemSelectedBg 다크 모드에서 활성 메뉴 항목의 배경 string #1677ff
darkItemSelectedColor 다크 모드에서 선택된 메뉴 항목의 색 string #fff
darkPopupBg 다크 모드에서 오버레이 메뉴의 배경색 string #001529
darkSubMenuItemBg 다크 모드에서 하위 메뉴 항목의 배경 string #000c17
dropdownWidth 팝업 메뉴의 너비 string | number 160
groupTitleColor 그룹 제목 텍스트의 색 string rgba(0,0,0,0.45)
groupTitleFontSize 그룹 제목의 글자 크기 number 14
groupTitleLineHeight 그룹 제목의 줄 높이 string | number 1.5714285714285714
horizontalItemBorderRadius 가로 메뉴 항목의 테두리 반경 number 0
horizontalItemHoverBg hover 시 가로 메뉴 항목의 배경색 string transparent
horizontalItemHoverColor 가로 메뉴 항목 텍스트의 hover 색 string #1677ff
horizontalItemSelectedBg 선택 시 가로 메뉴 항목의 배경색 string transparent
horizontalItemSelectedColor 선택된 가로 메뉴 항목 텍스트의 색 string #1677ff
horizontalLineHeight 가로 메뉴 항목의 LineHeight LineHeight<string | number> | undefined 46px
iconMarginInlineEnd 아이콘과 텍스트 사이 간격 MarginInlineEnd<string | number> | undefined 10
iconSize 아이콘 크기 number 14
itemActiveBg 활성 메뉴 항목의 배경색 string #e6f4ff
itemBg 메뉴 항목 배경 string #ffffff
itemBorderRadius 메뉴 항목의 반경 number 8
itemColor 메뉴 항목 텍스트의 색 string rgba(0,0,0,0.88)
itemDisabledColor 비활성 메뉴 항목 텍스트의 색 string rgba(0,0,0,0.25)
itemHeight 메뉴 항목의 높이 string | number 40
itemHoverBg hover 시 메뉴 항목의 배경색 string rgba(0,0,0,0.06)
itemHoverColor 메뉴 항목 텍스트의 hover 색 string rgba(0,0,0,0.88)
itemMarginBlock 메뉴 항목의 margin-block MarginBlock<string | number> | undefined 4
itemMarginInline 메뉴 항목의 가로 여백 number 4
itemPaddingInline 메뉴 항목의 padding-inline PaddingInline<string | number> | undefined 16
itemSelectedBg 선택 시 메뉴 항목의 배경색 string #e6f4ff
itemSelectedColor 선택된 메뉴 항목 텍스트의 색 string #1677ff
popupBg 팝업의 배경색 string #ffffff
subMenuItemBg 하위 메뉴 항목의 배경색 string rgba(0,0,0,0.02)
subMenuItemBorderRadius 하위 메뉴 항목의 반경 number 4
subMenuItemSelectedColor 하위 메뉴에 선택된 항목이 있을 때 하위 메뉴 제목의 색 string #1677ff
zIndexPopup 팝업 메뉴의 z-index number 1050

글로벌 토큰 (Global Token)

토큰 이름 (Token Name) 설명 (Description) 타입 (Type) 기본값 (Default Value)
borderRadius 기본 컴포넌트의 테두리 반경 number
borderRadiusLG LG 크기 테두리 반경이에요. Card, Modal 등 큰 테두리 반경을 가진 컴포넌트에 사용돼요. number
boxShadowSecondary 요소의 2차 박스 섀도우 스타일을 제어해요. string
colorBgElevated 팝업 레이어의 컨테이너 배경색이에요. 다크 모드에서는 이 토큰의 색이 colorBgContainer보다 약간 밝아요. 예: modal, pop-up, menu 등. string
colorPrimaryBorder 메인 색 그라데이션 아래의 스트로크 색이에요. Slider 같은 컴포넌트의 stroke에 사용돼요. string
colorSplit 구분자 색으로 사용돼요. colorBorderSecondary와 같은 색이지만 투명도를 가져요. string
colorText W3C 표준을 따르는 기본 텍스트 색이에요. 가장 어두운 중성색이기도 해요. string
colorTextLightSolid 배경색이 있는 텍스트의 강조 색을 제어해요. Primary Button 컴포넌트의 텍스트 등. string
controlHeightLG LG 컴포넌트 높이 number
fontFamily Ant Design의 글꼴은 시스템의 기본 인터페이스 글꼴을 우선시하고, 화면 표시에 적합한 대체 글꼴 라이브러리를 제공해 플랫폼과 브라우저에 따라 가독성을 유지하며 친근하고 안정적이며 전문적인 특성을 반영해요. string
fontSize 디자인 시스템에서 가장 널리 쓰이는 글자 크기로, 여기서 텍스트 그라데이션이 파생돼요. number
fontSizeLG 큰 글자 크기 number
lineHeight 텍스트의 줄 높이예요. number
lineType 기본 컴포넌트의 테두리 스타일 string
lineWidth 기본 컴포넌트의 테두리 두께 number
lineWidthFocus 컴포넌트가 포커스 상태일 때 선의 너비를 제어해요. number
margin 중간 크기의 요소 여백을 제어해요. number
marginXS 작은 크기의 요소 여백을 제어해요. number
motionDurationMid 동작 속도, 중간 속도예요. 중간 요소의 애니메이션 상호작용에 사용돼요. string
motionDurationSlow 동작 속도, 느린 속도예요. 큰 요소의 애니메이션 상호작용에 사용돼요. string
motionEaseInOut 미리 정의된 모션 곡선이에요. string
motionEaseInOutCirc 미리 정의된 모션 곡선이에요. string
motionEaseInQuint 미리 정의된 모션 곡선이에요. string
motionEaseOut 미리 정의된 모션 곡선이에요. string
motionEaseOutCirc 미리 정의된 모션 곡선이에요. string
motionEaseOutQuint 미리 정의된 모션 곡선이에요. string
padding 요소의 패딩을 제어해요. number
paddingXL 요소의 특대 패딩을 제어해요. number
paddingXS 요소의 아주 작은 패딩을 제어해요. number

더 알아보기 (Learn more)