Next.js App Router
Learn how to use Base UI with the Next.js App Router.
Example
Starting fresh on a new App Router-based project?
Jump right into the code with this example: Base UI - Next.js App Router with Tailwind CSS in TypeScript.
Next.js and React Server Components
The Next.js App Router implements React Server Components, an upcoming feature for React.
To support the App Router, the components and hooks from Base UI that need access to browser APIs are exported with the "use client"
directive.
Setting up Base UI with the App Router
Base UI gives you the freedom to choose your own styling solution, so setting up a Next.js App Router project largely depends on what you choose. This guide covers Tailwind CSS, Emotion, and other CSS-in-JS solutions like styled-components.
Tailwind CSS
Follow the Tailwind CSS guide on working with Next.js, and be sure to add the app
directory and other directories to tailwind.config.js
, as shown below:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}'
// or if not using the `src` directory:
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {},
},
plugins: [],
};
Refer to this example repo for a full working demo of a Next.js 13 app using Base UI and Tailwind CSS.
Emotion
If you're using Emotion, or something Emotion-based like MUI System, create a custom ThemeRegistry
component that combines the Emotion CacheProvider
, the Material UI ThemeProvider
, and the useServerInsertedHTML
hook from next/navigation
as follows:
// app/ThemeRegistry.tsx
'use client';
import createCache from '@emotion/cache';
import { useServerInsertedHTML } from 'next/navigation';
import { CacheProvider, ThemeProvider } from '@emotion/react';
import theme from '/path/to/your/theme';
// This implementation is from emotion-js
// https://github.com/emotion-js/emotion/issues/2928#issuecomment-1319747902
export default function ThemeRegistry(props) {
const { options, children } = props;
const [{ cache, flush }] = React.useState(() => {
const cache = createCache(options);
cache.compat = true;
const prevInsert = cache.insert;
let inserted: string[] = [];
cache.insert = (...args) => {
const serialized = args[1];
if (cache.inserted[serialized.name] === undefined) {
inserted.push(serialized.name);
}
return prevInsert(...args);
};
const flush = () => {
const prevInserted = inserted;
inserted = [];
return prevInserted;
};
return { cache, flush };
});
useServerInsertedHTML(() => {
const names = flush();
if (names.length === 0) {
return null;
}
let styles = '';
for (const name of names) {
styles += cache.inserted[name];
}
return (
<style
key={cache.key}
data-emotion={`${cache.key} ${names.join(' ')}`}
dangerouslySetInnerHTML={{
__html: styles,
}}
/>
);
});
return (
<CacheProvider value={cache}>
<ThemeProvider theme={theme}>{children}</ThemeProvider>
</CacheProvider>
);
}
// app/layout.js
export default function RootLayout(props) {
return (
<html lang="en">
<body>
<ThemeRegistry options={{ key: 'mui' }}>{props.children}</ThemeRegistry>
</body>
</html>
);
}
If you need to further override theme styles (e.g. using CSS modules), Emotion provides the prepend: true
option for createCache
to reverse the injection order, so custom styles can override the theme without using !important
.
Currently, prepend
does not work reliably with the App Router, but you can work around it by wrapping Emotion styles in a CSS @layer
with a modification to the snippet above:
useServerInsertedHTML(() => {
const names = flush();
if (names.length === 0) {
return null;
}
let styles = '';
for (const name of names) {
styles += cache.inserted[name];
}
return (
<style
key={cache.key}
data-emotion={`${cache.key} ${names.join(' ')}`}
dangerouslySetInnerHTML={{
- __html: styles,
+ __html: options.prepend ? `@layer emotion {${styles}}` : styles,
}}
/>
);
});
Other CSS-in-JS libraries
To use Next.js with Base UI and styled-components or other CSS-in-JS solutions, follow the Next.js doc on CSS-in-JS.
Customization
Using callbacks for slot props
A common customization method in Base UI is to pass a callback to slots in slotProps
in order to apply dynamic props. For example, you might want to change the background color by applying a different class when a Button is disabled:
// page.tsx
export default function Page() {
return (
<React.Fragment>
{/* Next.js won't render this button without 'use-client'*/}
<Button
slotProps={{
root: (ownerState: ButtonOwnerState) => ({
className: ownerState.disabled ? 'bg-gray-400' : 'bg-blue-400',
}),
}}
>
Submit
</Button>
{/* Next.js can render this */}
<Button
slotProps={{
root: {
className: 'bg-gray-400',
},
}}
>
Return
</Button>
</React.Fragment>
);
}
Unfortunately, this does not work in a Server Component since function props are non-serializable. Instead, the Next.js team recommend moving components like these "down the tree" to avoid this issue and improve overall performance.