HomeBlog Posts
Integrating Material-UI for Beautiful, Consistent Design

Integrating Material-UI for Beautiful, Consistent Design

2025-07-21 by Remi Kristelijn

Integrating Material-UI for Beautiful, Consistent Design

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.

Why Material-UI?

Material-UI provides:

  • Consistent Design: Follows Material Design principles
  • Rich Component Library: Pre-built, accessible components
  • Customizable Theming: Easy to adapt to your brand
  • TypeScript Support: Full type safety
  • Performance: Optimized for React applications

Step 1: Install Material-UI Dependencies

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 icons

Step 2: Set Up Theme Configuration

Create 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});

Step 3: Create Theme Registry

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}

Step 4: Update Root Layout

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}

Step 5: Create Navigation Component

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}

Step 6: Update Home Page

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}

Step 7: Create Home Page Components

Header Component

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}

Hero Component

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}

Features Component

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}

Footer Component

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}

Step 8: Update Blog Components

Enhanced PostCard

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}

Enhanced PostContent

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}

Step 9: Add Error Boundary

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&apos;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}

Step 10: Test Your Material-UI Integration

  1. Start your development server:

    1npm run dev
  2. Visit your blog to see the beautiful Material-UI design

  3. Test navigation between pages

  4. Verify that all components render correctly

Benefits of Material-UI Integration

  1. Professional Appearance: Consistent, modern design
  2. Accessibility: Built-in accessibility features
  3. Responsive Design: Works on all screen sizes
  4. Customizable: Easy to adapt to your brand
  5. Performance: Optimized for React applications

What's Next?

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.

Troubleshooting

Common Issues

  1. Styling Conflicts: Ensure Emotion is properly configured
  2. Server-Side Rendering: Use ThemeRegistry for SSR compatibility
  3. TypeScript Errors: Check that all MUI types are imported correctly
  4. Performance: Monitor bundle size and optimize imports

Resources


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.