Building a Modern Next.js Project with TypeScript, Tailwind CSS, Material-UI
Next.js is a powerful React framework that enables server-side rendering, static site generation, and client-side rendering all in one. When combined with TypeScript for static typing, Tailwind CSS for utility-first styling, Material-UI for pre-built UI components, Context API for data fetching and state management, you can build robust, scalable, and maintainable web applications.
In this blog, we will set up a Next.js project using these technologies and build a simple example application.
Prerequisites
Before we start, ensure you have the following installed on your machine:
- Node.js
- npm or Yarn
Setting Up the Project
First, create a new Next.js project using the following command:
npx create-next-app@latest my-nextjs-app --typescript
# or
yarn create next-app my-nextjs-app --typescript
Following questions will be asked after you run the above command. Follow default settings and pressed enter:
Navigate to the project directory:
cd my-nextjs-app
Installing Tailwind CSS
Next, we will install Tailwind CSS:
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
# or
yarn add -D tailwindcss@latest postcss@latest autoprefixer@latest
Initialize Tailwind CSS:
npx tailwindcss init -p
This will generate tailwind.config.js
and postcss.config.js
files.
Configure tailwind.config.js
to include the paths to all of your template files:
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {},
},
plugins: [],
};
Create a CSS file and include Tailwind’s base, components, and utilities styles. Update styles/globals.css
as follows:
@tailwind base;
@tailwind components;
@tailwind utilities;
Installing Material-UI
Now, let’s add Material-UI to our project:
npm install @mui/material @emotion/react @emotion/styled
# or
yarn add @mui/material @emotion/react @emotion/styled
Debugging errors: in case you find the following error
Steps to resolve the above error
If you don’t specifically need React 19 release candidate, consider downgrading React to a compatible version, like the latest stable React 18.
- Uninstall the Current React Version:
npm uninstall react react-dom
- Install a Compatible Version of React:
npm install react@18 react-dom@18
- Reattempt MUI Installation:
npm install @mui/material @emotion/react @emotion/styled
This should resolve the compatibility issue since React 18 is fully supported by @mui/material
.
Project Structure using App Router
To keep our project organised, we will follow a simple structure:
project-root/
│
├── app/
│ ├── api/
│ │ └── users/
│ │ └── route.ts # API route for users (GET request)
│ │
│ ├── components/
│ │ └── UserList.tsx # UserList component to display users
│ │
│ ├── page.tsx # Home page component (Root route)
│ │
│ ├── layout.tsx # Root layout for wrapping all pages
│ └── providers.tsx # Global provider for UserProvider
│
├── components/
│ └── Layout.tsx # Layout component (optional for consistent page elements like header, footer, etc.)
│
├── context/
│ └── UserContext.tsx # User context to manage global user state
│
├── pages/
│ └── api/ # Default Next.js folder for API routes if needed (you can ignore if not using)
│
├── public/
│ └── ... # Static assets like images, icons, etc.
│
├── styles/
│ └── globals.css # Global CSS file with Tailwind directives
│
├── tailwind.config.js # Tailwind CSS configuration file
├── postcss.config.js # PostCSS configuration file for Tailwind
├── package.json # Project dependencies and scripts
└── tsconfig.json # TypeScript configuration
Detailed Explanation:
app/
Directory:
app/api/users/route.ts
: Defines an API endpoint for retrieving the list of users (GET
handler).app/components/UserList.tsx
: The component that fetches and displays the list of users, styled using Tailwind CSS.app/page.tsx
: This is the root page, equivalent to the home page (/
). It renders the main content, such asUserList
.app/layout.tsx
: The layout component that wraps around all other components, providing consistent elements such as headers and footers.app/providers.tsx
: A dedicated file to manage global providers, likeUserProvider
(context).
components/
Directory:
components/Layout.tsx
: Defines consistent UI elements like headers, footers, or navigation that are used across different pages. This is used inlayout.tsx
if a consistent UI layout is desired.
context/
Directory:
context/UserContext.tsx
: Implements theUserProvider
for managing global user state and exposing context to components.
styles/
Directory:
styles/globals.css
: Contains the global CSS for the application, including the Tailwind directives (@tailwind base;
,@tailwind components;
,@tailwind utilities;
).
Configuration Files:
tailwind.config.js
: Tailwind configuration file, specifying paths to scan for class names.postcss.config.js
: Configures PostCSS to work with Tailwind.tsconfig.json
: TypeScript configuration to ensure type safety.
Creating the Context API
Let’s create a Context API for managing global state. Create a new file context/UserContext.tsx
:
import React, { createContext, useState, useContext, ReactNode } from 'react';
interface User {
id: number;
name: string;
email: string;
}
interface UserContextType {
users: User[];
setUsers: React.Dispatch<React.SetStateAction<User[]>>;
}
const UserContext = createContext<UserContextType | undefined>(undefined);
export const UserProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [users, setUsers] = useState<User[]>([]);
return (
<UserContext.Provider value={{ users, setUsers }}>
{children}
</UserContext.Provider>
);
};
export const useUserContext = () => {
const context = useContext(UserContext);
if (context === undefined) {
throw new Error('useUserContext must be used within a UserProvider');
}
return context;
};
Set Up Global Providers
- To adapt to the App Router, you need to create a global provider.
- Create a
providers.tsx
file in theapp
directory where you can add all your providers likeUserProvider
.
Create a Providers Component in the App Directory:
'use client';
import React from 'react';
import { UserProvider } from '../context/UserContext';
interface ProvidersProps {
children: React.ReactNode;
}
export function Providers({ children }: ProvidersProps) {
return (
<UserProvider>{children}</UserProvider>
);
}
Update Layout to Use Providers
- The App Router uses a
layout.tsx
file to wrap all pages inside a specific segment. - Create or edit the
layout.tsx
file in theapp
directory to use yourProviders
component.
Create a Layout Component in the App Directory:
import type { Metadata } from "next";
import localFont from "next/font/local";
import '../styles/global.css';
const geistSans = localFont({
src: "./fonts/GeistVF.woff",
variable: "--font-geist-sans",
weight: "100 900",
});
const geistMono = localFont({
src: "./fonts/GeistMonoVF.woff",
variable: "--font-geist-mono",
weight: "100 900",
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}
Mock API End Points
First, set up a mock API endpoint. Create a file app/api/users/route.ts
:
// app/api/users/route.ts
import { NextResponse } from 'next/server';
type User = {
id: number;
name: string;
email: string;
};
const users: User[] = [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Doe', email: 'jane@example.com' },
];
export async function GET() {
return NextResponse.json(users);
}
Now, create the UserList
component in app/components/UserList.tsx
:
// app/components/UserList.tsx
'use client';
import React, { useEffect, useState } from 'react';
type User = {
id: number;
name: string;
email: string;
};
export default function UserList() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchUsers = async () => {
try {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error('Failed to fetch users');
}
const data: User[] = await response.json();
setUsers(data);
} catch (error) {
setError((error as Error).message);
} finally {
setLoading(false);
}
};
fetchUsers();
}, []);
if (loading) {
return <div className="text-center text-xl font-medium p-8">Loading...</div>;
}
if (error) {
return <div className="text-center text-xl text-red-500 p-8">Error: {error}</div>;
}
return (
<div className="max-w-4xl mx-auto p-6 bg-gradient-to-r from-blue-50 to-blue-100 rounded-xl shadow-lg">
<h1 className="text-3xl font-bold text-gray-800 text-center mb-8">User List</h1>
<ul className="grid grid-cols-1 md:grid-cols-2 gap-6">
{users.map((user) => (
<li key={user.id} className="flex flex-col p-6 bg-white rounded-lg shadow-md hover:shadow-xl transition-shadow duration-300 ease-in-out transform hover:-translate-y-1">
<div className="flex items-center space-x-4 mb-4">
<div className="w-10 h-10 rounded-full bg-gradient-to-r from-purple-500 to-indigo-500 flex items-center justify-center text-white text-lg font-semibold">
{user.name.charAt(0).toUpperCase()}
</div>
<div className="flex-1">
<h2 className="text-lg font-semibold text-gray-800">{user.name}</h2>
<p className="text-gray-500">{user.email}</p>
</div>
</div>
<button className="mt-auto self-start px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors duration-300">
View Profile
</button>
</li>
))}
</ul>
</div>
);
}
Creating the Home Page
Finally, create the home page. Update app/page.tsx
:
import React from 'react';
import UserList from './components/UserList';
import { AppBar, Container, Toolbar, Typography } from '@mui/material';
const Home: React.FC = () => {
return (
<div>
<AppBar position="static">
<Toolbar>
<Typography variant="h6">
Next.js with TypeScript, Tailwind CSS, Material-UI and Context API
</Typography>
</Toolbar>
</AppBar>
<Container><UserList /></Container>
</div>
);
};
export default Home;
Styling with Tailwind CSS
We already included Tailwind’s base styles in globals.css
. Now, you can use Tailwind CSS classes to style your components. For instance, in pages/index.tsx
, we used text-3xl
, font-bold
, and underline
to style the heading.
Running the Project
Now, you can run your Next.js project:
npm run dev
# or
yarn dev
Open your browser and navigate to http://localhost:3000
to see your application in action.
Demo UI
In Next.js 14 with the App Router, the directory structure and way to manage providers like context and layout is different compared to the pages
router structure. Here's how you can adapt your UserContext
, and Layout
in the context of the App Router.
With the new App Router, global components such as contexts, providers, and layouts are defined in a more modular way, and this structure requires using layout.tsx
files and shared context providers for application-level contexts.
Conclusion
In this blog, we have set up a Next.js project using TypeScript, Tailwind CSS, Material-UI, and the Context API. We created a simple layout, fetched data from an API, and used the Context API to manage global state. This setup provides a solid foundation for building modern, scalable web applications with a great developer experience.