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.



