Create Calendar in React JS - Step-by-Step Guide

Faraz

By Faraz - Last Updated:

Learn how to create a calendar in React JS using Vite and date-fns. Follow this easy step-by-step guide to build a modern, responsive calendar app.


create-calendar-in-react-js-step-by-step-guide.webp

Calendars are important in many web apps, such as booking systems, to-do apps, and schedules. In this blog, you will learn how to create a calendar in React JS using Vite (for faster setup) and the date-fns library (to manage dates easily). We will keep the UI clean and responsive so it works well on all devices.

Whether you're a beginner or intermediate developer, this guide will help you build your own custom calendar in React step by step.

Environment Setup

Before you begin, make sure Node.js and npm (Node Package Manager) are installed on your computer. If not, you can download them from the official Node.js website. Once you’re ready, follow these steps:

We’ll use Vite to set up the React project, as it’s much faster and more modern than Create React App.

Step 1: Install Vite with React

Open your terminal and run:

npm create vite@latest react-calendar-app -- --template react
cd react-calendar-app
npm install

Step 2: Install date-fns

Now install date-fns for date formatting and manipulation:

npm install date-fns

Step 3: Start Development Server

Run your app:

npm run dev

Now your React app is ready to build the calendar.

Step-by-Step Guide to Build the Calendar

Add Tailwind CDN

Open index.html in your public folder (or root in Vite) and add inside <head>:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>React Calendar</title>
    <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

This will load Tailwind CSS via CDN.

Remove Unused CSS Files (src/assets/App.css & src/assets/Index.css)

Since we're using 100% Tailwind CSS via CDN, you can safely delete the default App.css and Index.css files to keep your project clean and lightweight.

Replace src/App.jsx with the Calendar Component Code:

