DEV Community

Ivan V.
Ivan V.

Posted on • Edited on

Using Next.js Link Component with Material UI Buttons and Menu Items

Update

  1. This blog post refers to Material-UI V4 but it should work with the V5 also.
  2. I've created a template repository for using Next.js with Material UI which you can use as a starting point.

demo: https://material-pwa.vercel.app/

repository: https://github.com/ivandotv/nextjs-material-pwa

Link component is a magic component of next.js framework, that does the routing both client-side and server-side (properly rendering links for SEO purposes).

Material UI is a most popular framework for implementing Google's material design system.
Since both frameworks expect a certain HTML structure to be present in order to render their components, using them together is not straightforward as it seems, luckily it's not that hard at all.

In this article, I'm going to show you how to properly render material UI buttons and menu items as nextjs links.

Rendering a material UI button is very easy, just wrap the button component with the nextjs link component and make sure you use passHref property on the link component.

<Link href="/about" passHref>
  <Button variant="contained" color="secondary">About</Button>
</Link>
Enter fullscreen mode Exit fullscreen mode

passHref must be used every time you use a custom component inside the Link component. Without it when you test your code client side it will appear that everything is working because the Link component will properly route the links however the generated a tag will not have a href property, so website crawlers will see no links, which will negatively impact your SEO.
The reason it appears that it works is that the link component has a value for href and it just listens to click events on its child components and then does the routing.

You can confirm this by disabling javascript in the browser and trying to navigate nextjs app, you will see that the links won't work.

Using ListItem Component

ListItem component is used inside all kinds of material UI menus, and it is a little bit trickier to set up because the generated HTML structure of the component is more complex than a simple button component.

Outhere on the internet you will find a lot of solutions that are using higher-order components and passing props all over the place however, the solution is very simple and it is offered by the API of the ListItem component itself.
What we need to do is to change the underlying element of the ListItem component to be an a tag. We can do this by using the component property of the ListItem.

<Link href="/about" passHref>
 <ListItem button component="a" onClick={onClick}>
  <ListItemText>About</ListItemText>
 </ListItem>
</Link>
Enter fullscreen mode Exit fullscreen mode

And that's it, links will work on the client-side, and they will be properly rendered on the server-side.

Top comments (15)

Collapse
 
codingjlu profile image
codingjlu

Okay... thanks for the article. Just want to point out though, that putting a button inside an <a> tag is invalid syntax. See stackoverflow.com/questions/639382....

Collapse
 
ivandotv profile image
Ivan V.

You can change the material ui button component to be represented with a different html element, so button could be represented with a div element.

Collapse
 
codingjlu profile image
codingjlu

Sure. I found this working for me and wanted to share it with anyone who'd think it's useful. In link.js:

import NextLink from "next/link";
import MuiLink from "@mui/material/Link";
import MuiButton from "@mui/material/Button";

