DMC, Inc.

Next.js 16 + Material UI Setup Guide for App Router + RSC: Dev Diary #2

In Part 1 of this two-part series, we walked through scaffolding a brand-new React application with Next.js. The result was a bare bones application with no styling to speak of. Early in any web application project, you have an important decision to make: are you going to build the look and feel of your app from scratch with custom CSS and JavaScript, or are you going to leverage a third-party solution to achieve a more professional-looking UI than you would with a custom solution and with less development effort? Most people choose the latter, and we will too.

At DMC, our preferred UI library is Material UI, a component library maintained by Google. I first used Material UI nearly a decade ago, before it even reached version 1.0. Since then, the library has evolved significantly and now includes first-class support for Next.js and React Server Components. In this post, we’ll walk through how to add Material UI to a new Next.js 16 application. At the time of writing, Material UI v9.0.0 is the version in use.

Installing the NPM Packages for Material UI

First, run the following command to install the core Material UI packages using Yarn:

C
yarn add @mui/material @emotion/react @emotion/styled


The Material UI documentation recommends installing @fontsource/roboto to use the Roboto font in your application. However,  at the time of writing this post, that package causes the TypeScript server in my project to crash, resulting in the loss of IntelliSense support in Visual Studio Code. Because of this, I recommend skipping that package. Later in this post, we’ll import the Roboto font using a different approach.

Next, run the following command to install the packages required to integrate Material UI with Next.js App Router:

C
yarn add @mui/material-nextjs @emotion/cache


Setting Up a Material UI Theme

One of Material UI’s most useful features is its customizable themes. Material UI includes a default theme that can be extensively customized—everything from spacing, color palettes, and even border radius can be modified based on your design sensibilities.

For this application, we’ll primarily use the default  Material UI theme while enabling built-in light/dark mode functionality. We can initialize the theme using the createTheme() function:

C
// src/theme.tsx
"use client";
import { createTheme } from "@mui/material/styles";

const theme = createTheme({
  typography: {
    fontFamily: "var(--font-roboto)",
  },
  cssVariables: {
    colorSchemeSelector: "class",
  },
  colorSchemes: {
    dark: true,
  },
});

export default theme;

This theme configuration does three things:

  1. Sets Roboto as the default font family for all Typography components.
  2. Enables dark mode support. Light mode is enabled by default.
  3. Enables CSS variables and configures the colorSchemeSelector to use CSS classes.

The final configuration helps prevent SSR flickering, which can occur because the server cannot detect a user’s preferred color mode during the initial render. With this setup, Material UI applies either a light or dark class to the <html> element on the client side and uses CSS variables to apply the appropriate theme colors. This approach provides a smoother transition between server-side rendering and client-side hydration while maintaining support for light and dark themes.

Screenshot of a JavaScript code snippet manages theme switching between light and dark modes for a webpage.
Image displays CSS code using the Material UI (mui) framework to style an app bar component when the application is in dark mode.

Color Mode Picker

Speaking of color mode (light/dark), it can be nice to offer users a way to switch between light mode and dark mode. With the way we’ve configured our theme, Material UI will automatically set the color mode based on the user’s system preference, set in their web browser. So if a user has configured “dark” mode in Chrome, for example, when they visit our application, they’ll automatically get the dark-mode version of our UI.

Some users may want dark mode specifically for our application and light mode elsewhere, so to provide that flexibility, we’ll build a simple color mode selector. For this, we’ll use the tools Material UI provides, including input and label components, along with tools for customizing them.

C
// colorThemePicker.tsx
"use client";
import FormControl from "@mui/material/FormControl";
import Select, { SelectChangeEvent } from "@mui/material/Select";
import MenuItem from "@mui/material/MenuItem";
import { useColorScheme } from "@mui/material/styles";
import { FormLabel } from "@mui/material";