import React, { useState, useEffect, useCallback, useMemo } from 'react';
import {
  format,
  startOfMonth,
  endOfMonth,
  startOfWeek,
  endOfWeek,
  addMonths,
  subMonths,
  addWeeks,
  subWeeks,
  addDays,
  subDays,
  isSameMonth,
  isSameDay,
  parseISO,
  eachDayOfInterval,
  getISOWeek, // For week number if needed
} from 'date-fns';
import { enUS } from 'date-fns/locale';
// -----------------------------------------------------------------------------
// 1. Custom Hook for Calendar Logic (useCalendar)
// This hook abstracts all date manipulation and view state.
// -----------------------------------------------------------------------------
const useCalendar = (initialDate = new Date(), initialView = 'month') => {
  const [currentDate, setCurrentDate] = useState(initialDate);
  const [currentView, setCurrentView] = useState(initialView);
  // Memoized date range for the current view
  const currentPeriod = useMemo(() => {
    switch (currentView) {
      case 'month':
        const monthStart = startOfMonth(currentDate);
        const monthEnd = endOfMonth(monthStart);
        const startDate = startOfWeek(monthStart, { weekStartsOn: 1 }); // Monday start
        const endDate = endOfWeek(monthEnd, { weekStartsOn: 1 });
        return { start: startDate, end: endDate };
      case 'week':
        const weekStart = startOfWeek(currentDate, { weekStartsOn: 1 });
        const weekEnd = endOfWeek(weekStart, { weekStartsOn: 1 });
        return { start: weekStart, end: weekEnd };
      case 'day':
        return { start: currentDate, end: currentDate };
      default:
        return { start: currentDate, end: currentDate };
    }
  }, [currentDate, currentView]);
  // Memoized days to render in the current view
  const daysInView = useMemo(() => {
    return eachDayOfInterval(currentPeriod);
  }, [currentPeriod]);
  // Navigation handlers
  const navigate = useCallback((direction) => {
    setCurrentDate((prevDate) => {
      switch (currentView) {
        case 'month':
          return direction === 'next' ? addMonths(prevDate, 1) : subMonths(prevDate, 1);
        case 'week':
          return direction === 'next' ? addWeeks(prevDate, 1) : subWeeks(prevDate, 1);
        case 'day':
          return direction === 'next' ? addDays(prevDate, 1) : subDays(prevDate, 1);
        default:
          return prevDate;
      }
    });
  }, [currentView]);
  const goToToday = useCallback(() => {
    setCurrentDate(new Date());
  }, []);
  return {
    currentDate,
    currentView,
    setCurrentView,
    currentPeriod,
    daysInView,
    navigate,
    goToToday,
  };
};
// -----------------------------------------------------------------------------
// 2. Main App Component
// Handles overall layout, state, and passes props to sub-components.
// -----------------------------------------------------------------------------
const App = () => {
  const {
    currentDate,
    currentView,
    setCurrentView,
    daysInView,
    navigate,
    goToToday,
  } = useCalendar();
  const [events, setEvents] = useState([]);
  const [showEventModal, setShowEventModal] = useState(false);
  const [selectedDateForModal, setSelectedDateForModal] = useState(null);
  // Opens the event modal for a specific date
  const openEventModal = useCallback((date) => {
    setSelectedDateForModal(date);
    setShowEventModal(true);
  }, []);
  // Closes the event modal
  const closeEventModal = useCallback(() => {
    setShowEventModal(false);
    setSelectedDateForModal(null);
  }, []);
  // Adds a new event to the state
  const addEvent = useCallback((newEvent) => {
    setEvents((prevEvents) => [...prevEvents, newEvent]);
    closeEventModal();
  }, [closeEventModal]);
  // Filters events for a given date
  const getEventsForDate = useCallback((date) => {
    // Ensuring comparison is robust by parsing ISO string
    return events.filter(event => isSameDay(parseISO(event.date), date));
  }, [events]);
  return (
    <div className="min-h-screen bg-gradient-to-br from-blue-50 via-gray-100 to-green-50 p-4 md:p-8 font-sans antialiased text-gray-800">
      <div className="max-w-7xl mx-auto bg-white bg-opacity-95 backdrop-filter backdrop-blur-lg rounded-xl shadow-2xl overflow-hidden transition-all duration-300">
        {/* Calendar Header */}
        <CalendarHeader
          currentDate={currentDate}
          currentView={currentView}
          setCurrentView={setCurrentView}
          navigate={navigate}
          goToToday={goToToday}
        />
        {/* Calendar Content based on view */}
        <div className="p-4 md:p-6 lg:p-8">
          {currentView === 'month' && (
            <MonthView
              currentDate={currentDate}
              daysInView={daysInView}
              openEventModal={openEventModal}
              getEventsForDate={getEventsForDate}
            />
          )}
          {currentView === 'week' && (
            <WeekView
              currentDate={currentDate}
              daysInView={daysInView}
              openEventModal={openEventModal}
              getEventsForDate={getEventsForDate}
            />
          )}
          {currentView === 'day' && (
            <DayView
              currentDate={currentDate}
              openEventModal={openEventModal}
              getEventsForDate={getEventsForDate}
            />
          )}
        </div>
        {/* Event Modal */}
        {showEventModal && (
          <EventModal
            selectedDate={selectedDateForModal}
            closeModal={closeEventModal}
            addEvent={addEvent}
          />
        )}
      </div>
    </div>
  );
};
// -----------------------------------------------------------------------------
// 3. CalendarHeader Component
// Manages view toggles and navigation.
// -----------------------------------------------------------------------------
const CalendarHeader = React.memo(({ currentDate, currentView, setCurrentView, navigate, goToToday }) => {
  // Determine header title based on current view
  const headerTitle = useMemo(() => {
    switch (currentView) {
      case 'month':
        return format(currentDate, 'MMMM yyyy', { locale: enUS });
      case 'week':
        const startOfWeekDate = startOfWeek(currentDate, { weekStartsOn: 1 });
        const endOfWeekDate = endOfWeek(currentDate, { weekStartsOn: 1 });
        return `${format(startOfWeekDate, 'MMM d', { locale: enUS })} - ${format(endOfWeekDate, 'MMM d, yyyy', { locale: enUS })}`;
      case 'day':
        return format(currentDate, 'EEEE, MMMM d, yyyy', { locale: enUS });
      default:
        return '';
    }
  }, [currentDate, currentView]);
  return (
    <div className="flex flex-col md:flex-row items-center justify-between p-4 md:p-6 bg-gray-800 text-white shadow-lg rounded-t-xl">
      <h2 className="text-3xl font-extrabold mb-4 md:mb-0 min-w-[200px]">
        {headerTitle}
      </h2>
      <div className="flex items-center space-x-3 mb-4 md:mb-0">
        <button
          onClick={() => navigate('prev')}
          className="p-2 rounded-full hover:bg-white hover:bg-opacity-10 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-gray-400"
          aria-label="Previous period"
        >
          <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 19l-7-7 7-7"></path></svg>
        </button>
        <button
          onClick={goToToday}
          className="px-4 py-2 bg-blue-200 text-blue-800 font-semibold rounded-full shadow-md hover:bg-blue-300 transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-blue-400"
        >
          Today
        </button>
        <button
          onClick={() => navigate('next')}
          className="p-2 rounded-full hover:bg-white hover:bg-opacity-10 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-gray-400"
          aria-label="Next period"
        >
          <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7"></path></svg>
        </button>
      </div>
      <div className="flex space-x-3">
        <button
          onClick={() => setCurrentView('month')}
          className={`px-5 py-2 rounded-full font-semibold transition-all duration-300 ${currentView === 'month' ? 'bg-blue-500 text-white shadow-md' : 'text-white hover:bg-white hover:bg-opacity-10'}`}
        >
          Month
        </button>
        <button
          onClick={() => setCurrentView('week')}
          className={`px-5 py-2 rounded-full font-semibold transition-all duration-300 ${currentView === 'week' ? 'bg-blue-500 text-white shadow-md' : 'text-white hover:bg-white hover:bg-opacity-10'}`}
        >
          Week
        </button>
        <button
          onClick={() => setCurrentView('day')}
          className={`px-5 py-2 rounded-full font-semibold transition-all duration-300 ${currentView === 'day' ? 'bg-blue-500 text-white shadow-md' : 'text-white hover:bg-white hover:bg-opacity-10'}`}
        >
          Day
        </button>
      </div>
    </div>
  );
});
// -----------------------------------------------------------------------------
// 4. MonthView Component
// Renders the monthly calendar grid.
// -----------------------------------------------------------------------------
const MonthView = React.memo(({ currentDate, daysInView, openEventModal, getEventsForDate }) => {
  const weekDays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
  return (
    <div className="grid grid-cols-7 gap-1 lg:gap-2">
      {weekDays.map((day) => (
        <div key={day} className="text-center font-bold text-gray-700 py-3 text-sm md:text-base">
          {day}
        </div>
      ))}
      {daysInView.map((day) => {
        const dayEvents = getEventsForDate(day);
        const isToday = isSameDay(day, new Date());
        const isCurrentMonth = isSameMonth(day, currentDate);
        return (
          <div
            key={day.toISOString()}
            className={`
              relative h-24 md:h-28 lg:h-36 p-1 md:p-2 rounded-lg cursor-pointer flex flex-col justify-between
              border border-gray-200 transition-all duration-200
              ${isCurrentMonth ? 'bg-white hover:bg-gray-50' : 'bg-gray-50 text-gray-400 opacity-70'}
              ${isToday ? 'border-2 border-blue-400 bg-blue-50 font-semibold' : ''}
            `}
            onClick={() => openEventModal(day)}
            role="gridcell"
            aria-label={`Date ${format(day, 'MMM d, yyyy')}`}
          >
            <span className={`text-right text-sm md:text-base ${isToday ? 'text-blue-700' : 'text-gray-800'}`}>
              {format(day, 'd', { locale: enUS })}
            </span>
            <div className="flex flex-col flex-grow overflow-y-auto mt-1 no-scrollbar">
              {dayEvents.slice(0, 2).map((event) => ( // Show max 2 events directly
                <div
                  key={event.id}
                  className="bg-teal-100 text-teal-800 text-xs font-medium px-2 py-1 rounded-md mb-1 truncate"
                  title={`${event.title} (${event.time})`}
                >
                  {event.title}
                </div>
              ))}
            </div>
            {dayEvents.length > 2 && (
                <div className="text-center text-xs text-gray-600 mt-1">
                  +{dayEvents.length - 2} more
                </div>
            )}
          </div>
        );
      })}
    </div>
  );
});
// -----------------------------------------------------------------------------
// 5. WeekView Component
// Renders the weekly calendar grid with time slots.
// -----------------------------------------------------------------------------
const WeekView = React.memo(({ currentDate, daysInView, openEventModal, getEventsForDate }) => {
  // Generate time slots for the vertical axis (00:00 to 23:00)
  const timeSlots = useMemo(() => Array.from({ length: 24 }, (_, i) => `${i < 10 ? '0' : ''}${i}:00`), []);
  return (
    
( <div key={day.toISOString()} className="col-span-1 text-center font-bold text-gray-700 text-sm md:text-base"> {format(day, 'EEE d', { locale: enUS })} {isSameDay(day, new Date()) && <span className="block text-blue-600 text-xs">(Today)</span>} </div> ))} </div> <div className="relative"> {/* Horizontal time grid lines */} {timeSlots.map((_, index) => ( <div key={`time-line-${index}`} className="absolute left-0 right-0 border-t border-gray-200" style={{ top: `${(index * 60) / (24 * 60) * 100}%`, height: '1px', zIndex: 0 }} ></div> ))} {/* Main week grid with time labels and day columns */} <div className="grid grid-cols-8 gap-1 md:gap-2"> {/* Time Labels Column */} <div className="col-span-1 flex flex-col"> {timeSlots.map((time) => ( <div key={time} className="h-10 flex items-center justify-end pr-2 text-xs text-gray-500"> {time} </div> ))} </div> {/* Day Columns for events */} {daysInView.map((day) => { const dayEvents = getEventsForDate(day); const isToday = isSameDay(day, new Date()); return ( <div key={day.toISOString()} className={` relative col-span-1 min-h-[600px] border border-gray-200 rounded-lg p-1 transition-all duration-200 ${isToday ? 'bg-blue-50 border-2 border-blue-400' : 'bg-white'} hover:bg-gray-50 cursor-pointer `} onClick={() => openEventModal(day)} role="gridcell" aria-label={`Day ${format(day, 'MMM d, yyyy')}`} > {/* Render events within the day column */} {dayEvents.map((event) => ( <div key={event.id} className="absolute w-[calc(100%-8px)] bg-teal-100 text-teal-800 text-xs font-medium px-2 py-1 rounded-md truncate shadow-sm z-10" style={{ // Position event based on its start time (assuming 40px height per hour) top: `${parseInt(event.time.split(':')[0]) * 40}px`, height: '35px', // Fixed height for visual consistency left: '4px', }} title={`${event.title} (${event.time})`} > {event.time} - {event.title} </div> ))} </div> ); })} </div> </div> </div> ); }); // ----------------------------------------------------------------------------- // 6. DayView Component // Renders events for a single selected day. // ----------------------------------------------------------------------------- const DayView = React.memo(({ currentDate, openEventModal, getEventsForDate }) => { const dayEvents = getEventsForDate(currentDate); const timeSlots = useMemo(() => Array.from({ length: 24 }, (_, i) => `${i < 10 ? '0' : ''}${i}:00`), []); return (
+ Add Event </button> <div className="grid grid-cols-1 md:grid-cols-[100px_1fr] gap-4"> {/* Time Labels Column */} <div className="flex flex-col"> {timeSlots.map((time) => ( <div key={time} className="h-10 flex items-center justify-end pr-2 text-xs text-gray-500 border-b border-gray-100"> {time} </div> ))} </div> {/* Events List for the day */} <div className="relative flex flex-col"> {/* Horizontal time grid lines */} {timeSlots.map((_, index) => ( <div key={`day-time-line-${index}`} className="absolute left-0 right-0 border-t border-gray-200" style={{ top: `${(index * 60) / (24 * 60) * 100}%`, height: '1px', zIndex: 0 }} ></div> ))} <div className="min-h-[600px] pt-2 relative"> {dayEvents.length > 0 ? ( dayEvents.map((event) => ( <div key={event.id} className="bg-teal-100 text-teal-800 text-sm font-medium px-3 py-2 rounded-lg mb-2 shadow-sm relative z-10" style={{ // Position event based on its start time top: `${parseInt(event.time.split(':')[0]) * 40}px`, position: 'absolute', width: 'calc(100% - 16px)', left: '8px', }} > <p className="font-bold">{event.title}</p> <p className="text-xs">{event.time}</p> </div> )) ) : ( <p className="text-center text-gray-500 mt-20">No events for this day. Click '+ Add Event' to add one!</p> )} </div> </div> </div> </div> ); }); // ----------------------------------------------------------------------------- // 7. EventModal Component // Form for adding new events. // ----------------------------------------------------------------------------- const EventModal = React.memo(({ selectedDate, closeModal, addEvent }) => { const [title, setTitle] = useState(''); const [time, setTime] = useState('09:00'); // Default time const handleSubmit = useCallback((e) => { e.preventDefault(); if (title.trim() === '') { // Basic validation - in a real app, this would be more robust console.error("Event title cannot be empty."); // Use console.error instead of alert return; } const newEvent = { id: Date.now(), // Simple unique ID for demo title, date: format(selectedDate, 'yyyy-MM-dd', { locale: enUS }), time, }; addEvent(newEvent); }, [title, time, selectedDate, addEvent]); return ( <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"> <div className="bg-white rounded-xl shadow-2xl p-6 w-full max-w-md transform transition-all duration-300 scale-100 opacity-100"> <div className="flex justify-between items-center mb-4"> <h3 className="text-2xl font-bold text-gray-800">Add Event for {format(selectedDate, 'MMM d, yyyy', { locale: enUS })}</h3> <button onClick={closeModal} className="text-gray-500 hover:text-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-300 rounded-full p-1" aria-label="Close modal" > <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path></svg> </button> </div> <form onSubmit={handleSubmit} className="space-y-4"> <div> <label htmlFor="eventTitle" className="block text-gray-700 text-sm font-semibold mb-2"> Event Title </label> <input type="text" id="eventTitle" value={title} onChange={(e) => setTitle(e.target.value)} className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-400 focus:border-blue-400 transition-colors duration-200" placeholder="e.g., Team Meeting" required /> </div> <div> <label htmlFor="eventTime" className="block text-gray-700 text-sm font-semibold mb-2"> Time </label> <input type="time" id="eventTime" value={time} onChange={(e) => setTime(e.target.value)} className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-400 focus:border-blue-400 transition-colors duration-200" required /> </div> <div className="flex justify-end space-x-3"> <button type="button" onClick={closeModal} className="px-5 py-2 bg-gray-200 text-gray-800 rounded-full font-semibold hover:bg-gray-300 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-gray-400" > Cancel </button> <button type="submit" className="px-5 py-2 bg-blue-600 text-white rounded-full font-semibold shadow-md hover:bg-blue-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500" > Add Event </button> </div> </form> </div> </div> ); }); export default App;

Run Your App

Go to your terminal:

npm run dev

Open http://localhost:5173/ and see your calendar.

Conclusion

You’ve just built a modern and responsive calendar in React JS using Vite and date-fns! This is perfect for scheduling apps, event planners, or booking platforms. You can extend this by adding features like:

  • Time slot selection
  • Integration with Google Calendar API

This setup is fast, clean, and great for SEO. Make sure to explore more with date-fns for advanced date features.

Calendar Live Demo ⟶

That’s a wrap!

I hope you enjoyed this article

Did you like it? Let me know in the comments below 🔥 and you can support me by buying me a coffee.

And don’t forget to sign up to our email newsletter so you can get useful content like this sent right to your inbox!

Thanks!
Faraz 😊

End of the article

Subscribe to my Newsletter

Get the latest posts delivered right to your inbox


Latest Components

Please allow ads on our site🥺