Next.js starter your AI actually understands. Ship internal tools in days not weeks. Pre-order $199 $499 → [Get it now]

How to build a calendar in React

Building a calendar component requires generating grids of days, handling month navigation, and marking events on specific dates. As the creator of CoreUI with over 10 years of React experience since 2014, I’ve built calendar interfaces for booking systems, scheduling tools, and project management dashboards. The most effective approach calculates the days grid from the current month state, renders weeks as rows, and highlights today and event dates. This produces a functional calendar with pure React and no external date libraries.

Generate the calendar grid and render it.

import { useState } from 'react'

function Calendar() {
  const [current, setCurrent] = useState(new Date())

  const year = current.getFullYear()
  const month = current.getMonth()

  const firstDay = new Date(year, month, 1).getDay()
  const daysInMonth = new Date(year, month + 1, 0).getDate()
  const today = new Date()

  const prevMonth = () => setCurrent(new Date(year, month - 1, 1))
  const nextMonth = () => setCurrent(new Date(year, month + 1, 1))

  const monthName = current.toLocaleString('en-US', { month: 'long', year: 'numeric' })

  const cells = []
  for (let i = 0; i < firstDay; i++) cells.push(null)
  for (let d = 1; d <= daysInMonth; d++) cells.push(d)

  const weeks = []
  for (let i = 0; i < cells.length; i += 7) {
    weeks.push(cells.slice(i, i + 7))
  }

  const isToday = (day) =>
    day === today.getDate() &&
    month === today.getMonth() &&
    year === today.getFullYear()

  return (
    <div className="calendar">
      <div className="calendar-header">
        <button onClick={prevMonth}></button>
        <h2>{monthName}</h2>
        <button onClick={nextMonth}></button>
      </div>
      <table>
        <thead>
          <tr>{['Sun','Mon','Tue','Wed','Thu','Fri','Sat'].map(d => <th key={d}>{d}</th>)}</tr>
        </thead>
        <tbody>
          {weeks.map((week, wi) => (
            <tr key={wi}>
              {week.map((day, di) => (
                <td key={di} className={day && isToday(day) ? 'today' : ''}>
                  {day || ''}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  )
}

firstDay calculates the starting offset (0=Sunday). Leading null cells create empty cells before the 1st. Days split into weeks of 7. isToday highlights the current date. Navigation updates the current date state.

Adding Clickable Days

Allow selecting dates and showing a selected state.

import { useState } from 'react'

function SelectableCalendar() {
  const [current, setCurrent] = useState(new Date())
  const [selected, setSelected] = useState(null)

  const year = current.getFullYear()
  const month = current.getMonth()
  const firstDay = new Date(year, month, 1).getDay()
  const daysInMonth = new Date(year, month + 1, 0).getDate()

  const selectDay = (day) => {
    if (!day) return
    setSelected(new Date(year, month, day))
  }

  const isSelected = (day) =>
    selected &&
    day === selected.getDate() &&
    month === selected.getMonth() &&
    year === selected.getFullYear()

  const cells = [
    ...Array(firstDay).fill(null),
    ...Array.from({ length: daysInMonth }, (_, i) => i + 1)
  ]

  return (
    <div>
      <div>
        <button onClick={() => setCurrent(new Date(year, month - 1, 1))}></button>
        <strong>{current.toLocaleString('en-US', { month: 'long', year: 'numeric' })}</strong>
        <button onClick={() => setCurrent(new Date(year, month + 1, 1))}></button>
      </div>
      {selected && <p>Selected: {selected.toLocaleDateString()}</p>}
      <div className="grid">
        {cells.map((day, i) => (
          <div
            key={i}
            onClick={() => selectDay(day)}
            className={[
              'cell',
              day ? 'day' : 'empty',
              isSelected(day) ? 'selected' : ''
            ].join(' ')}
          >
            {day}
          </div>
        ))}
      </div>
    </div>
  )
}

selected stores the chosen date. isSelected compares year, month, and day together. Clicking a day sets it as selected. The selected date displays in a human-readable format.

Displaying Events on Days

Mark days with scheduled events.

import { useState } from 'react'

const EVENTS = {
  '2026-03-12': ['Team meeting', 'Code review'],
  '2026-03-15': ['Client demo'],
  '2026-03-20': ['Sprint planning']
}

function EventCalendar() {
  const [current, setCurrent] = useState(new Date(2026, 2, 1))

  const year = current.getFullYear()
  const month = current.getMonth()
  const firstDay = new Date(year, month, 1).getDay()
  const daysInMonth = new Date(year, month + 1, 0).getDate()

  const getEvents = (day) => {
    if (!day) return []
    const key = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`
    return EVENTS[key] || []
  }

  const cells = [
    ...Array(firstDay).fill(null),
    ...Array.from({ length: daysInMonth }, (_, i) => i + 1)
  ]

  return (
    <div className="event-calendar">
      <div className="nav">
        <button onClick={() => setCurrent(new Date(year, month - 1, 1))}></button>
        <h3>{current.toLocaleString('en-US', { month: 'long', year: 'numeric' })}</h3>
        <button onClick={() => setCurrent(new Date(year, month + 1, 1))}></button>
      </div>
      <div className="grid">
        {cells.map((day, i) => (
          <div key={i} className="cell">
            <span className="day-number">{day}</span>
            {getEvents(day).map((event, ei) => (
              <div key={ei} className="event">{event}</div>
            ))}
          </div>
        ))}
      </div>
    </div>
  )
}

EVENTS maps ISO date strings to event arrays. getEvents builds the key from year, month, and day with zero-padding. Events render as badges inside each day cell. Replace the static object with API data for dynamic events.

Best Practice Note

This is the same calendar architecture we use in CoreUI scheduling interfaces. For production apps, replace the custom grid with date-fns to handle edge cases like DST changes and locale formatting. Add keyboard navigation (arrow keys for date selection, Enter to confirm) for accessibility. Consider CoreUI’s calendar components for production use which handle timezone differences, recurring events, and drag-and-drop scheduling out of the box, saving weeks of development.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.

Answers by CoreUI Core Team