export default function ColorThemePicker() {
  const { mode, setMode } = useColorScheme();
  if (!mode) {
    return null;
  }
  const handleChange = (event: SelectChangeEvent) => {
    setMode(event.target.value as "system" | "light" | "dark");
  };

  return (
    <FormControl
      sx={{
        display: "flex",
        flexDirection: "row",
        alignItems: "center",
        gap: 1,
      }}
    >
      <FormLabel id="theme-select-label">Theme</FormLabel>
      <Select
        labelId="theme-select-label"
        aria-labelledby="theme-select-label"
        id="theme-select"
        value={mode}
        label="Theme"
        onChange={handleChange}
        sx={{ minWidth: "10rem" }}
      >
        <MenuItem value="system">System</MenuItem>
        <MenuItem value="light">Light</MenuItem>
        <MenuItem value="dark">Dark</MenuItem>
      </Select>
    </FormControl>
  );
}

I’ll explain this from the top down.

We need a piece of state to track the user’s color mode selection: light, dark, or system (whatever they’ve selected in their browser settings). We also need a setter for that state so that we can update it based on the user’s actions. For that, we use a hook provided by Material UI: useColorScheme(). The mode returned by this hook is always undefined on the first render, so we handle that with a simple falsiness check:

C
if(!mode) {
  return null;
}

Next, we have our return statement where we declare the UI that this component should render. We’re making use of a few components from Material UI:

The latter two are kind of self-explanatory if you have a basic background in HTML. FormControl is a nice-to-have component that manages some state related to the input element that it wraps, such as focused state, filled state, error state, required state, etc.

Something I want to point out is the use of the sx prop on the FormControl component. This is a CSS-in-JS styling solution from Material UI, intended for one-off customization. Our application has precisely one color theme picker, so it easily fits the definition of a one-off customization. You can read more about this system in the Material UI documentation.

The purpose served by our use of the sx prop here is to apply some flexbox CSS properties to our FormControl to make its child elements (the FormLabel and Select components) appear side by side instead of displaying the form label above the select component.

C
<FormControl
  sx={{
    display: "flex",
    flexDirection: "row",
    alignItems: "center",
    gap: 1,
  }}
>/

Under the hood, a CSS selector is generated and applied to the rendered DOM element, and a <style> tag containing the corresponding CSS rule(s) is inserted into the DOM (see the screenshot below). So the result is similar to what we would get with a style sheet, except that it is generated at runtime after the initial HTML document is served to the client, which introduces some overhead for applying style rules.

Image displaying a CSS style block generated by the Material-UI (MUI) library, specifically targeting a form label component.

In short, the trade-off is simpler code that’s easier to understand in exchange for a performance drop: up to 3x slower render time for 1,000 components, according to the documentation, which means you have to be rendering a lot of items to get a noticeable performance difference.

Here’s what our color scheme picker looks like when rendered on the page:

Image displaying a drop-down menu for selecting a user interface Theme with "Light" selected.

And in dark mode:

Image displaying a drop-down menu for selecting a user interface Theme with "Dark" selected.

Not bad!

Client Components and Building a Nav Bar

Material UI has a <Link> component that makes it easy to render a professional-looking link on a web page. Since we’re using Next.js, we also want to use the <Link> component, which handles both server- and client-side routing. We’ll do this by providing the Next.js <Link> to the Material UI <Link> “component” prop, which essentially tells Material UI, “use this as the underlying component that you then attach all your fancy styles to.”

However, with Next.js 16, this can result in an error: “Functions cannot be passed directly to Client Component”. As outlined in the Material UI documentation for Next.js integration, we can work around this by creating a client-side wrapper component for the Next.js <Link> component.

C
// src\app\_components\link.tsx
'use client';
import Link, { LinkProps } from 'next/link';

export default Link;

This will come in handy when building our top nav bar for this application, which we can get started on:

C
// src\app\_components\topNavBar.tsx 
import NextLink from "./link";
// (rest of import section omitted for brevity)

