function HolidayCalendar({ country, year }) {
const { holidays, loading } = useHolidays(country, year);
if (loading) return <CalendarSkeleton />;
return (
<Calendar
year={year}
holidays={holidays}
onDayClick={(date) => console.log(date)}
/>
);
}
This guide builds a reusable React calendar component that displays holidays for any country. You'll create a custom hook for data fetching, a calendar grid component, and styling that highlights holiday dates.
The Challenge#
Building a holiday calendar involves several non-obvious problems:
Data sourcing: Holidays vary by country, region, and year. Hardcoding dates fails when holidays move (Easter, Thanksgiving) or when governments add new ones.
Regional complexity: Bavaria has different holidays than Berlin. California observes holidays that Texas doesn't. A country-level approach misses these variations.
Performance: Fetching holiday data on every render wastes API calls. You need caching that persists across sessions.
Date handling: JavaScript's Date object has timezone pitfalls. A holiday on "2026-01-01" can appear on December 31st if you're not careful with parsing.
This guide addresses each of these by using the World Data API for authoritative holiday data and building a component architecture that handles caching, regional queries, and correct date display.
Prerequisites#
Before starting, you need:
Node.js 18+ installed on your machine
A World Data API key — sign up at worlddataapi.com (free tier available)
Basic React knowledge — familiarity with hooks (useState, useEffect, useMemo)
A code editor and terminal access
Project Setup#
npm create vite@latest holiday-calendar -- --template react
cd holiday-calendar
npm install
No additional dependencies required — we'll use native fetch and CSS.
The Custom Hook#
Start with a hook that fetches and caches holiday data:
// src/hooks/useHolidays.js
import { useState, useEffect } from "react";
const cache = new Map();
export function useHolidays(location, year) {
const [holidays, setHolidays] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const key = `${location}:${year}`;
if (cache.has(key)) {
setHolidays(cache.get(key));
setLoading(false);
return;
}
setLoading(true);
setError(null);
fetch(`https://worlddataapi.com/v1/holidays/${location}?year=${year}`, {
headers: {
"X-API-Key": import.meta.env.VITE_WORLD_DATA_API_KEY,
},
})
.then((res) => {
if (!res.ok) throw new Error(`API error: ${res.status}`);
return res.json();
})
.then((data) => {
cache.set(key, data.holidays);
setHolidays(data.holidays);
})
.catch((err) => setError(err.message))
.finally(() => setLoading(false));
}, [location, year]);
return { holidays, loading, error };
}
Create a .env file:
VITE_WORLD_DATA_API_KEY=your_api_key_here
API Call Reference#
While this guide focuses on React, here's how to call the holidays endpoint in other languages:
curl:
curl -X GET "https://worlddataapi.com/v1/holidays/US?year=2026" \
-H "X-API-Key: YOUR_API_KEY"
Python:
import requests
response = requests.get(
"https://worlddataapi.com/v1/holidays/US",
params={"year": 2026},
headers={"X-API-Key": "YOUR_API_KEY"}
)
holidays = response.json()["holidays"]
for holiday in holidays:
print(f"{holiday['date']}: {holiday['name']}")
The Calendar Grid#
Build a month view that highlights holidays:
// src/components/MonthCalendar.jsx
import { useMemo } from "react";
import "./MonthCalendar.css";
const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const MONTHS = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
export function MonthCalendar({ year, month, holidays, onDayClick }) {
const holidayMap = useMemo(() => {
const map = new Map();
for (const h of holidays) {
map.set(h.date, h);
}
return map;
}, [holidays]);
const days = useMemo(() => {
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const startPadding = firstDay.getDay();
const totalDays = lastDay.getDate();
const result = [];
// Empty cells for days before the 1st
for (let i = 0; i < startPadding; i++) {
result.push({ day: null, date: null });
}
// Actual days
for (let day = 1; day <= totalDays; day++) {
const date = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
result.push({
day,
date,
holiday: holidayMap.get(date),
});
}
return result;
}, [year, month, holidayMap]);
return (
<div className="month-calendar">
<h3 className="month-title">
{MONTHS[month]} {year}
</h3>
<div className="calendar-grid">
{DAYS.map((day) => (
<div key={day} className="day-header">
{day}
</div>
))}
{days.map((cell, i) => (
<div
key={i}
className={`day-cell ${cell.holiday ? "holiday" : ""} ${cell.day ? "" : "empty"}`}
onClick={() =>
cell.day && onDayClick?.(cell.date, cell.holiday)
}
title={cell.holiday?.name}
>
{cell.day && (
<>
<span className="day-number">{cell.day}</span>
{cell.holiday && (
<span className="holiday-dot" />
)}
</>
)}
</div>
))}
</div>
</div>
);
}
Styling#
/* src/components/MonthCalendar.css */
.month-calendar {
font-family:
system-ui,
-apple-system,
sans-serif;
max-width: 320px;
}
.month-title {
text-align: center;
margin: 0 0 12px;
font-size: 1.1rem;
font-weight: 600;
}
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 2px;
}
.day-header {
text-align: center;
font-size: 0.75rem;
font-weight: 500;
color: #666;
padding: 8px 0;
}
.day-cell {
aspect-ratio: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
cursor: pointer;
border-radius: 4px;
transition: background-color 0.15s;
}
.day-cell:hover:not(.empty) {
background-color: #f0f0f0;
}
.day-cell.empty {
cursor: default;
}
.day-cell.holiday {
background-color: #fef3c7;
}
.day-cell.holiday:hover {
background-color: #fde68a;
}
.day-number {
font-size: 0.875rem;
}
.holiday-dot {
position: absolute;
bottom: 4px;
width: 4px;
height: 4px;
border-radius: 50%;
background-color: #f59e0b;
}
The Year View#
Combine monthly calendars into a year view:
// src/components/YearCalendar.jsx
import { MonthCalendar } from "./MonthCalendar";
import "./YearCalendar.css";
export function YearCalendar({ year, holidays, onDayClick }) {
return (
<div className="year-calendar">
{Array.from({ length: 12 }, (_, month) => (
<MonthCalendar
key={month}
year={year}
month={month}
holidays={holidays}
onDayClick={onDayClick}
/>
))}
</div>
);
}
/* src/components/YearCalendar.css */
.year-calendar {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 24px;
padding: 16px;
}
The Main Component#
Wire everything together with country selection:
// src/components/HolidayCalendar.jsx
import { useState } from "react";
import { useHolidays } from "../hooks/useHolidays";
import { YearCalendar } from "./YearCalendar";
import { HolidayModal } from "./HolidayModal";
import "./HolidayCalendar.css";
const COUNTRIES = [
{ code: "US", name: "United States" },
{ code: "GB", name: "United Kingdom" },
{ code: "DE", name: "Germany" },
{ code: "FR", name: "France" },
{ code: "JP", name: "Japan" },
{ code: "AU", name: "Australia" },
];
export function HolidayCalendar() {
const currentYear = new Date().getFullYear();
const [country, setCountry] = useState("US");
const [year, setYear] = useState(currentYear);
const [selectedHoliday, setSelectedHoliday] = useState(null);
const { holidays, loading, error } = useHolidays(country, year);
const handleDayClick = (date, holiday) => {
if (holiday) {
setSelectedHoliday(holiday);
}
};
return (
<div className="holiday-calendar">
<header className="calendar-header">
<h1>Holiday Calendar</h1>
<div className="controls">
<select
value={country}
onChange={(e) => setCountry(e.target.value)}
>
{COUNTRIES.map((c) => (
<option key={c.code} value={c.code}>
{c.name}
</option>
))}
</select>
<div className="year-controls">
<button onClick={() => setYear((y) => y - 1)}>←</button>
<span className="year-display">{year}</span>
<button onClick={() => setYear((y) => y + 1)}>→</button>
</div>
</div>
</header>
{loading && <div className="loading">Loading holidays...</div>}
{error && <div className="error">Error: {error}</div>}
{!loading && !error && (
<YearCalendar
year={year}
holidays={holidays}
onDayClick={handleDayClick}
/>
)}
{selectedHoliday && (
<HolidayModal
holiday={selectedHoliday}
onClose={() => setSelectedHoliday(null)}
/>
)}
</div>
);
}
The Holiday Modal#
Show holiday details when clicked:
// src/components/HolidayModal.jsx
import "./HolidayModal.css";
export function HolidayModal({ holiday, onClose }) {
const formattedDate = new Date(
holiday.date + "T00:00:00",
).toLocaleDateString("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
});
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<button className="modal-close" onClick={onClose}>
×
</button>
<h2>{holiday.name}</h2>
<p className="holiday-date">{formattedDate}</p>
<div className="holiday-types">
{holiday.types.map((type) => (
<span key={type} className={`type-badge type-${type}`}>
{type}
</span>
))}
</div>
</div>
</div>
);
}
/* src/components/HolidayModal.css */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.modal-content {
background: white;
padding: 24px;
border-radius: 8px;
max-width: 400px;
width: 90%;
position: relative;
}
.modal-close {
position: absolute;
top: 12px;
right: 12px;
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #666;
}
.modal-content h2 {
margin: 0 0 8px;
}
.holiday-date {
color: #666;
margin: 0 0 16px;
}
.holiday-types {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.type-badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
}
.type-public {
background: #dcfce7;
color: #166534;
}
.type-bank {
background: #dbeafe;
color: #1e40af;
}
.type-observance {
background: #f3e8ff;
color: #6b21a8;
}
Adding Regional Support#
Extend the country selector to include regions:
const LOCATIONS = [
{ code: "US", name: "United States" },
{ code: "US-CA", name: " └ California" },
{ code: "US-TX", name: " └ Texas" },
{ code: "US-NY", name: " └ New York" },
{ code: "DE", name: "Germany" },
{ code: "DE-BY", name: " └ Bavaria" },
{ code: "DE-BE", name: " └ Berlin" },
{ code: "GB", name: "United Kingdom" },
{ code: "GB-SCT", name: " └ Scotland" },
];
Regional queries return both national and regional holidays. Bavaria's 13 holidays include Germany's 9 federal holidays plus 4 state holidays.
Add a sidebar showing upcoming holidays:
// src/components/HolidayList.jsx
export function HolidayList({ holidays }) {
const sortedHolidays = [...holidays].sort(
(a, b) => new Date(a.date) - new Date(b.date),
);
return (
<aside className="holiday-list">
<h3>Holidays</h3>
<ul>
{sortedHolidays.map((holiday) => (
<li key={holiday.date}>
<time dateTime={holiday.date}>
{new Date(
holiday.date + "T00:00:00",
).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
})}
</time>
<span>{holiday.name}</span>
</li>
))}
</ul>
</aside>
);
}
Optimizing Performance#
Prefetch Adjacent Years#
function usePrefetchHolidays(location, year) {
const { holidays, loading, error } = useHolidays(location, year);
// Prefetch next and previous years
useEffect(() => {
if (!loading) {
// These calls hit the cache or trigger background fetches
fetch(
`https://worlddataapi.com/v1/holidays/${location}?year=${year + 1}`,
{
headers: {
"X-API-Key": import.meta.env.VITE_WORLD_DATA_API_KEY,
},
},
).catch(() => {}); // Ignore prefetch errors
fetch(
`https://worlddataapi.com/v1/holidays/${location}?year=${year - 1}`,
{
headers: {
"X-API-Key": import.meta.env.VITE_WORLD_DATA_API_KEY,
},
},
).catch(() => {});
}
}, [location, year, loading]);
return { holidays, loading, error };
}
Persist Cache to localStorage#
const CACHE_KEY = "holiday-cache";
const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
function loadCache() {
try {
const stored = localStorage.getItem(CACHE_KEY);
if (!stored) return new Map();
const { data, timestamp } = JSON.parse(stored);
if (Date.now() - timestamp > CACHE_TTL) {
localStorage.removeItem(CACHE_KEY);
return new Map();
}
return new Map(Object.entries(data));
} catch {
return new Map();
}
}
function saveCache(cache) {
const data = Object.fromEntries(cache);
localStorage.setItem(
CACHE_KEY,
JSON.stringify({
data,
timestamp: Date.now(),
}),
);
}
Accessibility#
Add keyboard navigation and ARIA labels:
<div
role="button"
tabIndex={cell.day ? 0 : -1}
aria-label={cell.holiday
? `${cell.day}, ${cell.holiday.name}`
: `${cell.day}`
}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
cell.day && onDayClick?.(cell.date, cell.holiday);
}
}}
// ... other props
>
Add skip link for the year navigation:
<a href="#calendar-grid" className="skip-link">
Skip to calendar
</a>
Testing#
// src/components/MonthCalendar.test.jsx
import { render, screen } from "@testing-library/react";
import { MonthCalendar } from "./MonthCalendar";
const mockHolidays = [
{ date: "2026-01-01", name: "New Year's Day", types: ["public"] },
{
date: "2026-01-19",
name: "Martin Luther King Jr. Day",
types: ["public"],
},
];
test("renders month with holidays highlighted", () => {
render(<MonthCalendar year={2026} month={0} holidays={mockHolidays} />);
expect(screen.getByText("January 2026")).toBeInTheDocument();
// New Year's Day should have holiday class
const newYearsDay = screen.getByTitle("New Year's Day");
expect(newYearsDay).toHaveClass("holiday");
});
Common Pitfalls#
Parsing dates without timezone awareness
// Wrong: This interprets the date in local timezone
const date = new Date("2026-01-01");
// Right: Append time to prevent timezone shift
const date = new Date("2026-01-01T00:00:00");
Not handling API errors gracefully
Always provide fallback UI when the API is unavailable. Users in regions with unreliable connections will see broken calendars otherwise.
Forgetting regional holidays
A calendar showing only national holidays will miss important dates. German users in Bavaria expect to see Assumption Day (August 15), which isn't a federal holiday. Always offer regional selection when relevant.
Over-fetching data
Without caching, navigating between years triggers redundant API calls. The in-memory cache shown in this guide prevents duplicate requests during a session, and the localStorage cache persists data across sessions.
Ignoring observances
The API returns different holiday types: public, bank, and observance. Filtering to only public holidays misses dates like Valentine's Day or Mother's Day that users may want to see.
Summary#
You've built a holiday calendar component that:
Fetches holiday data from the World Data API with proper caching
Renders a responsive year view with highlighted holidays
Supports regional holiday queries (state and province level)
Handles loading and error states gracefully
Provides accessible keyboard navigation
The component architecture separates data fetching (custom hook) from presentation (calendar components), making it straightforward to adapt for different UI frameworks or design systems.
Ready to add holidays to your application? Get your free API key and start building. The free tier includes 60 requests per day, enough for development and small projects.
Next Steps#
Adding International Holiday Support to Your App — Broader integration patterns
Handling Islamic Holidays in Software — Lunar calendar considerations
How to Build a Timezone-Aware Application — Displaying dates correctly