Build an Event Calendar with Next.js
Integrate and deeply customize react-calendar in a Next.js TypeScript app.
Introduction
30 Second Summary
Calendars are everywhere in the apps you use daily. From scheduling tools to project managers, they all started as a basic component that someone customized to fit their needs.
In this project, you will build a custom event calendar using React-Calendar and Next.js. You will replace its default styling with a polished Tailwind CSS theme, render event titles directly on day tiles, and build a detail panel that shows scheduled events when you click any day.
What You'll Build
Click any day on your polished calendar, see event titles right on the tiles, and watch a detail panel appear showing every event with its scheduled time.
By the end of this project, you'll have:
- A fully custom-themed calendar with rounded corners, clean typography, and an indigo color scheme that replaces react-calendar's default gray and blue styles.
- Event titles rendered directly inside day tiles so you can see your schedule at a glance, with overflow handling for busy days.
- An interactive detail panel that updates instantly when you click a day, showing event names and their scheduled times.
- Secret Mission: A form that lets you create new events directly from the calendar and see them appear on tiles in real time.
Are there any prerequisites?
This project assumes familiarity with React, TypeScript, and basic CSS. You do not need prior experience with react-calendar or Tailwind CSS.
Before We Start
Before We Start...
Before we dive in, let's lock in what you're about to build and why it matters. This step is your moment to commit to the goal so every action from here on has a clear purpose.
Set Up the Project
To build a custom event calendar, you need a React framework that supports TypeScript and modern CSS tooling.
In this step, you will scaffold a Next.js 16 project with Tailwind CSS v4, install the react-calendar library, and confirm everything runs.
In this step, get ready to:
- Scaffold a Next.js 16 project with TypeScript, Tailwind CSS, and the App Router.
- Install the react-calendar library.
- Start the development server and verify the default page loads.
Scaffold the Next.js project
The create-next-app CLI generates all the boilerplate files and installs dependencies in one command.
- Open your terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your Next.js project by running this command:
npx create-next-app@latest event-calendar --ts --tailwind --app --use-npm --eslint
What does this command do?
- npx create-next-app@latest runs the latest version of the Next.js project scaffolding tool.
- event-calendar is the name of the folder it creates on your Desktop.
- --ts enables TypeScript, --tailwind adds Tailwind CSS, --app uses the App Router, --use-npm picks npm as the package manager, and --eslint sets up linting.
Because you provided configuration flags, the CLI skips interactive prompts and uses recommended defaults for any remaining options. You will see a summary of those defaults printed in the terminal.
- Wait for the install to finish. You will see a success message and a list of files created.
- Navigate into the new project folder by running:
cd event-calendar
Seeing a permission error or command not found?
- Make sure Node.js is installed by running node --version in your terminal. You need version 20.9 or higher for Next.js 16.
- If npx asks whether to install the package, type y and press Enter.
Ask for help: Why is npx create-next-app failing for me?
Clean up the scaffold and prepare the project files
The scaffold generates starter styles and font imports that you do not need. You will replace them with a clean slate for your custom calendar theme.
- Open VS Code. Press Cmd+Space (macOS) or the Windows key (Windows), type VS Code, and press Enter.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Select the event-calendar folder on your Desktop, then click Open.
- In the Explorer sidebar, expand the app folder and open globals.css.
- Replace the entire contents of globals.css with this single line:
@import 'tailwindcss';
Why remove the default styles?
The scaffold adds demo styles for the welcome page. You will write your own CSS rules targeting react-calendar's internal class names in a later step. Starting with a clean file avoids specificity conflicts.
- Save globals.css.
- Open app/layout.tsx from the Explorer sidebar.
- Replace the entire contents of layout.tsx with the following:
import type { Metadata } from 'next';
import './globals.css';
export const metadata: Metadata = {
title: 'Event Calendar',
description: 'A custom event calendar built with React-Calendar and Next.js',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
What does this layout do?
- It imports globals.css so Tailwind CSS is available throughout the app.
- The metadata object sets the page title and description that appear in the browser tab.
- The RootLayout function wraps every page in a basic HTML structure. You removed the scaffold's font imports to keep the layout minimal.
- Save layout.tsx.
Install react-calendar
The react-calendar library provides a pre-built month-view calendar component with props for customizing tiles and handling day clicks.
- Open the integrated terminal in VS Code by pressing Ctrl+` (backtick key, same on macOS and Windows).
- Install react-calendar by running:
npm install react-calendar@6.0.1
- You should see output confirming the package was added to your node_modules folder.
Do I need to install TypeScript types separately?
No. Version 6.0.1 of react-calendar ships with built-in TypeScript type definitions. There is no need to install a separate @types package.
Install failing with a peer dependency error?
- Make sure you are inside the event-calendar folder. Run pwd (macOS) or cd (Windows) to check your current directory.
- If you see a warning about React version compatibility, it is safe to ignore. react-calendar v6 supports React 16.8 through React 19.
Ask for help: Why is npm install react-calendar failing?
Start the dev server and verify
Everything is installed. Before you predict what you will see, think about what the scaffold put in app/page.tsx (you did not change that file).
- Start the development server by running:
npm run dev
What do you expect to see when you open the browser? The scaffold's default page is still in place.
- Open your browser and navigate to http://localhost:3000.
You should see the Next.js default welcome page. The styling looks different from a fresh scaffold because you stripped the custom CSS, but the page content still renders.
Seeing a blank page or an error?
- Check your terminal for error output. A common cause is a typo in layout.tsx (make sure the <html> and <body> tags are properly closed).
- Make sure globals.css contains the line @import 'tailwindcss'; and nothing else.
- Confirm the terminal shows the server running on port 3000. If another process is using that port, stop it and re-run npm run dev.
Ask for help: My Next.js dev server is not showing the welcome page.
✔️ Awesome, I've got everything!
Great. Double check you have saved both globals.css and layout.tsx, and that your browser shows the Next.js welcome page at http://localhost:3000.
ⓧ I'd like to double check the full code
Here is what each file should look like. Compare yours against these:
app/globals.css
@import 'tailwindcss';
app/layout.tsx
import type { Metadata } from 'next';
import './globals.css';
export const metadata: Metadata = {
title: 'Event Calendar',
description: 'A custom event calendar built with React-Calendar and Next.js',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
app/page.tsx should be the untouched scaffold default. You did not edit this file.
Your project is scaffolded, react-calendar is installed, and the dev server is running. Next up, you will render the default calendar component and see what it looks like before any styling.
Render the Default Calendar
Your Next.js project is scaffolded and the dev server is running. Now you need to see react-calendar working with its default appearance before making any customizations.
Rendering the default calendar first gives you a visible baseline to compare against. Once you see the dated gray-border look, you will understand exactly why custom styling matters in the next step.
In this step, get ready to:
- Create a client component that renders the default calendar.
- Wire the component into your page and confirm it renders in the browser.
Create the calendar component
React-calendar uses browser interactivity (click handlers, internal state) to let users navigate months and select dates. In the App Router, any component that uses browser APIs must include a 'use client' directive at the top of the file.
- Create a new folder and file at app/components/EventCalendar.tsx by running this command from your event-calendar directory:
mkdir -p app/components && touch app/components/EventCalendar.tsx
- Confirm the file was created by running:
ls app/components
You should see EventCalendar.tsx listed in the output.
- Open app/components/EventCalendar.tsx in VS Code and paste the following code:
'use client';
import { useState } from 'react';
import Calendar from 'react-calendar';
import 'react-calendar/dist/Calendar.css';
export default function EventCalendar() {
// Track the currently selected date
const [value, setValue] = useState<Date>(new Date());
return (
<Calendar
value={value}
onChange={(val) => setValue(val as Date)}
/>
);
}
What does this code do?
- 'use client' tells Next.js this component runs in the browser, not on the server. React-calendar needs this because it handles clicks and manages internal state.
- import Calendar from 'react-calendar' brings in the calendar component itself. The CSS import on the next line loads the library's default visual styles.
- useState<Date> tracks which date the user has selected. When a day is clicked, onChange fires and updates this value.
- Save EventCalendar.tsx.
- Check the terminal where npm run dev is running. You should see no error messages.
Seeing a module not found error?
- Make sure react-calendar is installed. Run npm install react-calendar@6.0.1 from your event-calendar directory and restart the dev server.
- Check that the CSS import path is exactly react-calendar/dist/Calendar.css with no typos.
- help me fix a module not found error for react-calendar in my Next.js project
Wire it into the page
The component exists but nothing renders it yet. You need to replace the default Next.js welcome page with your calendar.
- Open app/page.tsx and replace its entire contents with the following code:
import EventCalendar from './components/EventCalendar';
export default function Home() {
return (
<main className="min-h-screen bg-gray-50 px-4 py-12">
<div className="mx-auto max-w-4xl">
<h1 className="mb-8 text-center text-3xl font-bold text-gray-900">
Event Calendar
</h1>
<EventCalendar />
</div>
</main>
);
}
What does this code do?
- The import pulls in your client component. Even though page.tsx is a server component by default, it can render client components as children.
- The Tailwind CSS utility classes on <main> and <div> center the calendar on a light gray background with comfortable spacing.
- Save page.tsx.
Before you check the browser, what do you think the calendar will look like? Will it match your app's style, or will it look like a completely different widget dropped onto the page?
- Switch to your browser at http://localhost:3000. The page should refresh automatically.
You should see a working calendar with default react-calendar styling. It has gray borders, a yellow highlight on today's date, and a blue background on the selected date. You can click the navigation arrows to move between months.
Why does it look so different from the rest of the page?
The calendar's default CSS was designed years ago by the library author. It clashes with your Tailwind-styled page because it uses its own border colors, font sizes, and highlight colors. In the next step, you will strip these defaults and write your own theme.
Don't see the calendar?
- Make sure you saved both EventCalendar.tsx and page.tsx. Unsaved changes will not trigger a browser refresh.
- Check that the import path in page.tsx is ./components/EventCalendar (with a lowercase c in components). File paths are case-sensitive.
- Confirm the dev server is still running in your terminal. If it crashed, run npm run dev again.
- help me debug why my EventCalendar component is not rendering in the browser
✔️ Awesome, I've got everything!
Great. Double check that both files are saved and the calendar displays with navigation arrows at the top.
ⓧ I'd like to double check the full code
Here is the full code for each file you created or edited in this step.
app/components/EventCalendar.tsx
'use client';
import { useState } from 'react';
import Calendar from 'react-calendar';
import 'react-calendar/dist/Calendar.css';
export default function EventCalendar() {
const [value, setValue] = useState<Date>(new Date());
return (
<Calendar
value={value}
onChange={(val) => setValue(val as Date)}
/>
);
}
app/page.tsx
import EventCalendar from './components/EventCalendar';
export default function Home() {
return (
<main className="min-h-screen bg-gray-50 px-4 py-12">
<div className="mx-auto max-w-4xl">
<h1 className="mb-8 text-center text-3xl font-bold text-gray-900">
Event Calendar
</h1>
<EventCalendar />
</div>
</main>
);
}
You now have a working calendar on screen. It functions perfectly, but those default styles stick out against your clean Tailwind layout. Next up, you will strip the default CSS and write a fully custom theme that makes this calendar look like it belongs.
Apply Custom Styling with Tailwind
In the previous step, you rendered the calendar with react-calendar's default stylesheet. It works, but the gray borders, blue selection highlight, and yellow today marker look dated.
Third-party React components render their own DOM with fixed class names. You cannot apply Tailwind CSS utility classes directly to those internal elements. The pattern is to target the library's class names in your own CSS file and write rules from scratch. In this step, you'll strip the default stylesheet and replace it with a modern, custom theme.
In this step, get ready to:
- Remove the default react-calendar stylesheet.
- Write custom CSS rules targeting react-calendar's internal class names.
- Transform the calendar into a modern card-based design with an indigo accent color.
Remove the default CSS import
Before writing any custom styles, you need to remove the bundled stylesheet so the two don't fight each other.
- In app/components/EventCalendar.tsx, find this line:
import 'react-calendar/dist/Calendar.css';
- Delete that entire line.
- Save EventCalendar.tsx and check your browser.
The calendar now appears as a vertical stack of unstyled text and buttons with no alignment, borders, or colors. That confirms the default styles are gone.
Why start from scratch instead of overriding?
Overriding an existing stylesheet means battling specificity and discovering which rules are doing what. Writing from scratch gives you full control and keeps your CSS predictable.
Style the calendar container and navigation
react-calendar renders elements with class names like .react-calendar and .react-calendar__navigation. You'll target these directly in your global stylesheet.
- Open app/globals.css and add the following below the existing @import 'tailwindcss'; line:
/* React-Calendar Custom Styles */
.react-calendar {
width: 100%;
max-width: 400px;
background: white;
border: 1px solid #e5e7eb;
border-radius: 0.75rem;
padding: 1rem;
font-family: inherit;
line-height: 1.5;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}
.react-calendar *,
.react-calendar *:before,
.react-calendar *:after {
box-sizing: border-box;
}
.react-calendar button {
margin: 0;
border: 0;
outline: none;
}
.react-calendar button:enabled:hover {
cursor: pointer;
}
What does this code do?
The .react-calendar rule styles the outer container as a white card with rounded corners, a subtle border, and a soft shadow.
The reset rules (box-sizing, button resets) ensure consistent sizing and remove browser default button styles so your later rules work predictably.
- Save globals.css and check your browser.
The calendar now sits inside a rounded white card with a border and shadow. The buttons are still unstyled text, which you'll fix next.
Don't see the card appearance?
Make sure you added the CSS below the @import 'tailwindcss'; line, not above it. Tailwind's reset can override your rules if your styles come first.
Also confirm your dev server is still running in the terminal.
help me debug why my custom CSS for react-calendar isn't showing up
- Still in app/globals.css, add the navigation styles below the code you just wrote:
/* Navigation */
.react-calendar__navigation {
display: flex;
align-items: center;
margin-bottom: 1rem;
}
.react-calendar__navigation button {
min-width: 36px;
padding: 0.5rem;
background: none;
font-size: 0.875rem;
font-weight: 600;
color: #374151;
border-radius: 0.5rem;
}
.react-calendar__navigation button:enabled:hover,
.react-calendar__navigation button:enabled:focus {
background-color: #f3f4f6;
}
.react-calendar__navigation button:disabled {
color: #9ca3af;
}
What does this code do?
The navigation bar (the month/year label and arrow buttons) becomes a flexbox row. Each button gets rounded corners and a light hover background so it feels responsive without being distracting.
- Save globals.css and check your browser.
The month/year header and arrow buttons now sit in a clean horizontal row. Hover over the arrows to see the subtle gray highlight.
Add structural styles for tiles
Each day in the calendar is a tile (a <button> with the class .react-calendar__tile). You need a fixed minimum height and a column layout so event content can sit below the date number later.
- In app/globals.css, add the weekday header and tile base styles below your navigation rules:
/* Weekday Headers */
.react-calendar__month-view__weekdays {
text-align: center;
text-transform: uppercase;
font-size: 0.7rem;
font-weight: 600;
color: #6b7280;
}
.react-calendar__month-view__weekdays__weekday {
padding: 0.5rem 0;
}
.react-calendar__month-view__weekdays__weekday abbr {
text-decoration: none;
}
/* Day Tiles */
.react-calendar__tile {
max-width: 100%;
padding: 0.5rem 0.25rem;
background: none;
text-align: center;
font-size: 0.8rem;
border-radius: 0.5rem;
color: #374151;
min-height: 60px;
display: flex;
flex-direction: column;
align-items: center;
}
What does this code do?
The weekday headers (Mon, Tue, etc.) get uppercase styling and the default underline on the <abbr> elements is removed.
Each tile uses min-height: 60px with flex-direction: column so the date number stays at the top and event content can stack below it. This preparation prevents layout shifts when you add event labels in the next step.
- Save globals.css and check your browser.
The weekday labels are now compact uppercase text. Each day tile has consistent height and spacing, forming a clean grid.
Tiles look collapsed or misaligned?
Double-check that min-height: 60px is inside the .react-calendar__tile rule and not accidentally placed in a different selector.
help me fix react-calendar tile layout issues in my custom CSS
- Still in app/globals.css, add the tile interaction and state styles below the code you just wrote:
.react-calendar__tile:disabled {
background-color: #f9fafb;
color: #d1d5db;
}
.react-calendar__tile:enabled:hover,
.react-calendar__tile:enabled:focus {
background-color: #f3f4f6;
}
.react-calendar__tile--now {
background-color: #fef3c7;
font-weight: 600;
}
.react-calendar__tile--now:enabled:hover,
.react-calendar__tile--now:enabled:focus {
background-color: #fde68a;
}
.react-calendar__tile--active {
background-color: #4f46e5;
color: white;
}
.react-calendar__tile--active:enabled:hover,
.react-calendar__tile--active:enabled:focus {
background-color: #4338ca;
}
.react-calendar__tile--active .truncate {
background-color: rgba(255, 255, 255, 0.2);
color: white;
}
What does this code do?
These rules create the color states you see as you interact with the calendar:
- Hover/focus tiles get a light gray background so you know what you're about to click.
- Today's date (--now) gets an amber background to stand out.
- The selected date (--active) turns indigo with white text for a strong visual anchor.
- The .truncate rule pre-styles event labels inside an active tile so they remain readable on the dark background.
- Save globals.css and check your browser.
Click a day tile. It turns indigo. Today's tile has an amber background. Hovering over any tile shows a gray highlight.
- Add the final set of styles for neighboring-month days, weekends, and the event indicator class:
/* Neighboring month days */
.react-calendar__month-view__days__day--neighboringMonth {
color: #d1d5db;
}
/* Weekend days */
.react-calendar__month-view__days__day--weekend {
color: #dc2626;
}
.react-calendar__tile--active.react-calendar__month-view__days__day--weekend {
color: white;
}
/* Event tile indicator */
.has-events {
font-weight: 600;
}
What does this code do?
Neighboring-month days (the grayed-out overflow from the previous/next month) are faded to reduce noise. Weekend days appear in red for quick visual distinction. The .has-events class is a placeholder you'll use in the next step to bold days that have events.
- Save globals.css.
Before you check the browser, predict what difference you expect from these last rules. Which days should now appear in red?
Check your browser. Saturday and Sunday columns now display in red, and any overflow days from the previous or next month are faded to light gray. The calendar looks completely different from the default theme you started with.
Weekend days not red?
Confirm the selector is exactly .react-calendar__month-view__days__day--weekend. A missing segment (like leaving out __days) will silently fail.
help me figure out why my weekend day styles aren't being applied to react-calendar
✔️ Awesome, I've got everything!
Great. Double-check you've saved both app/globals.css and app/components/EventCalendar.tsx.
ⓧ I'd like to double check the full code
Your EventCalendar.tsx should now look like this (the only change is removing the CSS import):
'use client';
import { useState } from 'react';
import Calendar from 'react-calendar';
export default function EventCalendar() {
const [value, setValue] = useState<Date>(new Date());
return (
<Calendar
value={value}
onChange={(val) => setValue(val as Date)}
/>
);
}
Your globals.css is longer. Scroll through and confirm each section is present, in this order:
- @import 'tailwindcss'; at the top.
- The base .react-calendar card styles and button/box-sizing resets.
- The .react-calendar__navigation flexbox rules.
- The weekday header styles and .react-calendar__tile base layout (with min-height: 60px).
- The tile state rules: --now (amber), --active (indigo), hover/focus (gray).
- The neighboring-month, weekend, and .has-events rules at the bottom.
Your calendar now has a polished custom theme built entirely from your own CSS rules. Next up, you'll make it useful by rendering event data directly inside those tiles.
Display Events on Calendar Tiles
Your calendar looks polished, but it is not showing any actual events. Right now it is just a date picker with nice styling.
A calendar earns its place when it shows you what is happening on each day at a glance. In this step, you will create sample event data and use react-calendar's tileContent prop to render event titles directly inside day tiles.
In this step, get ready to:
- Create an events data file with a TypeScript interface and sample events.
- Render event title labels on calendar tiles using the tileContent prop.
- Conditionally style days that have events using the tileClassName prop.
Create the events data file
Before the calendar can display anything, it needs structured event data to draw from. You will define a TypeScript interface for events and export an array of sample data.
- Create a new folder and file at app/data/events.ts by running this command from your event-calendar project folder:
mkdir -p app/data && touch app/data/events.ts
- Open app/data/events.ts in VS Code and paste the following code:
export interface CalendarEvent {
id: string;
title: string;
date: Date;
time: string;
}
const today = new Date();
const year = today.getFullYear();
const month = today.getMonth();
export const events: CalendarEvent[] = [
{ id: '1', title: 'Team Standup', date: new Date(year, month, 5), time: '09:00 AM' },
{ id: '2', title: 'Design Review', date: new Date(year, month, 5), time: '02:00 PM' },
{ id: '3', title: 'Sprint Planning', date: new Date(year, month, 12), time: '10:00 AM' },
{ id: '4', title: 'Lunch with Client', date: new Date(year, month, 12), time: '12:30 PM' },
{ id: '5', title: 'Code Review', date: new Date(year, month, 18), time: '03:00 PM' },
{ id: '6', title: 'Product Demo', date: new Date(year, month, 22), time: '11:00 AM' },
{ id: '7', title: 'Workshop', date: new Date(year, month, 22), time: '04:00 PM' },
{ id: '8', title: '1:1 Meeting', date: new Date(year, month, 28), time: '09:30 AM' },
];
What does this code do?
The CalendarEvent interface defines the shape each event must follow: a unique id, a title, a date (a JavaScript Date object), and a time string for display.
The sample events use the current year and month so they always appear in the calendar's default view. Days 5 and 12 each have two events, making them useful for testing the overflow indicator later.
- Save app/data/events.ts.
- Confirm the file exists by running:
ls app/data/
You should see events.ts listed in the output.
Don't see events.ts?
Make sure you ran the mkdir -p app/data command first. The data folder does not exist by default in a Next.js project.
Help me debug why my events.ts file isn't showing up.
✔️ Awesome, I've got everything!
Great. Make sure you have saved the file before moving on.
ⓧ I'd like to double check the full code
export interface CalendarEvent {
id: string;
title: string;
date: Date;
time: string;
}
const today = new Date();
const year = today.getFullYear();
const month = today.getMonth();
export const events: CalendarEvent[] = [
{ id: '1', title: 'Team Standup', date: new Date(year, month, 5), time: '09:00 AM' },
{ id: '2', title: 'Design Review', date: new Date(year, month, 5), time: '02:00 PM' },
{ id: '3', title: 'Sprint Planning', date: new Date(year, month, 12), time: '10:00 AM' },
{ id: '4', title: 'Lunch with Client', date: new Date(year, month, 12), time: '12:30 PM' },
{ id: '5', title: 'Code Review', date: new Date(year, month, 18), time: '03:00 PM' },
{ id: '6', title: 'Product Demo', date: new Date(year, month, 22), time: '11:00 AM' },
{ id: '7', title: 'Workshop', date: new Date(year, month, 22), time: '04:00 PM' },
{ id: '8', title: '1:1 Meeting', date: new Date(year, month, 28), time: '09:30 AM' },
];
Implement tileContent and tileClassName
Now you will rewrite EventCalendar.tsx to import the events, compare dates, and render small title labels inside each day tile that has events.
- Open app/components/EventCalendar.tsx and replace its entire contents with the following code (this is the first section of the file):
'use client';
import { useState } from 'react';
import Calendar from 'react-calendar';
import { events, CalendarEvent } from '../data/events';
// Compare two dates ignoring time
function isSameDay(date1: Date, date2: Date): boolean {
return (
date1.getFullYear() === date2.getFullYear() &&
date1.getMonth() === date2.getMonth() &&
date1.getDate() === date2.getDate()
);
}
// Filter events array to only those matching a given day
function getEventsForDate(date: Date): CalendarEvent[] {
return events.filter((event) => isSameDay(event.date, date));
}
export default function EventCalendar() {
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
const selectedEvents = getEventsForDate(selectedDate);
function handleDayClick(value: Date) {
setSelectedDate(value);
}
What does this code do?
The isSameDay helper compares year, month, and day of two Date objects while ignoring the time portion. This lets you match events to calendar tiles reliably.
The getEventsForDate function filters the events array down to only those whose date matches the tile being rendered. The component uses onClickDay instead of onChange because onClickDay fires only when a day tile is clicked, not on navigation.
- Directly below handleDayClick, add the tileContent function that renders event labels on each tile:
function tileContent({ date, view }: { date: Date; view: string }) {
if (view === 'month') {
const dayEvents = getEventsForDate(date);
if (dayEvents.length > 0) {
return (
<div className="mt-1 flex flex-col gap-0.5">
{dayEvents.slice(0, 2).map((event) => (
<div
key={event.id}
className="truncate rounded bg-indigo-100 px-1 text-[10px] leading-tight text-indigo-700"
>
{event.title}
</div>
))}
{dayEvents.length > 2 && (
<div className="text-[10px] leading-tight text-gray-500">
+{dayEvents.length - 2} more
</div>
)}
</div>
);
}
}
return null;
}
What does tileContent do?
react-calendar calls this function for every tile it renders. When the view is month, it checks if any events match that day.
If events exist, it renders up to 2 small title labels with indigo styling. If there are more than 2 events on a single day, a "+N more" indicator appears instead of overflowing the tile.
- Below tileContent, add the tileClassName function and the return statement to complete the component:
function tileClassName({ date, view }: { date: Date; view: string }) {
if (view === 'month') {
const dayEvents = getEventsForDate(date);
if (dayEvents.length > 0) return 'has-events';
}
return null;
}
return (
<div className="flex flex-col items-center gap-8 lg:flex-row lg:items-start lg:justify-center">
<div className="calendar-wrapper">
<Calendar
onClickDay={handleDayClick}
value={selectedDate}
tileContent={tileContent}
tileClassName={tileClassName}
/>
</div>
</div>
);
}
What does tileClassName do?
The tileClassName function returns the CSS class has-events for any day with events. You already defined this class in globals.css in the previous step. It makes the date number bold so event days stand out even before reading the labels.
The return statement wraps the calendar in a flex container that stacks vertically on mobile and arranges side-by-side on larger screens. This layout prepares space for the detail panel you will build in the next step.
- Save app/components/EventCalendar.tsx.
Before you check the browser, what do you think you will see on the calendar now?
- Check your browser at http://localhost:3000.
You should see small indigo-colored event title labels on days 5, 12, 18, 22, and 28 of the current month. Days 5 and 12 each show two labels. Clicking a day selects it (the tile turns indigo) but nothing else happens yet.
Don't see event labels on the tiles?
- Confirm app/data/events.ts is saved and exports both events and CalendarEvent. A missing export causes an import error.
- Check that the import path in EventCalendar.tsx is '../data/events' (two dots, not one). One dot means "same folder" and will not find the data directory.
- Make sure the tileContent function is passed as a prop to <Calendar />. Defining the function without passing it means react-calendar never calls it.
Help me debug why my tileContent labels are not appearing on the react-calendar tiles.
Prepare the EventPanel import for the next step
The next step creates a detail panel that shows full event info when you click a day. You will add the import and render call now so the component is ready.
- In EventCalendar.tsx, find the import section at the top of the file. Locate this line:
import { events, CalendarEvent } from '../data/events';
- Add this import directly below it:
import EventPanel from './EventPanel';
- Next, find the closing </div> of the calendar-wrapper div in the return statement:
</div>
</div>
);
}
- Add the EventPanel render between the two closing </div> tags, so the end of your return looks like this:
</div>
<EventPanel date={selectedDate} events={selectedEvents} />
</div>
);
}
- Save EventCalendar.tsx.
Seeing a module-not-found error?
Your browser now shows an error because EventPanel does not exist yet. This is expected. You will create this component in the next step, and the error will disappear the moment the file exists.
✔️ Awesome, I've got everything!
Great. Double check you have saved both app/data/events.ts and app/components/EventCalendar.tsx.
ⓧ I'd like to double check the full code
Your EventCalendar.tsx is over 30 lines, so confirm you have each section in order:
- The 'use client' directive at the very top.
- Four import lines: useState, Calendar, events/CalendarEvent, and EventPanel.
- The isSameDay helper function comparing year, month, and date.
- The getEventsForDate function filtering events by day.
- The EventCalendar component with selectedDate state, selectedEvents, and handleDayClick.
- The tileContent function returning indigo labels (max 2, with +N more).
- The tileClassName function returning 'has-events'.
- The return statement with a flex wrapper, calendar-wrapper div containing <Calendar />, and <EventPanel /> below it.
Your calendar tiles now display event information at a glance. Next up, you will create the EventPanel component so clicking a day reveals a detail panel with full event titles and timestamps.
Build the Event Detail Panel
Your calendar tiles show event titles, but they are truncated to fit the tiny tile space. There is no way to see timestamps or full titles for a busy day.
In this step, you will create a detail panel that appears beside the calendar. When you click any day, the panel updates to show the full event list with titles and scheduled times.
In this step, get ready to:
- Create the EventPanel component with a formatted date heading, event cards, and an empty state.
- Test the interaction between clicking days and the panel updating.
Create the EventPanel component
Your EventCalendar.tsx already imports and renders EventPanel, but the file does not exist yet. The browser is showing an error until you create it.
- Create a new file at app/components/EventPanel.tsx and add the type import and props interface:
import { CalendarEvent } from '../data/events';
interface EventPanelProps {
date: Date;
events: CalendarEvent[];
}
What does this code do?
The EventPanelProps interface defines the contract for this component. It receives the currently selected date and an array of CalendarEvent objects to display.
Importing CalendarEvent from events.ts ensures the component uses the same event shape as the rest of the app.
- Below the interface, add the component function that renders the date heading and event list:
export default function EventPanel({ date, events }: EventPanelProps) {
const formattedDate = date.toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
});
return (
<div className="w-full max-w-sm rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
<h2 className="text-lg font-semibold text-gray-900">{formattedDate}</h2>
{events.length === 0 ? (
<p className="mt-4 text-sm text-gray-500">No events scheduled for this day.</p>
) : (
<ul className="mt-4 flex flex-col gap-3">
{events.map((event) => (
<li key={event.id} className="rounded-lg border border-indigo-100 bg-indigo-50 p-3">
<p className="font-medium text-gray-900">{event.title}</p>
<p className="mt-1 text-sm text-gray-600">{event.time}</p>
</li>
))}
</ul>
)}
</div>
);
}
What does this code do?
- toLocaleDateString('en-US', ...) formats the selected date into a readable string like "Wednesday, July 30, 2026".
- The ternary checks events.length === 0. If true, it renders a "no events" message. Otherwise, it maps over the events array to render each one as a card.
- Each event card shows the title in bold and the time in smaller gray text, wrapped in an indigo-tinted container.
- Save app/components/EventPanel.tsx.
- Check your browser. The error should be gone, and you should see a white panel to the right of the calendar (or below it on a narrow window) displaying today's date as a heading.
Still seeing an error?
- Make sure the file is saved at exactly app/components/EventPanel.tsx. A typo in the path (like component instead of components) will keep the import failing.
- Confirm the function is exported with export default. Without the default export, the import in EventCalendar.tsx cannot find the component.
- Check that your interface uses CalendarEvent[] (with square brackets) for the events prop type.
Why is my EventPanel not rendering?
✔️ Awesome, I've got everything!
Great. Make sure you have saved EventPanel.tsx before moving on.
ⓧ I'd like to double check the full code
Your EventPanel.tsx file combines the two chunks above, in order:
- Chunk 1: The CalendarEvent import and the EventPanelProps interface (6 lines including the blank line separator).
- Chunk 2: The export default function EventPanel component with the date formatting and conditional JSX return.
Scroll up and confirm both chunks are present in your file, in that order, with a blank line between the interface closing brace and the function declaration.
Test the calendar and panel interaction
The calendar is already wired to pass selectedDate and selectedEvents to your new panel. Every time you click a day, React re-renders the panel with the matching events.
Before you click around, what do you think happens when you click a day that has no events in the sample data?
- Click the 5th of the current month (a day with two events).
- Confirm the panel heading updates to show the full date, and two event cards appear with titles ("Team Standup" and "Design Review") and their timestamps.
- Click a day with no events (like the 1st or 2nd).
- Confirm the panel shows "No events scheduled for this day." as a gray message.
The panel updates instantly on each click because React re-renders it whenever selectedDate state changes.
Panel not updating when you click?
- Make sure you are clicking on a day tile, not the navigation arrows. The onClickDay handler only fires when you click a numbered day.
- If the panel shows the correct date heading but no events, verify that the sample events in events.ts use the current year and month. They are generated dynamically using new Date(), so they should always match.
Why does clicking a day not update the detail panel?
Your calendar now has a fully interactive detail panel. Click any day and you get the full picture of what is scheduled. Next up, you will extend this into a Secret Mission where you can add brand new events directly from the calendar.
Secret mission
Add New Events from the Calendar
Your calendar displays events, but what if you could create new ones right from the UI? In this mission, you'll build a controlled form that adds events dynamically, updating both the calendar tiles and the detail panel in real time.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.
Resources you used:
- The event-calendar project folder on your Desktop (contains all source files, node_modules, and configuration).
Keep everything running
No action needed. Choose this if you are still actively building or want to extend the calendar further.
- Your project stays on your Desktop ready to use.
- Run npm run dev from inside the event-calendar folder any time you want to start the dev server again.
Pause - I'll come back to this later
Stop the dev server to free up your terminal. Your files stay intact for when you return.
- Press Ctrl+C in the terminal where the dev server is running to stop it.
- Close the browser tab showing http://localhost:3000.
- Your project files remain on your Desktop. Run npm run dev again when you are ready to resume.
Delete - I don't want to use this again
Remove all project files permanently. You can always re-create the project by following the steps again.
- Press Ctrl+C in the terminal to stop the dev server if it is still running.
- Delete the project folder by running this command from your Desktop:
cd ~/Desktop
rm -rf event-calendar
On Windows, run this instead:
cd ~\Desktop
Remove-Item -Recurse -Force event-calendar
- Confirm the folder is gone by running ls (macOS/Linux) or dir (Windows). You should no longer see event-calendar listed.
That's a Wrap!
That's a Wrap!
Nice work! You just built a fully custom event calendar using react-calendar and Next.js with TypeScript.
You've learned how to:
- Integrated react-calendar into a Next.js 16 App Router project and replaced its default styling with a fully custom Tailwind CSS theme by targeting internal class names like .react-calendar__tile and .react-calendar__navigation.
- Used the tileContent and tileClassName props to render event titles directly inside calendar day tiles with overflow handling for busy days.
- Built an interactive detail panel that shows event names and scheduled timestamps when clicking any day, with a clean empty state for days without events.
- Secret Mission: Added a dynamic "Add Event" form that updates both the calendar tiles and detail panel in real time using React state management.
Ready to quiz yourself?