function TopNavBar() {
  return (
    <AppBar position="static">
      <Container
        maxWidth="xl"
        sx={{ display: "flex", justifyContent: "space-between" }}
      >
        <Box
          sx={{
            display: "flex",
            justifyContent: "space-between",
            alignItems: "center",
          }}
          component="nav"
        >
          <Link component={NextLink} href="/" color="inherit" underline="none">
            <Typography
              variant="body1"
              component="span"
              sx={{ display: "flex", alignItems: "center", gap: 0.5 }}
            >
              <Icon>home</Icon>
              Home
            </Typography>
          </Link>
        </Box>
        <ColorThemePicker />
      </Container>
    </AppBar>
  );
}

export default TopNavBar;

For our top nav bar, we’re using the AppBar component from Material UI. This component looks great at the top of the screen and is an ideal place for top-level links, action buttons, menus, a login/logout button, etc.

We’re putting a couple of elements into our top nav bar to start out:

  1. A list of links (currently just a link to the home page)
  2. The color theme picker (this will eventually be replaced with a “hamburger button” that opens a modal dialog with settings/options)

I want these two components to be evenly spaced across the app bar, which sounds like a job for flexbox. To make that happen, I need to wrap them in a component, and Material UI’s Container component fits the bill. I’m using Container because it’s a responsive component—we can use the maxWidth prop to define how wide it’s allowed to get based on the screen size. I want this nav bar to resize responsively all the way up to large desktop screens, so I’m providing “xl” as the value here – the width of the container will automatically adjust to smaller screen sizes.

We can use the sx prop here to provide custom styling rules for this component—in this case, that would be using the “display” and “justifyContent” CSS properties to declare that the children of this component should be spaced evenly across the width of this container.

C
<Container
  maxWidth="xl"
  sx={{ display: "flex", justifyContent: "space-between" }}
>

To follow HTML best practices, we want to wrap our navigation links in a semantic <nav> element. But we also want to make sure that the links themselves are evenly spaced, so we need to apply some CSS rules to this <nav> element. The best way to accomplish this with Material UI is to use the generic Box component.

Whenever you need to wrap other components with a containing element and apply style rules to it, <Box> is the go-to component. And we can tell Material UI which HTML element to render in the DOM by using the component prop, so we’ll pass the value “nav” to that prop to ensure it renders as a proper nav element. And again, we’re using the sx prop to define some basic flexbox properties and ensure that our link elements inside this box are spaced evenly and positioned nicely.

C
<Box
  sx={{
    display: "flex",
    justifyContent: "space-between",
    alignItems: "center",
  }}
  component="nav"
>

To start with, we’re going to only have one link in our nav bar: “Home”. The simplest version of this would just be to use Material UI’s Link component, pass my wrapper for the Next.js Link to the “component” prop, and, of course, set href to “/”. Like the following:

C
<Link component={NextLink} href="/" color="inherit" underline="none">
  Home
</Link>

But I also want to add a nifty little house icon to this link. Because I want this link to be inviting: “Come on home, you’re tired, and we have a nice fire going. You can take off your socks and cozy up by the fireplace with some hot cocoa and a grilled cheese sandwich.”

Material UI makes that easy with its Icon component. You can choose SVG or Font for the icons. To get off to a quick and easy start, I recommend the Font option. Later in this post, I’ll show how to enable that, but here’s what it’s going to look like in this component:

C
<Link component={NextLink} href="/" color="inherit" underline="none">
  <Icon>home</Icon>
  Home
</Link>

This ends up looking like the following when rendered on the page:

Screenshot of "home" link and "home" icon. The icon is positioned awkwardly off-center, making it look crooked.

Oops! That icon is crooked! Or more accurately, it’s not vertically centered, so it looks awkward next to the text “Home”. We’ll fix that by applying some flexbox styling to the Link component:

C
<Link
  component={NextLink}
  href="/"
  color="inherit"
  underline="none"
  sx={{ display: "flex", alignItems: "center", gap: "0.5rem" }}
>
  <Icon>home</Icon>
  Home
</Link>

The result looks so much better!

Screenshot of "home" link and "home icon", vertically aligned to look neater.

