2025-07-21 by Remi Kristelijn
In this fourth post of our series, I'll show you how to integrate Material-UI (MUI) into your Next.js blog. We'll replace the basic HTML with beautiful, consistent UI components and create a professional design system.
Material-UI provides:
Add MUI and its peer dependencies:
npm install @mui/material @emotion/react @emotion/styled @mui/icons-material
Package explanations:
@mui/material: Core Material-UI components@emotion/react & @emotion/styled: Styling engine@mui/icons-material: Material Design iconsCreate src/lib/theme.ts to define your design system:
import { createTheme } from '@mui/material/styles'; export const theme = createTheme({ palette: { primary: { main: '#1976d2', }, secondary: { main: '#dc004e', }, }, typography: { fontFamily: [ '-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'Roboto', '"Helvetica Neue"', 'Arial', 'sans-serif', ].join(','), }, components: { MuiButton: { styleOverrides: { root: { textTransform: 'none', }, }, }, }, });
Create src/components/ThemeRegistry.tsx for server-side rendering compatibility:
'use client'; import createCache from '@emotion/cache'; import { useServerInsertedHTML } from 'next/navigation'; import { CacheProvider } from '@emotion/react'; import { ThemeProvider } from '@mui/material/styles'; import CssBaseline from '@mui/material/CssBaseline'; import { theme } from '@/lib/theme'; import { useState } from 'react'; export default function ThemeRegistry({ children }: { children: React.ReactNode }) { const [{ cache, flush }] = useState(() => { const cache = createCache({ key: 'mui' }); 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}> <CssBaseline /> {children} </ThemeProvider> </CacheProvider> ); }
Modify src/app/layout.tsx to include the theme registry:
import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import ThemeRegistry from '@/components/ThemeRegistry'; import ErrorBoundary from '@/components/ErrorBoundary'; const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"], }); const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"], }); export const metadata: Metadata = { title: "Next.js Blog", description: "A modern blog built with Next.js and Material-UI", }; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( <html lang="en"> <head> <meta name="emotion-insertion-point" content="" /> </head> <body className={`${geistSans.variable} ${geistMono.variable}`}> <ThemeRegistry> <ErrorBoundary> {children} </ErrorBoundary> </ThemeRegistry> </body> </html> ); }
Build src/components/Navigation.tsx for consistent header navigation:
import Link from 'next/link'; import { AppBar, Toolbar, Button, Typography } from '@mui/material'; import { ArrowBack as ArrowBackIcon, Home as HomeIcon } from '@mui/icons-material'; import type { NavigationProps } from '@/types'; interface ExtendedNavigationProps extends NavigationProps { showBlogPosts?: boolean; } export default function Navigation({ title, showHome = true, showBack = false, showBlogPosts = false }: ExtendedNavigationProps) { return ( <AppBar position="static" color="default" elevation={1}> <Toolbar> {showHome && ( <Button color="inherit" component={Link} href="/" startIcon={<HomeIcon />} > Home </Button> )} {showBack && ( <Button color="inherit" component={Link} href="/posts" startIcon={<ArrowBackIcon />} > Blog Posts </Button> )} {showBlogPosts && ( <Button color="inherit" component={Link} href="/posts" sx={{ ml: 'auto' }} > Blog Posts </Button> )} {title && ( <Typography variant="h6" component="div" sx={{ flexGrow: 1, ml: 2 }}> {title} </Typography> )} </Toolbar> </AppBar> ); }
Transform src/app/page.tsx with Material-UI components:
import { Box } from '@mui/material'; import Header from '@/components/Header'; import Hero from '@/components/Hero'; import Features from '@/components/Features'; import Footer from '@/components/Footer'; export default function Home() { return ( <Box sx={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}> <Header title="Next.js Blog" showBlogPostsButton={true} /> <Hero /> <Features /> <Footer /> </Box> ); }
Create src/components/Header.tsx:
import { AppBar, Toolbar, Button, Typography } from '@mui/material'; import Link from 'next/link'; interface HeaderProps { title: string; showBlogPostsButton?: boolean; } export default function Header({ title, showBlogPostsButton = false }: HeaderProps) { return ( <AppBar position="static" color="default" elevation={1}> <Toolbar> <Typography variant="h6" component="div" sx={{ flexGrow: 1 }}> {title} </Typography> {showBlogPostsButton && ( <Button color="inherit" component={Link} href="/posts" > Blog Posts </Button> )} </Toolbar> </AppBar> ); }
Create src/components/Hero.tsx:
import { Box, Container, Typography, Button, Stack } from '@mui/material'; import Image from 'next/image'; import Link from 'next/link'; export default function Hero() { return ( <Box sx={{ bgcolor: 'background.paper', pt: 8, pb: 6, }} > <Container maxWidth="sm"> <Typography component="h1" variant="h2" align="center" color="text.primary" gutterBottom > Welcome to My Blog </Typography> <Typography variant="h5" align="center" color="text.secondary" paragraph> A modern blog built with Next.js 15, Material-UI, and MDX. Explore articles about web development, technology, and more. </Typography> <Stack sx={{ pt: 4 }} direction="row" spacing={2} justifyContent="center" > <Button component={Link} href="/posts" variant="contained"> Read Blog Posts </Button> <Button component={Link} href="/posts" variant="outlined"> Learn More </Button> </Stack> </Container> </Box> ); }
Create src/components/Features.tsx:
import { Container, Grid, Card, CardContent, Typography } from '@mui/material'; import { Code, Speed, Palette } from '@mui/icons-material'; const features = [ { title: 'Modern Tech Stack', description: 'Built with Next.js 15, TypeScript, and Material-UI for a robust foundation.', icon: <Code fontSize="large" color="primary" />, }, { title: 'Fast Performance', description: 'Optimized for speed with static generation and Cloudflare CDN.', icon: <Speed fontSize="large" color="primary" />, }, { title: 'Beautiful Design', description: 'Consistent, accessible design using Material Design principles.', icon: <Palette fontSize="large" color="primary" />, }, ]; export default function Features() { return ( <Container sx={{ py: 8 }} maxWidth="md"> <Grid container spacing={4}> {features.map((feature, index) => ( <Grid item key={index} xs={12} sm={6} md={4}> <Card sx={{ height: '100%', display: 'flex', flexDirection: 'column', textAlign: 'center', }} > <CardContent sx={{ flexGrow: 1 }}> <Box sx={{ mb: 2 }}> {feature.icon} </Box> <Typography gutterBottom variant="h5" component="h2"> {feature.title} </Typography> <Typography> {feature.description} </Typography> </CardContent> </Card> </Grid> ))} </Grid> </Container> ); }
Create src/components/Footer.tsx:
import { Box, Container, Stack, Button, Typography } from '@mui/material'; import { GitHub, Twitter, LinkedIn } from '@mui/icons-material'; export default function Footer() { return ( <Box component="footer" sx={{ py: 3, px: 2, mt: 'auto', backgroundColor: (theme) => theme.palette.mode === 'light' ? theme.palette.grey[200] : theme.palette.grey[800], }} > <Container maxWidth="sm"> <Typography variant="body1" align="center"> Built with Next.js and Material-UI </Typography> <Stack direction="row" spacing={2} justifyContent="center" sx={{ mt: 2 }} > <Button component="a" href="https://github.com" target="_blank" rel="noopener noreferrer" startIcon={<GitHub />} size="small" > GitHub </Button> <Button component="a" href="https://twitter.com" target="_blank" rel="noopener noreferrer" startIcon={<Twitter />} size="small" > Twitter </Button> <Button component="a" href="https://linkedin.com" target="_blank" rel="noopener noreferrer" startIcon={<LinkedIn />} size="small" > LinkedIn </Button> </Stack> </Container> </Box> ); }
Update src/components/PostCard.tsx with better styling:
import Link from 'next/link'; import { Card, CardContent, Typography, Chip, Box } from '@mui/material'; import { CalendarToday } from '@mui/icons-material'; import type { PostCardProps } from '@/types'; export default function PostCard({ post }: PostCardProps) { return ( <Card component={Link} href={`/posts/${post.slug}`} sx={{ textDecoration: 'none', transition: 'transform 0.2s ease-in-out', '&:hover': { transform: 'translateY(-2px)', }, }} > <CardContent> <Typography variant="h5" component="h2" gutterBottom> {post.title} </Typography> <Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}> <CalendarToday sx={{ fontSize: 16, mr: 0.5 }} /> <Typography variant="body2" color="text.secondary"> {new Date(post.date).toLocaleDateString()} </Typography> </Box> <Typography variant="body1" color="text.secondary"> {post.excerpt} </Typography> </CardContent> </Card> ); }
Update src/components/PostContent.tsx with better typography:
import { Typography, Box, Paper, Chip } from '@mui/material'; import { CalendarToday } from '@mui/icons-material'; import ReactMarkdown from 'react-markdown'; import type { PostContentProps } from '@/types'; export default function PostContent({ post }: PostContentProps) { return ( <Paper sx={{ p: 4 }}> <Typography variant="h3" component="h1" gutterBottom> {post.title} </Typography> <Box sx={{ display: 'flex', alignItems: 'center', mb: 3 }}> <CalendarToday sx={{ fontSize: 18, mr: 1 }} /> <Typography variant="body2" color="text.secondary"> {new Date(post.date).toLocaleDateString()} </Typography> </Box> <Box sx={{ mt: 4 }}> <ReactMarkdown>{post.content}</ReactMarkdown> </Box> </Paper> ); }
Create src/components/ErrorBoundary.tsx for better error handling:
'use client'; import React from 'react'; import { Box, Typography, Button } from '@mui/material'; interface ErrorBoundaryState { hasError: boolean; } export default class ErrorBoundary extends React.Component< { children: React.ReactNode }, ErrorBoundaryState > { constructor(props: { children: React.ReactNode }) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(): ErrorBoundaryState { return { hasError: true }; } componentDidCatch(error: unknown, errorInfo: unknown) { console.error('Error caught by boundary:', error, errorInfo); } render() { if (this.state.hasError) { return ( <Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', p: 3, }} > <Typography variant="h4" gutterBottom> Something went wrong </Typography> <Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}> We're sorry, but something unexpected happened. </Typography> <Button variant="contained" onClick={() => window.location.reload()} > Reload Page </Button> </Box> ); } return this.props.children; } }
Start your development server:
npm run dev
Visit your blog to see the beautiful Material-UI design
Test navigation between pages
Verify that all components render correctly
In the final post, we'll optimize the code by applying the coding principles from rules.md. We'll refactor components, improve type safety, and ensure the code follows best practices.
Your Next.js blog now has a beautiful, professional design with Material-UI! The interface is consistent, accessible, and modern. In the final post, we'll optimize the code quality and apply best practices.