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:

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 icons

Step 2: Set Up Theme Configuration

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

Step 3: Create Theme Registry

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

Step 4: Update Root Layout

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

Step 5: Create Navigation Component

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

Step 6: Update Home Page

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

Step 7: Create Home Page Components

Header Component

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

Hero Component

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

Features Component

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

Footer Component

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

Step 8: Update Blog Components

Enhanced PostCard

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

Enhanced PostContent

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

Step 9: Add Error Boundary

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&apos;re sorry, but something unexpected happened.
          </Typography>
          <Button
            variant="contained"
            onClick={() => window.location.reload()}
          >
            Reload Page
          </Button>
        </Box>
      );
    }

    return this.props.children;
  }
}

Step 10: Test Your Material-UI Integration

  1. Start your development server:

    npm 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.