Finally, after the nav links themselves, we have the color theme picker. Altogether, our nav bar looks like the following screenshot:

Screenshot of top nav bar.

Not bad, at least to start.

Updating the Root Layout

Now we’re ready to update our root layout (/src/app/layout.tsx) to use Material UI in our application.

Roboto Font Set Up

To enable using the Roboto font as the default font for our app, we need to use Next.js font optimization. The result of this step is that we end up self-hosting the Roboto font from Google:

C
const roboto = Roboto({
  weight: ["300", "400", "500", "700"],
  subsets: ["latin"],
  display: "swap",
  variable: "--font-roboto",
});

With that set up, we can give our HTML (root) element a new CSS class that will help give every typography element the Roboto font:

C
<html lang="en" className={roboto.variable} suppressHydrationWarning>

Remember how we set up our theme with the following property?

C
typography: {
  fontFamily: "var(--font-roboto)",
},

That “var(–font-roboto)” element is a CSS variable. When combined, these two things result in a CSS variable named “–font-roboto” that’s scoped to a CSS class that’s generated at runtime and inserted into the stylesheet. This CSS class is then assigned to the <html> element so that any element in our HTML document can use that CSS variable. And by utilizing the “fontFamily” property of the “typography” section of our theme, we ensure that every typography element (MUI or not) uses the value of that variable for its “font-family” CSS property.

The following screenshots from Chrome dev tools demonstrate this in action:

HTML tag showing the dynamically-generated CSS class.
Screenshot showing the CSS variable "--font-roboto"
Screenshot of a CSS rule for a label component, with the "font-family" property set to "var(--font-roboto)"

Material Icons Set Up

We need to import the Material Icons font so that we can use icons in our app. The easiest way to do this is to get it from the CDN. We can use the <head> element in our root layout.tsx file to insert a stylesheet link into the HTML document, ensuring that the user’s browser downloads that font.

C
<head>
  <link
    rel="stylesheet"
    href="https://fonts.googleapis.com/icon?family=Material+Icons"
  />
</head>

There’s also an NPM package you can install to self-host the font, but as I said earlier, it seems to crash the TypeScript server when using Next.js 16, so we’ll use the CDN until that gets sorted out.

AppRouterCacheProvider

For optimal performance, it’s best to wrap your application with Material UI’s AppRouterCacheProvider component. According to the documentation, this component “is responsible for collecting the CSS generated by MUI System on the server, as Next.js is streaming chunks of the .html page to the client.”

C
<body>
  <AppRouterCacheProvider>
    <main>
      {children}
    </main>
  </AppRouterCacheProvider>
</body>

ThemeProvider

For our application to use our custom theme, we need to wrap our application with the ThemeProvider component from Material UI, and provide our custom theme to the “theme” prop:

C
import theme from "../theme";
// ...
<body>
  <AppRouterCacheProvider>
    <ThemeProvider theme={theme}>
      <main>
        {children}
      </main>
    </ThemeProvider>
  </AppRouterCacheProvider>
</body>

CssBaseline

The CssBaseline component allows us to clear out some undesirable CSS rules that browsers tend to have enabled by default. For example, without this component, there would be a default “margin” rule for <html> and <body>, resulting in a space around our actual page. So, our AppBar would not stretch all the way to the edges of the page, which is a jarring look. See the following screenshot for an example of what that looks like:

Screenshot of the margin that appears by default if CssBaseline isn't used.

So to avoid this, we simply have to add the CssBaseline component to our root layout:

C
<body>
  <AppRouterCacheProvider>
    <ThemeProvider theme={theme}>
      <CssBaseline />
      <main>
        {children}
      </main>
    </ThemeProvider>
  </AppRouterCacheProvider>
</body>

That makes the margin disappear, as we can see in this screenshot:

Screenshot of app without the browser default margin, thanks to CssBaseline.

InitColorSchemeScript

