This website uses cookies and self-hosted analytics. More information.

Responsive Menu with Ant Design

·
react
javascript
ant design

Ant Design + React

TLDR; Github repository

I have seen several posts on Github and Stackoverflow asking for solutions to make Ant Menu component responsive. And since I haven't found a single post about it, that usually means you have to figure it out yourself. In the end, the solution is rather simple:

  1. Create a Menu component which contains individual Menu items as specified in Ant Design Docs.
  2. Create a Container component which accepts Menu component as a prop and will take care of the Menu visibility & the viewport resize events.
  3. That's it!

Let's see some code:

// 1. MenuMarkup.js
const MenuMarkup = ({ mobileVersion, activeLinkKey, onLinkClick, className }) => (
  <Menu
    theme={mobileVersion ? 'light' : 'dark'}
    mode={mobileVersion ? 'vertical' : 'horizontal'}
    selectedKeys={[`${activeLinkKey}`]}
    className={className}
  >
    <Menu.Item key='/'>
      <Link onClick={onLinkClick} to='/'>Home</Link>
    </Menu.Item>
    <Menu.Item key='/about'>
      <Link onClick={onLinkClick} to='/about'>About</Link>
    </Menu.Item>
    <Menu.Item key='/topics'>
      <Link onClick={onLinkClick} to='/topics'>Topics</Link>
    </Menu.Item>
  </Menu>
);

You can read more about the Menu component in a pretty detailed Ant Design documentation.

Let's quickly get through props the MenuMarkup accepts:

mobileVersion - through this prop we will decide whether the Menu will be rendered in horizontal (Desktop) or vertical (Mobile) mode

activeLinkKey - could be for example provided by React Router's HOC, this way Menu can keep track of the currently active url in order to add proper classes for active Menu Item. Note the key attribute at each Menu.Item.

onLinkClick - called whenever the Menu Item is clicked in order to close the mobile version of the Menu

className - custom class name, for styling purposes.

Now that we have the Menu markup, lets see how to make it responsive.

We will pass the Menu markup as a prop to our Container component, which will handle rendering.

Container component however accepts a few more props, lets quickly review them:

<ResponsiveNav 
    menuMarkup={MenuMarkup}
    activeLinkKey={location.pathname}
    mobileBreakPoint={767}
    placement='bottom'
/>

menuMarkup - required, the actual menu markup

activeLinkKey - required, to let the Ant's Menu component know which item should be set to active

mobileBreakPoint - default 575px, breakpoint at which should the mobile navigation be rendered

placement - default 'bottom', position of Popover, please see an excellent Ant Design Docs

applyViewportChange - default 250ms, how often should we check for viewport width in order to render correct navigation

Now, it's finally time for the the juicy stuff, the actual Container component itself:

class ResponsiveNav extends Component {
  state = {
    viewportWidth: 0,
    menuVisible: false,
  };

  componentDidMount() {
    // update viewportWidth on initial load
    this.saveViewportDimensions();
    // update viewportWidth whenever the viewport is resized
    window.addEventListener('resize', this.saveViewportDimensions);
  }

  componentWillUnmount() {
    // clean up - remove the listener before unmounting the component
    window.removeEventListener('resize', this.saveViewportDimensions);
  }

  handleMenuVisibility = (menuVisible) => {
    this.setState({ menuVisible });
  };
  
  // first of all notice lodash.throttle() helper
  // we do not want to run the saveViewportDimensions() hundreads of times
  // from start to finish whenever the viewport is being resized
  saveViewportDimensions = throttle(() => {
    this.setState({
      viewportWidth: window.innerWidth,
    })
  }, this.props.applyViewportChange); // default 250ms

  render() {
    const MenuMarkup = this.props.menuMarkup; // get the Menu markup, passed as prop

    if (this.state.viewportWidth > this.props.mobileBreakPoint) {
      // viewportWidth is over the mobileBreakpoint - render menu as is 
      return <MenuMarkup activeLinkKey={this.props.activeLinkKey} />;
    }

    return (
      <Popover
        content={<MenuMarkup
          // whenever the Menu item is clicked, hide the Popover
          onLinkClick={() => this.handleMenuVisibility(false)}
          // pass the proper activeLinkKey (this comes from React Router's props.location.pathname)
          activeLinkKey={this.props.activeLinkKey}
          mobileVersion // same as mobileVersion={true}
          // className='to-override-mobile-menu-class'
          />
        }
        trigger='click'
        placement={this.props.placement}
        visible={this.state.menuVisible}
        onVisibleChange={this.handleMenuVisibility}
      >
        <Icon
          className='iconHamburger'
          type='menu'
        />
      </Popover>
    );
  }
}

ResponsiveNav.propTypes = {
  mobileBreakPoint: PropTypes.number,
  applyViewportChange: PropTypes.number,
  activeLinkKey: PropTypes.string,
  placement: PropTypes.string,
  menuMarkup: PropTypes.oneOfType([
    PropTypes.func,
    PropTypes.object,
  ]),
};

ResponsiveNav.defaultProps = {
  mobileBreakPoint: 575,
  applyViewportChange: 250,
  placement: 'bottom',
};

Again, I highly suggest to read more about Popover in Ant Design documentation

Whenever the viewport width will be 575px or less, we will pass the MenuMarkup into Popover and render a hamburger icon instead.

Every time the hamburger icon is clicked, either true or false will be passed to the handleMenuVisibility() thus showing or hiding the Popover component.

onVisibleChange could also look like the following snippet to make things (hopefully) a bit more clear:

// menuVisible is either true or false
onVisibleChange={(menuVisible) => {this.handleMenuVisibility(menuVisible)}

To see it all in action, simply resize this page or see complete code on Github

So there you have it, a quick and simple working example of responsive menu with Ant Design.