Next.js App Router
Learn how to use Base UI with the Next.js App Router.
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.
React Server Components should not be conflated with the concept of server-side rendering (SSR). So-called Client Components are still server-rendered to HTML.
For more details, see this explanation of Client Components and SSR from the React Working Group.
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: [],
};
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 (for example 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 render props
A common customization method in Base UI is to pass a callback to the render
or className
props in order to apply dynamic values.
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 className={(state: ButtonState) => (state.disabled ? 'bg-gray-400' : 'bg-blue-400')}>
Submit
</Button>
{/* Next.js can render this */}
<Button 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.