The InitColorSchemeScript component, when rendered before any of our actual content, will cause a script to run that attaches an attribute (a CSS class in our case) to the <html> element, indicating whether to use light or dark mode, depending on user preference. This runs before React itself starts doing client-side rendering and hydration; the result is that we prevent a “flicker” that would otherwise occur because the color scheme is not the default that’s assumed during server-side render.

C
<body>
  <AppRouterCacheProvider>
    <ThemeProvider theme={theme}>
      <CssBaseline />
      <InitColorSchemeScript attribute="class" />
      <main>
        {children}
      </main>
    </ThemeProvider>
  </AppRouterCacheProvider>
</body>

Wrapping App Contents in a Container and Adding the Nav Bar

To finish things up here, we’re going to wrap the child components (i.e., the contents of any given page) in a Container component so that they appear to be the same width as the content of our AppBar. Without this, the content of the app’s pages would stretch to the edge of the screen. See the following screenshot—it doesn’t look right.

Screenshot of the web page's content stretching all the way to the edge of the browser window, making it wider than the nav bar's contents.

By wrapping {children} in a container and giving it maxWidth of “xl”, matching the approach used for the AppBar content, we ensure the entire application aligns cleanly and maintains a comfortable margin between the screen edge and the app’s content.

Screenshot of the web page's content having the same margin from the edge of the screen as the nav bar's contents.

We’ll also render our custom TopNavBar component before the container, so that the nav bar always appears at the top of the page, with the current web page being displayed beneath it.

C
<body>
  <AppRouterCacheProvider>
    <ThemeProvider theme={theme}>
      <CssBaseline />
      <InitColorSchemeScript attribute="class" />
      <main>
        <TopNavBar />
        <Container maxWidth="xl">{children}</Container>
      </main>
    </ThemeProvider>
  </AppRouterCacheProvider>
</body>

The Full layout.tsx File

Altogether, our layout.tsx file looks like this:

C
// src\app\layout.tsx
import type { Metadata } from "next";
import { AppRouterCacheProvider } from "@mui/material-nextjs/v15-appRouter";
import { Roboto } from "next/font/google";
import { ThemeProvider } from "@mui/material/styles";
import theme from "../theme";
import { Container, CssBaseline, InitColorSchemeScript } from "@mui/material";
import TopNavBar from "./_components/topNavBar";

const roboto = Roboto({
  weight: ["300", "400", "500", "700"],
  subsets: ["latin"],
  display: "swap",
  variable: "--font-roboto",
});

export const metadata: Metadata = {
  title: "DBA Dashboard",
  description:
    "A dashboard UI for exploring the results from the SQL Server First Responder Kit stored procedures",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en" className={roboto.variable} suppressHydrationWarning>
      <head>
        <link
          rel="stylesheet"
          href="https://fonts.googleapis.com/icon?family=Material+Icons"
        />
      </head>
      <body>
        <AppRouterCacheProvider>
          <ThemeProvider theme={theme}>
            <CssBaseline />
            <InitColorSchemeScript attribute="class" />
            <main>
              <TopNavBar />
              <Container maxWidth="xl">{children}</Container>
            </main>
          </ThemeProvider>
        </AppRouterCacheProvider>
      </body>
    </html>
  );
}

We can also adjust the page.tsx (home page) component to be a bit more minimal, since it no longer needs to render its own <main>:

C
// src\app\page.tsx
export default function Home() {
  return (
    <>
      <header>
        <h1>DBA Dashboard</h1>
      </header>
      <p>
        Welcome to the DBA Dashboard! There's not much here yet, but stay
        tuned for updates as we build out the UI for exploring the results from
        the SQL Server First Responder Kit stored procedures.
      </p>
    </>
  );
}

Conclusion

And that’s it! We’ve successfully set up Material UI with our Next.js 16 application—going forward, we can easily make every UI element we add to the app look professional with minimal effort. In the next post, we’ll get authentication set up using a library called Better Auth, along with Microsoft’s Entra ID authentication service.

Have an upcoming project? DMC can help you take the next step.

Take your project to the next level with engineering solutions from DMC. Learn more about our Application Development solutions or contact us to get started today!