export default function Link({ type, href, children, ...props }) {
  if (type === "link" || !type) {
    return (
      <NextLink href={href} passHref>
        <MuiLink {...props}>{children}</MuiLink>
      </NextLink>
    );
  } else if (type === "button") {
    return (
      <NextLink href={href} passHref>
        <MuiButton {...props}>{children}</MuiButton>
      </NextLink>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Then you can just use it as a utility components just like you would with Next and MUI's buttons:

import Link from "../path/to/link";
// Later...
<Link type="button" href="/somewhere">Yay!</Link>
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
cristianeto profile image
Cristian Guamán

super!!!

Thread Thread
 
petroswursta profile image
petros-wursta

I tried this but was not able to get it to work.

I added a links.js file and added your code.

then this
import Link from "../path/to/link";
// Later...
Yay!

but it doesn't work, I get the following error.

Any help please?!

Image description

Thread Thread
 
codingjlu profile image
codingjlu

Could you show a bit more code? Did you adjust the code to actually fit your project? Did you paste the correct code into your link component?

Thread Thread
 
petroswursta profile image
petros-wursta

Thank you! Looks like I got it to work. I needed to install the mui library.

I just need to add styling b/c it just shows up as texts instead of a button.

Thread Thread
 
codingjlu profile image
codingjlu

Great, glad it worked out!

Collapse
 
rajnoua profile image
Raj • Edited

This is also a valid option now. Works like a charm!

<Button
    variant="contained"
    color="primary"
    LinkComponent={Link}
    href="/parties/add"
>
    Add Party
</Button>
Enter fullscreen mode Exit fullscreen mode
Collapse
 
ivandotv profile image
Ivan V.

Thanks Raj! Good to know!

Collapse
 
henettaja profile image
Henri Väisänen • Edited

This is the only way out of the one's mentioned on this post (and in the comments) that seem to handle keyboard navigation and other accessibility (a11y) concerns correctly without further setup. However, this does apparently require Next.js 13 to work correctly (or perhaps at all?), since previously the Next.js Link component didn't expose the native HTML <a> anchor tag.

The issue with the other implementations is that wrapping the <Link> component around the <Button> causes there to be two focusable elements for each navigation button, which isn't good for anyone navigating the page with a keyboard and likely causes additional more severe issues for people using a screen reader. I'm not sure if there are better implementations for Next.js versions prior to version 13, but I would hope there are.

If not and/or you're about to use the <Link> component wrapped around the <Button> component, then a quick and dirty fix would be the following:

  1. Make sure the inner button isn't focusable by setting tabindex="-1" on it. The reason this is a quick and dirty fix is that it means you won't get the Button's focus state when navigating to the <Link> with a keyboard.
  2. This probably messes up with a screen readers ability to read the text that communicates where the <Link> is taking you (because the text isn't in the <Link>, but instead in the <Button>), so you can fix that by using [the aria-labelledBy attribute]((developer.mozilla.org/en-US/docs/W...) (this is recommended over aria-label whenever a descriptive element is visible on the page, like in this case.

With good conscience I can't necessarily recommend this workaround even if you are not on version 13 or higher yet, because I'm not familiar with earlier Next.js versions and if there are other implementations that could potentially resolve these issues without having to manually configure the a11y stuff. From version 13 onwards that kind of implementation is @rajnoua 's example above, it overcomes all these issues altogether (you can also implement it globally via MUI theme, details in the second last paragraph).

If you're on a version lower than 13, I encourage you to try to find a better way that takes a11y into account and if you find one that's better that this quick and dirty one, please comment here for others to see as well, since this post seems to come up quite high on Google and not everyone will be able to switch to version 13 very soon.

The quick and dirty (not recommended) way to fix some of the a11y concerns of the original implementation in this post:

/* value of aria-labelledBy on the Link and the id on the corresponding 
* Button should match and they should be unique to the specific 
* Link and Button pair, take that into account if you have multiple buttons 
*/
<Link aria-labelledBy="about-nav-button" href="/about" passHref>
  <Button tabIndex={-1} id="about-nav-button">About</Button>
</Link>
Enter fullscreen mode Exit fullscreen mode

So, use @rajnoua 's example that's in the comment I'm answering to, you can even implement it globally via MUI's theme (haven't tested personally). Just check this answer by Shikyo on Stackoverflow.

Maybe author @ivandotv could update the post with info about the new implementation for Next.js 13 and with info about the a11y concerns of the original, since these comments won't reach everyone reading this.

Collapse
 
ivandotv profile image
Ivan V.

I would gladly update the post, can you give me a working gist that I can add to the article?

Thread Thread
 
henettaja profile image
Henri Väisänen

Thank you for your willingness to update the post! Sorry it took me a while to answer you, been a little busy.

Here's a quick gist I made for you about the accessible implementation for Next.js 13:

AccessibleLink.tsx gist on Github

Collapse
 
12dimov profile image
Vania Dimova

You are life saver, glad to see it in one page. This is how they should write docs! Thank you!

Collapse
 
fabarea profile image
Fabien Udriot

Very useful piece of information. Thanks.