Create React Next.js app with Material UI - Step by step
Create React Next.js app with Material UI - Step by step
TD;DR
Instead of replicating steps below use GitHub template.
GitHub - igorrendulic/nextjs-react-typescript-material-starter-template
Contribute to igorrendulic/nextjs-react-typescript-material-starter-template development by creating an account on GitHub.
Install dependencies: yarn install
Run the server: yarn dev
Installations
- Install npx, yarn, update create-next-app
- Create next.js app
npx create-next-app my-app-name --typescript
cd my-app-name
3. Install Material UI
yarn add @mui/material @emotion/react @emotion/styled @emotion/server
4. Install Roboto font
yarn add @fontsource/roboto
5. Install Material Icons
yarn add @mui/icons-material
The Code ("src" folder)
Create utility
sub-folder.
Create theme
sub-folder.
Create components
sub-folder
Create file createEmotionCache.ts
:
import createCache from '@emotion/cache';
const createEmotionCache = () => {
return createCache({ key: 'css', prepend: true });
};
export default createEmotionCache;
Under theme
create index.ts
:
import type { Theme } from "@mui/material";
import {
createTheme as createMuiTheme,
responsiveFontSizes,
} from "@mui/material/styles";
import { baseThemeOptions } from "./base-theme-options";
import { darkThemeOptions } from "./dark-theme-options";
import { lightThemeOptions } from "./light-theme-options";
interface Neutral {
100: string;
200: string;
300: string;
400: string;
500: string;
600: string;
700: string;
800: string;
900: string;
}
declare module "@mui/material/styles" {
interface Palette {
neutral?: Neutral;
}
interface PaletteOptions {
neutral?: Neutral;
}
}
interface ThemeConfig {
responsiveFontSizes?: boolean;
mode: "light" | "dark";
}
export const createTheme = (config: ThemeConfig): Theme => {
let theme = createMuiTheme(
baseThemeOptions,
config.mode === "dark" ? darkThemeOptions : lightThemeOptions
);
theme = responsiveFontSizes(theme);
return theme;
};
Under theme
sub-folder create base-theme-options.ts
:
import { ThemeOptions } from '@mui/material';
export const baseThemeOptions: ThemeOptions = {
breakpoints: {
values: {
xs: 0,
sm: 600,
md: 1000,
lg: 1200,
xl: 1920
}
},
components: {
MuiAvatar: {
styleOverrides: {
root: {
fontSize: 14,
fontWeight: 600,
letterSpacing: 0
}
}
},
MuiButton: {
defaultProps: {
disableElevation: true
},
styleOverrides: {
root: {
textTransform: 'none',
borderRadius: '3px',
},
sizeSmall: {
padding: '6px 16px'
},
sizeMedium: {
padding: '8px 20px'
},
sizeLarge: {
padding: '11px 24px'
},
textSizeSmall: {
padding: '7px 12px'
},
textSizeMedium: {
padding: '9px 16px'
},
textSizeLarge: {
padding: '12px 16px'
},
}
},
MuiCardActions: {
styleOverrides: {
root: {
}
}
},
MuiCardContent: {
styleOverrides: {
root: {
}
}
},
MuiCardHeader: {
defaultProps: {
titleTypographyProps: {
variant: 'h6'
},
subheaderTypographyProps: {
variant: 'body2'
}
},
styleOverrides: {
root: {
}
}
},
MuiCheckbox: {
defaultProps: {
color: 'primary'
}
},
MuiCssBaseline: {
styleOverrides: {
html: {
MozOsxFontSmoothing: 'grayscale',
WebkitFontSmoothing: 'antialiased',
display: 'flex',
flexDirection: 'column',
minHeight: '100%',
width: '100%'
},
body: {
display: 'flex',
flex: '1 1 auto',
flexDirection: 'column',
minHeight: '100%',
width: '100%'
},
'#nprogress': {
pointerEvents: 'none'
},
'#nprogress .bar': {
backgroundColor: '#000000',
height: 3,
left: 0,
position: 'fixed',
top: 0,
width: '100%',
zIndex: 2000
}
}
},
MuiPaper: {
styleOverrides: {
root: {
}
}
},
MuiPopover: {
defaultProps: {
}
},
MuiRadio: {
defaultProps: {
}
},
MuiSwitch: {
defaultProps: {
}
},
MuiTab: {
styleOverrides: {
root: {
}
}
},
MuiTableCell: {
styleOverrides: {
root: {
}
}
},
MuiTableHead: {
styleOverrides: {
root: {
}
}
}
},
shape: {
borderRadius: 8
},
typography: {
button: {
fontWeight: 600
},
fontFamily: '"Roboto+Slab:300,400,500&display=swap", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"',
body1: {
fontSize: '1.5rem',
fontWeight: 400,
lineHeight: 1.875,
},
body2: {
fontSize: '1.275rem',
fontWeight: 400,
lineHeight: 1.875
},
subtitle1: {
fontSize: '1rem',
fontWeight: 500,
lineHeight: 1.975
},
subtitle2: {
fontSize: '0.975rem',
fontWeight: 500,
lineHeight: 1.875
},
overline: {
fontSize: '0.75rem',
fontWeight: 600,
letterSpacing: '0.5px',
lineHeight: 2.5,
textTransform: 'uppercase'
},
caption: {
fontSize: '0.75rem',
fontWeight: 400,
lineHeight: 1.66
},
h1: {
fontWeight: 700,
fontSize: '2.875rem',
lineHeight: 1.375,
},
h2: {
fontWeight: 700,
fontSize: '2rem',
lineHeight: 1.375,
},
h3: {
fontWeight: 700,
fontSize: '1.875rem',
lineHeight: 1.375
},
h4: {
fontWeight: 700,
fontSize: '1.5rem',
lineHeight: 1.375
},
h5: {
fontWeight: 600,
fontSize: '1.25rem',
lineHeight: 1.375
},
h6: {
fontWeight: 600,
fontSize: '1.125rem',
lineHeight: 1.375
}
},
};
Under theme
sub-folder create light-theme-options.ts
:
import { ThemeOptions } from '@mui/material';
// Colors
const neutral = {
100: '#F5F5F5',
200: '#EEEEEE',
300: '#E0E0E0',
400: '#BDBDBD',
500: '#9E9E9E',
600: '#757575',
700: '#616161',
800: '#424242',
900: '#212121'
};
const background = {
default: '#212121',
paper: '#000',
};
const divider = 'rgba(255, 255, 255, 0.12)';
const primary = {
main: '#fff',
light: '#fff',
dark: '#fff',
contrastText: neutral[500]
};
const secondary = {
main: '#616161',
light: '#616161',
dark: '#424242',
contrastText: neutral[200]
};
const success = {
main: '#2e7d32',
light: '#4caf50',
dark: '#1b5e20',
contrastText: '#FFFFFF'
};
const info = {
main: '#0288d1',
light: '#03a9f4',
dark: '#01579b',
contrastText: '#FFFFFF'
};
const warning = {
main: '#ed6c02',
light: '#ff9800',
dark: '#e65100',
contrastText: '#FFFFFF'
};
const error = {
main: '#d32f2f',
light: '#ef5350',
dark: '#c62828',
contrastText: '#FFFFFF'
};
const text = {
primary: '#BDBDBD',
secondary: '#E0E0E0',
disabled: 'rgba(55, 65, 81, 0.48)'
};
export const darkThemeOptions: ThemeOptions = {
components: {
MuiAvatar: {
styleOverrides: {
root: {
backgroundColor: neutral[500],
color: '#FFFFFF'
}
}
},
MuiInputBase: {
styleOverrides: {
input: {
'&::placeholder': {
opacity: 1,
color: text.secondary
}
}
}
},
MuiOutlinedInput: {
styleOverrides: {
notchedOutline: {
borderColor: divider
}
}
},
MuiMenu: {
styleOverrides: {
paper: {
borderColor: divider,
borderStyle: 'solid',
borderWidth: 1
}
}
},
},
palette: {
action: {
active: neutral[400],
hover: '#616161',
selected: 'rgba(255, 255, 255, 0.08)',
disabledBackground: 'rgba(255, 255, 255, 0.12)',
disabled: 'rgba(255, 255, 255, 0.26)'
},
background,
divider,
error,
info,
mode: 'dark',
neutral,
primary,
secondary,
success,
text,
warning
},
};
Under theme
subfolder create dark-theme-options.ts
:
import { ThemeOptions } from '@mui/material';
// Colors
const neutral = {
100: '#F5F5F5',
200: '#EEEEEE',
300: '#E0E0E0',
400: '#BDBDBD',
500: '#9E9E9E',
600: '#757575',
700: '#616161',
800: '#424242',
900: '#212121'
};
const background = {
default: '#212121',
paper: '#000',
};
const divider = 'rgba(255, 255, 255, 0.12)';
const primary = {
main: '#fff',
light: '#fff',
dark: '#fff',
contrastText: neutral[500]
};
const secondary = {
main: '#616161',
light: '#616161',
dark: '#424242',
contrastText: neutral[200]
};
const success = {
main: '#2e7d32',
light: '#4caf50',
dark: '#1b5e20',
contrastText: '#FFFFFF'
};
const info = {
main: '#0288d1',
light: '#03a9f4',
dark: '#01579b',
contrastText: '#FFFFFF'
};
const warning = {
main: '#ed6c02',
light: '#ff9800',
dark: '#e65100',
contrastText: '#FFFFFF'
};
const error = {
main: '#d32f2f',
light: '#ef5350',
dark: '#c62828',
contrastText: '#FFFFFF'
};
const text = {
primary: '#BDBDBD',
secondary: '#E0E0E0',
disabled: 'rgba(55, 65, 81, 0.48)'
};
export const darkThemeOptions: ThemeOptions = {
components: {
MuiAvatar: {
styleOverrides: {
root: {
backgroundColor: neutral[500],
color: '#FFFFFF'
}
}
},
MuiInputBase: {
styleOverrides: {
input: {
'&::placeholder': {
opacity: 1,
color: text.secondary
}
}
}
},
MuiOutlinedInput: {
styleOverrides: {
notchedOutline: {
borderColor: divider
}
}
},
MuiMenu: {
styleOverrides: {
paper: {
borderColor: divider,
borderStyle: 'solid',
borderWidth: 1
}
}
},
},
palette: {
action: {
active: neutral[400],
hover: '#616161',
selected: 'rgba(255, 255, 255, 0.08)',
disabledBackground: 'rgba(255, 255, 255, 0.12)',
disabled: 'rgba(255, 255, 255, 0.26)'
},
background,
divider,
error,
info,
mode: 'dark',
neutral,
primary,
secondary,
success,
text,
warning
},
};
Under pages
sub-folder update _document.tsx
file:
import * as React from "react";
import Document, { Html, Head, Main, NextScript } from "next/document";
import createEmotionServer from "@emotion/server/create-instance";
import createEmotionCache from "../utility/createEmotionCache";
export default class MyDocument extends Document {
render() {
return (
<Html lang="en">
<Head>
<link
rel="apple-touch-icon"
sizes="180x180"
href="/apple-touch-icon.png"
/>
<link rel="icon" type="image/png" href="/favicon-16x16.png" />
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon-32x32.png"
/>
<link rel="icon" type="image/png" sizes="16x16" href="/favicon.ico" />
<meta name="theme-color" content="#000" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
// `getInitialProps` belongs to `_document` (instead of `_app`),
// it's compatible with static-site generation (SSG).
MyDocument.getInitialProps = async (ctx) => {
// Resolution order
//
// On the server:
// 1. app.getInitialProps
// 2. page.getInitialProps
// 3. document.getInitialProps
// 4. app.render
// 5. page.render
// 6. document.render
//
// On the server with error:
// 1. document.getInitialProps
// 2. app.render
// 3. page.render
// 4. document.render
//
// On the client
// 1. app.getInitialProps
// 2. page.getInitialProps
// 3. app.render
// 4. page.render
const originalRenderPage = ctx.renderPage;
// You can consider sharing the same emotion cache between all the SSR requests to speed up performance.
// However, be aware that it can have global side effects.
const cache = createEmotionCache();
const { extractCriticalToChunks } = createEmotionServer(cache);
/* eslint-disable */
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App: any) => (props) =>
<App emotionCache={cache} {...props} />,
});
/* eslint-enable */
const initialProps = await Document.getInitialProps(ctx);
// This is important. It prevents emotion to render invalid HTML.
// See https://github.com/mui-org/material-ui/issues/26561#issuecomment-855286153
const emotionStyles = extractCriticalToChunks(initialProps.html);
const emotionStyleTags = emotionStyles.styles.map((style) => (
<style
data-emotion={`${style.key} ${style.ids.join(" ")}`}
key={style.key}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: style.css }}
/>
));
return {
...initialProps,
// Styles fragment is rendered after the app and page rendering finish.
styles: [
...React.Children.toArray(initialProps.styles),
...emotionStyleTags,
],
};
};
Under pages
sub-folder update _app.tsx
:
import type { AppProps } from "next/app";
import "@fontsource/roboto/300.css";
import "@fontsource/roboto/400.css";
import "@fontsource/roboto/500.css";
import "@fontsource/roboto/700.css";
import { CacheProvider, EmotionCache } from "@emotion/react";
import createEmotionCache from "@/utility/createEmotionCache";
import { FC, useEffect } from "react";
import Head from "next/head";
import { CssBaseline, ThemeProvider } from "@mui/material";
import { createTheme } from "@/theme";
import { ReactNode } from "react";
import { NextPage } from "next";
type MyAppProps = AppProps & {
Component: NextPage;
emotionCache: EmotionCache;
};
const clientSideEmotionCache = createEmotionCache();
const MyApp: FC<MyAppProps> = (props) => {
const { Component, emotionCache = clientSideEmotionCache, pageProps } = props;
const getLayout = Component.getLayout || ((page: ReactNode) => page);
useEffect(() => {}, []);
return (
<CacheProvider value={emotionCache}>
<Head>
<meta name="viewport" content="initial-scale=1, width=device-width" />
</Head>
<ThemeProvider
theme={createTheme({
mode: "light",
})}
>
<CssBaseline />
{getLayout(<Component {...pageProps} />)}
</ThemeProvider>
</CacheProvider>
);
};
export default MyApp;
Under pages
sub-folder update index.tsx
:
import { HomePage } from "@/components/home";
import { MainLayout } from "@/components/main-layout";
import type { NextPage } from "next";
import Head from "next/head";
import {
ReactElement,
JSXElementConstructor,
ReactFragment,
ReactPortal,
} from "react";
const Home: NextPage = () => {
return (
<>
<Head>
<title>My app</title>
</Head>
<main>
<HomePage />
</main>
</>
);
};
Home.getLayout = (
page:
| string
| number
| boolean
| ReactElement<any, string | JSXElementConstructor<any>>
| ReactFragment
| ReactPortal
| null
| undefined
) => <MainLayout>{page}</MainLayout>;
export default Home;
Under root
folder create next.d.ts
:
// next.d.ts
import type {
NextComponentType,
NextPageContext,
NextLayoutComponentType,
} from 'next';
import type { AppProps } from 'next/app';
declare module 'next' {
type NextPage<P = {}> = NextComponentType<
NextPageContext,
any,
P
> & {
getLayout?: (page: ReactNode) => ReactNode;
};
}
declare module 'next/app' {
type AppLayoutProps<P = {}> = AppProps & {
Component: NextLayoutComponentType;
};
}