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:
1npm 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:
1import { createTheme } from '@mui/material/styles'; 2 3export const theme = createTheme({ 4 palette: { 5 primary: { 6 main: '#1976d2', 7 }, 8 secondary: { 9 main: '#dc004e', 10 }, 11 }, 12 typography: { 13 fontFamily: [ 14 '-apple-system', 15 'BlinkMacSystemFont', 16 '"Segoe UI"', 17 'Roboto', 18 '"Helvetica Neue"', 19 'Arial', 20 'sans-serif', 21 ].join(','), 22 }, 23 components: { 24 MuiButton: { 25 styleOverrides: { 26 root: { 27 textTransform: 'none', 28 }, 29 }, 30 }, 31 }, 32});
Create src/components/ThemeRegistry.tsx for server-side rendering compatibility:
1'use client'; 2 3import createCache from '@emotion/cache'; 4import { useServerInsertedHTML } from 'next/navigation'; 5import { CacheProvider } from '@emotion/react'; 6import { ThemeProvider } from '@mui/material/styles'; 7import CssBaseline from '@mui/material/CssBaseline'; 8import { theme } from '@/lib/theme'; 9import { useState } from 'react'; 10 11export default function ThemeRegistry({ children }: { children: React.ReactNode }) { 12 const [{ cache, flush }] = useState(() => { 13 const cache = createCache({ key: 'mui' }); 14 cache.compat = true; 15 const prevInsert = cache.insert; 16 let inserted: string[] = []; 17 cache.insert = (...args) => { 18 const serialized = args[1]; 19 if (cache.inserted[serialized.name] === undefined) { 20 inserted.push(serialized.name); 21 } 22 return prevInsert(...args); 23 }; 24 const flush = () => { 25 const prevInserted = inserted; 26 inserted = []; 27 return prevInserted; 28 }; 29 return { cache, flush }; 30 }); 31 32 useServerInsertedHTML(() => { 33 const names = flush(); 34 if (names.length === 0) { 35 return null; 36 } 37 let styles = ''; 38 for (const name of names) { 39 styles += cache.inserted[name]; 40 } 41 return ( 42 <style 43 key={cache.key} 44 data-emotion={`${cache.key} ${names.join(' ')}`} 45 dangerouslySetInnerHTML={{ 46 __html: styles, 47 }} 48 /> 49 ); 50 }); 51 52 return ( 53 <CacheProvider value={cache}> 54 <ThemeProvider theme={theme}> 55 <CssBaseline /> 56 {children} 57 </ThemeProvider> 58 </CacheProvider> 59 ); 60}
Modify src/app/layout.tsx to include the theme registry:
1import type { Metadata } from "next"; 2import { Geist, Geist_Mono } from "next/font/google"; 3import ThemeRegistry from '@/components/ThemeRegistry'; 4import ErrorBoundary from '@/components/ErrorBoundary'; 5 6const geistSans = Geist({ 7 variable: "--font-geist-sans", 8 subsets: ["latin"], 9}); 10 11const geistMono = Geist_Mono({ 12 variable: "--font-geist-mono", 13 subsets: ["latin"], 14}); 15 16export const metadata: Metadata = { 17 title: "Next.js Blog", 18 description: "A modern blog built with Next.js and Material-UI", 19}; 20 21export default function RootLayout({ 22 children, 23}: Readonly<{ 24 children: React.ReactNode; 25}>) { 26 return ( 27 <html lang="en"> 28 <head> 29 <meta name="emotion-insertion-point" content="" /> 30 </head> 31 <body className={`${geistSans.variable} ${geistMono.variable}`}> 32 <ThemeRegistry> 33 <ErrorBoundary> 34 {children} 35 </ErrorBoundary> 36 </ThemeRegistry> 37 </body> 38 </html> 39 ); 40}
Build src/components/Navigation.tsx for consistent header navigation:
1import Link from 'next/link'; 2import { AppBar, Toolbar, Button, Typography } from '@mui/material'; 3import { ArrowBack as ArrowBackIcon, Home as HomeIcon } from '@mui/icons-material'; 4import type { NavigationProps } from '@/types'; 5 6interface ExtendedNavigationProps extends NavigationProps { 7 showBlogPosts?: boolean; 8} 9 10export default function Navigation({ 11 title, 12 showHome = true, 13 showBack = false, 14 showBlogPosts = false 15}: ExtendedNavigationProps) { 16 return ( 17 <AppBar position="static" color="default" elevation={1}> 18 <Toolbar> 19 {showHome && ( 20 <Button 21 color="inherit" 22 component={Link} 23 href="/" 24 startIcon={<HomeIcon />} 25 > 26 Home 27 </Button> 28 )} 29 30 {showBack && ( 31 <Button 32 color="inherit" 33 component={Link} 34 href="/posts" 35 startIcon={<ArrowBackIcon />} 36 > 37 Blog Posts 38 </Button> 39 )} 40 41 {showBlogPosts && ( 42 <Button 43 color="inherit" 44 component={Link} 45 href="/posts" 46 sx={{ ml: 'auto' }} 47 > 48 Blog Posts 49 </Button> 50 )} 51 52 {title && ( 53 <Typography variant="h6" component="div" sx={{ flexGrow: 1, ml: 2 }}> 54 {title} 55 </Typography> 56 )} 57 </Toolbar> 58 </AppBar> 59 ); 60}
Transform src/app/page.tsx with Material-UI components:
1import { Box } from '@mui/material'; 2import Header from '@/components/Header'; 3import Hero from '@/components/Hero'; 4import Features from '@/components/Features'; 5import Footer from '@/components/Footer'; 6 7export default function Home() { 8 return ( 9 <Box sx={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}> 10 <Header title="Next.js Blog" showBlogPostsButton={true} /> 11 <Hero /> 12 <Features /> 13 <Footer /> 14 </Box> 15 ); 16}
Create src/components/Header.tsx:
1import { AppBar, Toolbar, Button, Typography } from '@mui/material'; 2import Link from 'next/link'; 3 4interface HeaderProps { 5 title: string; 6 showBlogPostsButton?: boolean; 7} 8 9export default function Header({ title, showBlogPostsButton = false }: HeaderProps) { 10 return ( 11 <AppBar position="static" color="default" elevation={1}> 12 <Toolbar> 13 <Typography variant="h6" component="div" sx={{ flexGrow: 1 }}> 14 {title} 15 </Typography> 16 {showBlogPostsButton && ( 17 <Button 18 color="inherit" 19 component={Link} 20 href="/posts" 21 > 22 Blog Posts 23 </Button> 24 )} 25 </Toolbar> 26 </AppBar> 27 ); 28}
Create src/components/Hero.tsx:
1import { Box, Container, Typography, Button, Stack } from '@mui/material'; 2import Image from 'next/image'; 3import Link from 'next/link'; 4 5export default function Hero() { 6 return ( 7 <Box 8 sx={{ 9 bgcolor: 'background.paper', 10 pt: 8, 11 pb: 6, 12 }} 13 > 14 <Container maxWidth="sm"> 15 <Typography 16 component="h1" 17 variant="h2" 18 align="center" 19 color="text.primary" 20 gutterBottom 21 > 22 Welcome to My Blog 23 </Typography> 24 <Typography variant="h5" align="center" color="text.secondary" paragraph> 25 A modern blog built with Next.js 15, Material-UI, and MDX. 26 Explore articles about web development, technology, and more. 27 </Typography> 28 <Stack 29 sx={{ pt: 4 }} 30 direction="row" 31 spacing={2} 32 justifyContent="center" 33 > 34 <Button component={Link} href="/posts" variant="contained"> 35 Read Blog Posts 36 </Button> 37 <Button component={Link} href="/posts" variant="outlined"> 38 Learn More 39 </Button> 40 </Stack> 41 </Container> 42 </Box> 43 ); 44}
Create src/components/Features.tsx:
1import { Container, Grid, Card, CardContent, Typography } from '@mui/material'; 2import { Code, Speed, Palette } from '@mui/icons-material'; 3 4const features = [ 5 { 6 title: 'Modern Tech Stack', 7 description: 'Built with Next.js 15, TypeScript, and Material-UI for a robust foundation.', 8 icon: <Code fontSize="large" color="primary" />, 9 }, 10 { 11 title: 'Fast Performance', 12 description: 'Optimized for speed with static generation and Cloudflare CDN.', 13 icon: <Speed fontSize="large" color="primary" />, 14 }, 15 { 16 title: 'Beautiful Design', 17 description: 'Consistent, accessible design using Material Design principles.', 18 icon: <Palette fontSize="large" color="primary" />, 19 }, 20]; 21 22export default function Features() { 23 return ( 24 <Container sx={{ py: 8 }} maxWidth="md"> 25 <Grid container spacing={4}> 26 {features.map((feature, index) => ( 27 <Grid item key={index} xs={12} sm={6} md={4}> 28 <Card 29 sx={{ 30 height: '100%', 31 display: 'flex', 32 flexDirection: 'column', 33 textAlign: 'center', 34 }} 35 > 36 <CardContent sx={{ flexGrow: 1 }}> 37 <Box sx={{ mb: 2 }}> 38 {feature.icon} 39 </Box> 40 <Typography gutterBottom variant="h5" component="h2"> 41 {feature.title} 42 </Typography> 43 <Typography> 44 {feature.description} 45 </Typography> 46 </CardContent> 47 </Card> 48 </Grid> 49 ))} 50 </Grid> 51 </Container> 52 ); 53}
Create src/components/Footer.tsx:
1import { Box, Container, Stack, Button, Typography } from '@mui/material'; 2import { GitHub, Twitter, LinkedIn } from '@mui/icons-material'; 3 4export default function Footer() { 5 return ( 6 <Box 7 component="footer" 8 sx={{ 9 py: 3, 10 px: 2, 11 mt: 'auto', 12 backgroundColor: (theme) => 13 theme.palette.mode === 'light' 14 ? theme.palette.grey[200] 15 : theme.palette.grey[800], 16 }} 17 > 18 <Container maxWidth="sm"> 19 <Typography variant="body1" align="center"> 20 Built with Next.js and Material-UI 21 </Typography> 22 <Stack 23 direction="row" 24 spacing={2} 25 justifyContent="center" 26 sx={{ mt: 2 }} 27 > 28 <Button 29 component="a" 30 href="https://github.com" 31 target="_blank" 32 rel="noopener noreferrer" 33 startIcon={<GitHub />} 34 size="small" 35 > 36 GitHub 37 </Button> 38 <Button 39 component="a" 40 href="https://twitter.com" 41 target="_blank" 42 rel="noopener noreferrer" 43 startIcon={<Twitter />} 44 size="small" 45 > 46 Twitter 47 </Button> 48 <Button 49 component="a" 50 href="https://linkedin.com" 51 target="_blank" 52 rel="noopener noreferrer" 53 startIcon={<LinkedIn />} 54 size="small" 55 > 56 LinkedIn 57 </Button> 58 </Stack> 59 </Container> 60 </Box> 61 ); 62}
Update src/components/PostCard.tsx with better styling:
1import Link from 'next/link'; 2import { Card, CardContent, Typography, Chip, Box } from '@mui/material'; 3import { CalendarToday } from '@mui/icons-material'; 4import type { PostCardProps } from '@/types'; 5 6export default function PostCard({ post }: PostCardProps) { 7 return ( 8 <Card 9 component={Link} 10 href={`/posts/${post.slug}`} 11 sx={{ 12 textDecoration: 'none', 13 transition: 'transform 0.2s ease-in-out', 14 '&:hover': { 15 transform: 'translateY(-2px)', 16 }, 17 }} 18 > 19 <CardContent> 20 <Typography variant="h5" component="h2" gutterBottom> 21 {post.title} 22 </Typography> 23 <Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}> 24 <CalendarToday sx={{ fontSize: 16, mr: 0.5 }} /> 25 <Typography variant="body2" color="text.secondary"> 26 {new Date(post.date).toLocaleDateString()} 27 </Typography> 28 </Box> 29 <Typography variant="body1" color="text.secondary"> 30 {post.excerpt} 31 </Typography> 32 </CardContent> 33 </Card> 34 ); 35}
Update src/components/PostContent.tsx with better typography:
1import { Typography, Box, Paper, Chip } from '@mui/material'; 2import { CalendarToday } from '@mui/icons-material'; 3import ReactMarkdown from 'react-markdown'; 4import type { PostContentProps } from '@/types'; 5 6export default function PostContent({ post }: PostContentProps) { 7 return ( 8 <Paper sx={{ p: 4 }}> 9 <Typography variant="h3" component="h1" gutterBottom> 10 {post.title} 11 </Typography> 12 <Box sx={{ display: 'flex', alignItems: 'center', mb: 3 }}> 13 <CalendarToday sx={{ fontSize: 18, mr: 1 }} /> 14 <Typography variant="body2" color="text.secondary"> 15 {new Date(post.date).toLocaleDateString()} 16 </Typography> 17 </Box> 18 <Box sx={{ mt: 4 }}> 19 <ReactMarkdown>{post.content}</ReactMarkdown> 20 </Box> 21 </Paper> 22 ); 23}
Create src/components/ErrorBoundary.tsx for better error handling:
1'use client'; 2 3import React from 'react'; 4import { Box, Typography, Button } from '@mui/material'; 5 6interface ErrorBoundaryState { 7 hasError: boolean; 8} 9 10export default class ErrorBoundary extends React.Component< 11 { children: React.ReactNode }, 12 ErrorBoundaryState 13> { 14 constructor(props: { children: React.ReactNode }) { 15 super(props); 16 this.state = { hasError: false }; 17 } 18 19 static getDerivedStateFromError(): ErrorBoundaryState { 20 return { hasError: true }; 21 } 22 23 componentDidCatch(error: unknown, errorInfo: unknown) { 24 console.error('Error caught by boundary:', error, errorInfo); 25 } 26 27 render() { 28 if (this.state.hasError) { 29 return ( 30 <Box 31 sx={{ 32 display: 'flex', 33 flexDirection: 'column', 34 alignItems: 'center', 35 justifyContent: 'center', 36 minHeight: '100vh', 37 p: 3, 38 }} 39 > 40 <Typography variant="h4" gutterBottom> 41 Something went wrong 42 </Typography> 43 <Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}> 44 We're sorry, but something unexpected happened. 45 </Typography> 46 <Button 47 variant="contained" 48 onClick={() => window.location.reload()} 49 > 50 Reload Page 51 </Button> 52 </Box> 53 ); 54 } 55 56 return this.props.children; 57 } 58}
Start your development server:
1npm 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.