Next.js Project with TypeScript, Tailwind CSS, Material-UI

Building a Modern Next.js Project with TypeScript, Tailwind CSS, Material-UI

Meenu Matharu
7 min readNov 14, 2024

--

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.

  1. Uninstall the Current React Version:
npm uninstall react react-dom
  1. Install a Compatible Version of React:
npm install react@18 react-dom@18
  1. 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 as UserList.
  • 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, like UserProvider (context).

components/ Directory:

  • components/Layout.tsx: Defines consistent UI elements like headers, footers, or navigation that are used across different pages. This is used in layout.tsx if a consistent UI layout is desired.

context/ Directory:

  • context/UserContext.tsx: Implements the UserProvider 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 the app directory where you can add all your providers like UserProvider.

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 the app directory to use your Providers 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.

--

--

Meenu Matharu
Meenu Matharu

Written by Meenu Matharu

🚀 Passionate Frontend Developer | Storyteller on a Coding Journey 🌟 Dive deep into the world of frontend technologies like HTML, CSS, JavaScript and React

No responses yet