UI Components Hard
Calendar View
A full-featured monthly calendar with event creation, editing, and deletion. Supports multiple event categories and drag-to-create on day cells.
Open in Lab
MCP
css vanilla-js
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background: #0f172a;
color: #f1f5f9;
min-height: 100vh;
display: flex;
align-items: flex-start;
justify-content: center;
padding: 2rem 1rem;
}
/* ── Wrapper ── */
.cal-wrapper {
width: min(960px, 100%);
display: flex;
flex-direction: column;
gap: 0;
background: #1e293b;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 1.25rem;
overflow: hidden;
}
/* ── Header ── */
.cal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.25rem 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
gap: 1rem;
flex-wrap: wrap;
}
.cal-header-left,
.cal-header-right {
display: flex;
align-items: center;
gap: 0.5rem;
}
.cal-month-label {
font-size: 1.125rem;
font-weight: 700;
color: #f1f5f9;
min-width: 10rem;
text-align: center;
}
/* ── Buttons ── */
.btn-icon {
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 0.5rem;
color: #94a3b8;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.btn-icon:hover {
background: rgba(255, 255, 255, 0.1);
color: #f1f5f9;
}
.btn-primary {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 1rem;
background: #38bdf8;
border: none;
border-radius: 0.625rem;
color: #0f172a;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s;
}
.btn-primary:hover {
background: #7dd3fc;
}
.btn-secondary {
padding: 0.5rem 0.875rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 0.625rem;
color: #94a3b8;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.1);
color: #f1f5f9;
}
.btn-danger {
padding: 0.5rem 0.875rem;
background: rgba(248, 113, 113, 0.12);
border: 1px solid rgba(248, 113, 113, 0.2);
border-radius: 0.625rem;
color: #f87171;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: background 0.15s;
}
.btn-danger:hover {
background: rgba(248, 113, 113, 0.2);
}
/* ── View toggle ── */
.view-toggle {
display: flex;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 0.5rem;
overflow: hidden;
}
.view-btn {
padding: 0.375rem 0.75rem;
background: transparent;
border: none;
color: #64748b;
font-size: 0.8125rem;
font-weight: 500;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.view-btn.active {
background: rgba(56, 189, 248, 0.15);
color: #38bdf8;
}
/* ── Day-of-week header ── */
.cal-dow-header {
display: grid;
grid-template-columns: repeat(7, 1fr);
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
background: rgba(255, 255, 255, 0.02);
}
.cal-dow-header > div {
padding: 0.625rem 0.75rem;
font-size: 0.75rem;
font-weight: 600;
color: #475569;
text-align: center;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* ── Calendar Grid ── */
.cal-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-auto-rows: minmax(112px, 1fr);
border-left: 1px solid rgba(255, 255, 255, 0.04);
}
.cal-cell {
border-right: 1px solid rgba(255, 255, 255, 0.04);
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
padding: 0.375rem;
cursor: pointer;
transition: background 0.12s;
min-height: 112px;
position: relative;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.cal-cell:hover {
background: rgba(255, 255, 255, 0.03);
}
.cal-cell.other-month {
opacity: 0.3;
}
.cal-cell.other-month:hover {
opacity: 0.45;
}
.cal-cell.today .cal-date-num {
background: #38bdf8;
color: #0f172a;
border-radius: 50%;
width: 1.625rem;
height: 1.625rem;
display: flex;
align-items: center;
justify-content: center;
}
.cal-date-num {
font-size: 0.8125rem;
font-weight: 600;
color: #64748b;
align-self: flex-end;
line-height: 1;
padding: 0.2rem 0.3rem;
}
.cal-cell.today .cal-date-num {
color: #0f172a;
padding: 0;
}
/* ── Event pills ── */
.cal-event-pill {
display: block;
width: 100%;
padding: 0.2rem 0.4rem;
border-radius: 0.3rem;
font-size: 0.7rem;
font-weight: 500;
color: #0f172a;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
transition: opacity 0.12s;
line-height: 1.4;
}
.cal-event-pill:hover {
opacity: 0.85;
}
.cal-event-pill.multi-start {
border-radius: 0.3rem 0 0 0.3rem;
}
.cal-event-pill.multi-mid {
border-radius: 0;
opacity: 0.75;
font-size: 0.01px;
color: transparent;
}
.cal-event-pill.multi-end {
border-radius: 0 0.3rem 0.3rem 0;
}
.cal-more-link {
font-size: 0.7rem;
color: #64748b;
padding: 0 0.3rem;
cursor: pointer;
}
.cal-more-link:hover {
color: #38bdf8;
}
/* ── Modal ── */
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
padding: 1rem;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease;
}
.modal-backdrop.open {
opacity: 1;
pointer-events: all;
}
.modal {
background: #1e293b;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 1rem;
width: min(520px, 100%);
max-height: 90vh;
overflow-y: auto;
transform: scale(0.96) translateY(8px);
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.modal-backdrop.open .modal {
transform: scale(1) translateY(0);
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.25rem 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.modal-title {
font-size: 1.0625rem;
font-weight: 700;
color: #f1f5f9;
}
.modal-close {
margin-left: auto;
}
.modal-body {
padding: 1.25rem 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.modal-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.5rem;
border-top: 1px solid rgba(255, 255, 255, 0.06);
}
.modal-footer-right {
display: flex;
gap: 0.5rem;
}
/* ── Form ── */
.form-group {
display: flex;
flex-direction: column;
gap: 0.375rem;
flex: 1;
}
.form-row {
display: flex;
gap: 0.75rem;
}
.form-label {
font-size: 0.8125rem;
font-weight: 500;
color: #94a3b8;
}
.form-input {
width: 100%;
padding: 0.5rem 0.75rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 0.5rem;
color: #f1f5f9;
font-size: 0.875rem;
font-family: inherit;
outline: none;
transition: border-color 0.15s;
}
.form-input:focus {
border-color: #38bdf8;
}
.form-input option {
background: #1e293b;
}
.form-textarea {
resize: vertical;
min-height: 5rem;
}
/* ── Color swatches ── */
.color-swatches {
display: flex;
gap: 0.5rem;
}
.swatch {
width: 1.75rem;
height: 1.75rem;
border-radius: 50%;
border: 2px solid transparent;
cursor: pointer;
transition: transform 0.12s, border-color 0.12s;
}
.swatch:hover {
transform: scale(1.15);
}
.swatch.active {
border-color: #f1f5f9;
transform: scale(1.1);
}
/* ── Week View ── */
.week-grid {
display: none; /* shown via JS */
grid-template-columns: repeat(7, 1fr);
gap: 1px;
background: rgba(255, 255, 255, 0.04);
border-top: 1px solid rgba(255, 255, 255, 0.06);
}
.week-col {
background: #0f172a;
display: flex;
flex-direction: column;
min-height: 480px;
cursor: pointer;
transition: background 0.12s;
}
.week-col:hover {
background: #111827;
}
.week-col--today {
background: rgba(56, 189, 248, 0.04);
}
.week-col--today:hover {
background: rgba(56, 189, 248, 0.07);
}
.week-day-header {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.75rem 0.375rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
gap: 0.25rem;
flex-shrink: 0;
}
.week-day-name {
font-size: 0.6875rem;
font-weight: 600;
color: #475569;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.week-day-num {
font-size: 1rem;
font-weight: 700;
color: #94a3b8;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.today-num {
background: #38bdf8;
color: #0f172a;
}
.week-event-pill {
margin: 0.25rem 0.375rem;
padding: 0.375rem 0.5rem;
border-radius: 0.35rem;
border: none;
color: #0f172a;
font-size: 0.75rem;
font-weight: 600;
text-align: left;
cursor: pointer;
display: flex;
flex-direction: column;
gap: 1px;
transition: opacity 0.15s, transform 0.15s;
width: calc(100% - 0.75rem);
}
.week-event-pill:hover {
opacity: 0.85;
transform: translateY(-1px);
}
.week-event-time {
font-size: 0.65rem;
opacity: 0.8;
font-weight: 500;
}
.week-event-title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* ── Scrollbar ── */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
}(function () {
"use strict";
/* ── State ── */
const today = new Date();
let viewYear = 2026;
let viewMonth = 1; // February — where the demo events are
let viewDay = 15; // mid-month
let currentView = "month";
let events = [
{
id: "e1",
title: "Team Standup",
date: "2026-02-02",
endDate: "2026-02-02",
startTime: "09:00",
endTime: "09:30",
category: "meeting",
color: "#38bdf8",
notes: "Daily sync",
},
{
id: "e2",
title: "Team Standup",
date: "2026-02-03",
endDate: "2026-02-03",
startTime: "09:00",
endTime: "09:30",
category: "meeting",
color: "#38bdf8",
notes: "Daily sync",
},
{
id: "e3",
title: "Team Standup",
date: "2026-02-04",
endDate: "2026-02-04",
startTime: "09:00",
endTime: "09:30",
category: "meeting",
color: "#38bdf8",
notes: "Daily sync",
},
{
id: "e4",
title: "Team Standup",
date: "2026-02-05",
endDate: "2026-02-05",
startTime: "09:00",
endTime: "09:30",
category: "meeting",
color: "#38bdf8",
notes: "Daily sync",
},
{
id: "e5",
title: "Team Standup",
date: "2026-02-06",
endDate: "2026-02-06",
startTime: "09:00",
endTime: "09:30",
category: "meeting",
color: "#38bdf8",
notes: "Daily sync",
},
{
id: "e6",
title: "Product Review",
date: "2026-02-04",
endDate: "2026-02-04",
startTime: "14:00",
endTime: "15:30",
category: "meeting",
color: "#a78bfa",
notes: "Q1 product review with stakeholders",
},
{
id: "e7",
title: "Design Sprint",
date: "2026-02-10",
endDate: "2026-02-12",
startTime: "09:00",
endTime: "17:00",
category: "event",
color: "#fb923c",
notes: "3-day design sprint for new onboarding flow",
},
{
id: "e8",
title: "Q1 Planning",
date: "2026-02-18",
endDate: "2026-02-18",
startTime: "10:00",
endTime: "12:00",
category: "meeting",
color: "#f87171",
notes: "Quarterly planning session",
},
{
id: "e9",
title: "All Hands",
date: "2026-02-25",
endDate: "2026-02-25",
startTime: "11:00",
endTime: "12:00",
category: "meeting",
color: "#34d399",
notes: "Company all-hands meeting",
},
{
id: "e10",
title: "Team Standup",
date: "2026-02-09",
endDate: "2026-02-09",
startTime: "09:00",
endTime: "09:30",
category: "meeting",
color: "#38bdf8",
notes: "Daily sync",
},
{
id: "e11",
title: "Team Standup",
date: "2026-02-11",
endDate: "2026-02-11",
startTime: "09:00",
endTime: "09:30",
category: "meeting",
color: "#38bdf8",
notes: "Daily sync",
},
];
let nextId = 20;
let selectedColor = "#38bdf8";
let editingEventId = null;
/* ── Elements ── */
const grid = document.getElementById("cal-grid");
const monthLabel = document.getElementById("cal-month-label");
const backdrop = document.getElementById("modal-backdrop");
const modalTitle = document.getElementById("modal-title");
const eventId = document.getElementById("event-id");
const eventTitle = document.getElementById("event-title");
const eventDate = document.getElementById("event-date");
const eventEndDate = document.getElementById("event-end-date");
const eventStartTime = document.getElementById("event-start-time");
const eventEndTime = document.getElementById("event-end-time");
const eventCategory = document.getElementById("event-category");
const eventNotes = document.getElementById("event-notes");
const colorSwatches = document.getElementById("color-swatches");
const modalDelete = document.getElementById("modal-delete");
/* ── Time options ── */
(function populateTimes() {
const times = [];
for (let h = 0; h < 24; h++) {
for (let m of [0, 30]) {
const hh = String(h).padStart(2, "0");
const mm = String(m).padStart(2, "0");
const ampm = h < 12 ? "AM" : "PM";
const displayH = h % 12 || 12;
times.push({ val: `${hh}:${mm}`, label: `${displayH}:${mm} ${ampm}` });
}
}
times.forEach((t) => {
const o1 = new Option(t.label, t.val);
const o2 = new Option(t.label, t.val);
eventStartTime.appendChild(o1);
eventEndTime.appendChild(o2);
});
eventStartTime.value = "09:00";
eventEndTime.value = "10:00";
})();
/* ── Month/Week navigation ── */
document.getElementById("cal-prev").addEventListener("click", () => {
if (currentView === "week") {
const ws = getWeekStart(viewYear, viewMonth, viewDay || 1);
ws.setDate(ws.getDate() - 7);
viewYear = ws.getFullYear();
viewMonth = ws.getMonth();
viewDay = ws.getDate() + 3; // middle of week
} else {
viewMonth--;
if (viewMonth < 0) {
viewMonth = 11;
viewYear--;
}
}
renderView();
});
document.getElementById("cal-next").addEventListener("click", () => {
if (currentView === "week") {
const ws = getWeekStart(viewYear, viewMonth, viewDay || 1);
ws.setDate(ws.getDate() + 7);
viewYear = ws.getFullYear();
viewMonth = ws.getMonth();
viewDay = ws.getDate() + 3;
} else {
viewMonth++;
if (viewMonth > 11) {
viewMonth = 0;
viewYear++;
}
}
renderView();
});
document.getElementById("cal-today").addEventListener("click", () => {
viewYear = today.getFullYear();
viewMonth = today.getMonth();
viewDay = today.getDate();
renderView();
});
/* ── View toggle ── */
document.querySelectorAll(".view-btn").forEach((btn) => {
btn.addEventListener("click", () => {
document.querySelectorAll(".view-btn").forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
currentView = btn.dataset.view; // get 'month' or 'week' from data-view attribute
renderView();
});
});
/* ── New event button ── */
document.getElementById("cal-new-event").addEventListener("click", () => openModal(null));
/* ── Color swatches ── */
colorSwatches.addEventListener("click", (e) => {
const swatch = e.target.closest(".swatch");
if (!swatch) return;
colorSwatches.querySelectorAll(".swatch").forEach((s) => s.classList.remove("active"));
swatch.classList.add("active");
selectedColor = swatch.dataset.color;
});
/* ── Modal controls ── */
document.getElementById("modal-close").addEventListener("click", closeModal);
document.getElementById("modal-cancel").addEventListener("click", closeModal);
backdrop.addEventListener("click", (e) => {
if (e.target === backdrop) closeModal();
});
document.getElementById("modal-save").addEventListener("click", saveEvent);
modalDelete.addEventListener("click", deleteEvent);
/* ── Keyboard ── */
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") closeModal();
});
/* ── renderView dispatcher ── */
function renderView() {
if (currentView === "week") {
renderWeekView();
} else {
renderCalendar();
}
}
/* ── Render calendar (month view) ── */
function renderCalendar() {
// Hide week grid if it exists, show month grid
const weekGrid = document.getElementById("week-grid");
if (weekGrid) weekGrid.style.display = "none";
grid.style.display = "grid";
const MONTHS = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
monthLabel.textContent = `${MONTHS[viewMonth]} ${viewYear}`;
grid.innerHTML = "";
const firstDay = new Date(viewYear, viewMonth, 1).getDay();
const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate();
const prevMonthDays = new Date(viewYear, viewMonth, 0).getDate();
// Total cells: always 6 rows × 7 cols = 42
const totalCells = 42;
for (let i = 0; i < totalCells; i++) {
const cell = document.createElement("div");
cell.className = "cal-cell";
let cellDate;
if (i < firstDay) {
// Previous month
const d = prevMonthDays - firstDay + i + 1;
cellDate = new Date(viewYear, viewMonth - 1, d);
cell.classList.add("other-month");
} else if (i >= firstDay + daysInMonth) {
// Next month
const d = i - firstDay - daysInMonth + 1;
cellDate = new Date(viewYear, viewMonth + 1, d);
cell.classList.add("other-month");
} else {
// Current month
const d = i - firstDay + 1;
cellDate = new Date(viewYear, viewMonth, d);
if (isSameDay(cellDate, today)) cell.classList.add("today");
}
const dateStr = formatDate(cellDate);
// Date number
const dateNum = document.createElement("span");
dateNum.className = "cal-date-num";
dateNum.textContent = cellDate.getDate();
cell.appendChild(dateNum);
// Events for this cell
const cellEvents = getEventsForDate(cellDate);
const maxVisible = 2;
cellEvents.slice(0, maxVisible).forEach((ev) => {
const pill = createEventPill(ev, cellDate);
cell.appendChild(pill);
});
if (cellEvents.length > maxVisible) {
const more = document.createElement("span");
more.className = "cal-more-link";
more.textContent = `+${cellEvents.length - maxVisible} more`;
cell.appendChild(more);
}
// Click cell to add event
cell.addEventListener("click", (e) => {
if (e.target.closest(".cal-event-pill")) return;
openModal(null, dateStr);
});
grid.appendChild(cell);
}
}
/* ── Render week view ── */
function renderWeekView() {
const MONTHS_SHORT = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
const startOfWeek = getWeekStart(viewYear, viewMonth, viewDay || 1);
// Update the label to show the week range
const endOfWeek = new Date(startOfWeek);
endOfWeek.setDate(endOfWeek.getDate() + 6);
monthLabel.textContent = `${MONTHS_SHORT[startOfWeek.getMonth()]} ${startOfWeek.getDate()} \u2013 ${MONTHS_SHORT[endOfWeek.getMonth()]} ${endOfWeek.getDate()}, ${endOfWeek.getFullYear()}`;
// Hide month grid, show week grid
grid.style.display = "none";
let weekGrid = document.getElementById("week-grid");
if (!weekGrid) {
weekGrid = document.createElement("div");
weekGrid.id = "week-grid";
weekGrid.className = "week-grid";
grid.parentNode.insertBefore(weekGrid, grid.nextSibling);
}
weekGrid.style.display = "grid";
weekGrid.innerHTML = "";
const DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
for (let d = 0; d < 7; d++) {
const dayDate = new Date(startOfWeek);
dayDate.setDate(startOfWeek.getDate() + d);
const isToday = isSameDay(dayDate, today);
const col = document.createElement("div");
col.className = `week-col${isToday ? " week-col--today" : ""}`;
// Day header
const header = document.createElement("div");
header.className = "week-day-header";
header.innerHTML = `<span class="week-day-name">${DAY_NAMES[dayDate.getDay()]}</span><span class="week-day-num${isToday ? " today-num" : ""}">${dayDate.getDate()}</span>`;
col.appendChild(header);
// Click column to add event
col.addEventListener("click", (e) => {
if (e.target.closest(".week-event-pill")) return;
openModal(null, formatDate(dayDate));
});
// Events for this day
const dayEvents = getEventsForDate(dayDate);
dayEvents.forEach((ev) => {
const pill = document.createElement("button");
pill.className = "week-event-pill";
pill.style.background = ev.color;
pill.innerHTML = `<span class="week-event-time">${ev.startTime}</span><span class="week-event-title">${ev.title}</span>`;
pill.addEventListener("click", (e) => {
e.stopPropagation();
openModal(ev.id);
});
col.appendChild(pill);
});
weekGrid.appendChild(col);
}
}
/* ── Get Monday (Sunday) of the week ── */
function getWeekStart(year, month, day) {
const d = new Date(year, month, day);
const dow = d.getDay(); // 0=Sun
d.setDate(d.getDate() - dow); // Go to Sunday
return d;
}
function createEventPill(ev, cellDate) {
const pill = document.createElement("button");
pill.className = "cal-event-pill";
const startDate = parseDate(ev.date);
const endDate = parseDate(ev.endDate || ev.date);
const isMultiDay = !isSameDay(startDate, endDate);
if (isMultiDay) {
if (isSameDay(cellDate, startDate)) {
pill.classList.add("multi-start");
pill.textContent = ev.title;
} else if (isSameDay(cellDate, endDate)) {
pill.classList.add("multi-end");
pill.textContent = ev.title;
} else {
pill.classList.add("multi-mid");
pill.textContent = ev.title;
}
} else {
pill.textContent = ev.title;
}
pill.style.background = ev.color;
pill.title = `${ev.title} — ${ev.startTime}`;
pill.addEventListener("click", (e) => {
e.stopPropagation();
openModal(ev.id);
});
return pill;
}
function getEventsForDate(date) {
const dateStr = formatDate(date);
return events
.filter((ev) => {
const start = ev.date;
const end = ev.endDate || ev.date;
return dateStr >= start && dateStr <= end;
})
.sort((a, b) => a.startTime.localeCompare(b.startTime));
}
/* ── Modal ── */
function openModal(id, dateStr) {
editingEventId = id || null;
selectedColor = "#38bdf8";
if (id) {
const ev = events.find((e) => e.id === id);
if (!ev) return;
modalTitle.textContent = "Edit Event";
eventId.value = ev.id;
eventTitle.value = ev.title;
eventDate.value = ev.date;
eventEndDate.value = ev.endDate || ev.date;
eventStartTime.value = ev.startTime;
eventEndTime.value = ev.endTime;
eventCategory.value = ev.category;
eventNotes.value = ev.notes || "";
selectedColor = ev.color;
modalDelete.style.display = "block";
} else {
modalTitle.textContent = "New Event";
eventId.value = "";
eventTitle.value = "";
eventDate.value = dateStr || formatDate(today);
eventEndDate.value = dateStr || formatDate(today);
eventStartTime.value = "09:00";
eventEndTime.value = "10:00";
eventCategory.value = "meeting";
eventNotes.value = "";
modalDelete.style.display = "none";
}
// Sync swatch selection
colorSwatches.querySelectorAll(".swatch").forEach((s) => {
s.classList.toggle("active", s.dataset.color === selectedColor);
});
backdrop.classList.add("open");
backdrop.removeAttribute("aria-hidden");
eventTitle.focus();
}
function closeModal() {
backdrop.classList.remove("open");
backdrop.setAttribute("aria-hidden", "true");
editingEventId = null;
}
function saveEvent() {
const title = eventTitle.value.trim();
if (!title) {
eventTitle.focus();
return;
}
const ev = {
id: editingEventId || `e${nextId++}`,
title,
date: eventDate.value,
endDate: eventEndDate.value || eventDate.value,
startTime: eventStartTime.value,
endTime: eventEndTime.value,
category: eventCategory.value,
color: selectedColor,
notes: eventNotes.value.trim(),
};
if (editingEventId) {
const idx = events.findIndex((e) => e.id === editingEventId);
if (idx > -1) events[idx] = ev;
} else {
events.push(ev);
}
closeModal();
renderView();
}
function deleteEvent() {
if (!editingEventId) return;
events = events.filter((e) => e.id !== editingEventId);
closeModal();
renderView();
}
/* ── Helpers ── */
function formatDate(d) {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`;
}
function parseDate(str) {
const [y, m, d] = str.split("-").map(Number);
return new Date(y, m - 1, d);
}
function isSameDay(a, b) {
return (
a.getFullYear() === b.getFullYear() &&
a.getMonth() === b.getMonth() &&
a.getDate() === b.getDate()
);
}
/* ── Init ── */
renderView();
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Calendar View</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="cal-wrapper">
<!-- Header -->
<div class="cal-header">
<div class="cal-header-left">
<button class="btn-icon" id="cal-prev" aria-label="Previous month">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="m15 18-6-6 6-6"/></svg>
</button>
<h2 class="cal-month-label" id="cal-month-label">February 2026</h2>
<button class="btn-icon" id="cal-next" aria-label="Next month">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="m9 18 6-6-6-6"/></svg>
</button>
<button class="btn-secondary" id="cal-today">Today</button>
</div>
<div class="cal-header-right">
<div class="view-toggle">
<button class="view-btn active" data-view="month">Month</button>
<button class="view-btn" data-view="week">Week</button>
</div>
<button class="btn-primary" id="cal-new-event">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M12 5v14M5 12h14"/></svg>
New Event
</button>
</div>
</div>
<!-- Day of week headers -->
<div class="cal-dow-header">
<div>Sun</div><div>Mon</div><div>Tue</div><div>Wed</div>
<div>Thu</div><div>Fri</div><div>Sat</div>
</div>
<!-- Calendar grid -->
<div class="cal-grid" id="cal-grid"></div>
</div>
<!-- Event Modal -->
<div class="modal-backdrop" id="modal-backdrop" aria-hidden="true">
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title">
<div class="modal-header">
<h3 class="modal-title" id="modal-title">New Event</h3>
<button class="btn-icon modal-close" id="modal-close" aria-label="Close">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M18 6 6 18M6 6l12 12"/></svg>
</button>
</div>
<div class="modal-body">
<input type="hidden" id="event-id" />
<div class="form-group">
<label class="form-label" for="event-title">Event Title</label>
<input class="form-input" type="text" id="event-title" placeholder="e.g. Team Meeting" />
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label" for="event-date">Date</label>
<input class="form-input" type="date" id="event-date" />
</div>
<div class="form-group">
<label class="form-label" for="event-end-date">End Date</label>
<input class="form-input" type="date" id="event-end-date" />
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label" for="event-start-time">Start Time</label>
<select class="form-input" id="event-start-time"></select>
</div>
<div class="form-group">
<label class="form-label" for="event-end-time">End Time</label>
<select class="form-input" id="event-end-time"></select>
</div>
</div>
<div class="form-group">
<label class="form-label" for="event-category">Category</label>
<select class="form-input" id="event-category">
<option value="meeting">Meeting</option>
<option value="event">Event</option>
<option value="reminder">Reminder</option>
<option value="holiday">Holiday</option>
<option value="task">Task</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Color</label>
<div class="color-swatches" id="color-swatches">
<button class="swatch active" data-color="#38bdf8" style="background:#38bdf8" aria-label="Sky blue"></button>
<button class="swatch" data-color="#a78bfa" style="background:#a78bfa" aria-label="Purple"></button>
<button class="swatch" data-color="#fb923c" style="background:#fb923c" aria-label="Orange"></button>
<button class="swatch" data-color="#f87171" style="background:#f87171" aria-label="Red"></button>
<button class="swatch" data-color="#34d399" style="background:#34d399" aria-label="Teal"></button>
<button class="swatch" data-color="#fbbf24" style="background:#fbbf24" aria-label="Yellow"></button>
</div>
</div>
<div class="form-group">
<label class="form-label" for="event-notes">Notes</label>
<textarea class="form-input form-textarea" id="event-notes" placeholder="Optional notes..."></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn-danger" id="modal-delete" style="display:none">Delete</button>
<div class="modal-footer-right">
<button class="btn-secondary" id="modal-cancel">Cancel</button>
<button class="btn-primary" id="modal-save">Save Event</button>
</div>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>Calendar View
A full-featured monthly calendar component with event management. Create, edit, and delete events directly from the grid, with support for multi-day spans, event categories, and color-coding. Events are stored in-memory and rendered dynamically on a navigable month grid.
Features
- Month grid with day cells, today highlight, and event pills
- Create events by clicking any day cell — modal pre-fills the date
- Edit and delete existing events via click-to-open modal
- Multi-day events span across cells with continuous pill rendering
- 5 event category types: Meeting, Event, Reminder, Holiday, Task
- 6 color swatches per event for quick visual grouping
- Navigate months with Prev / Today / Next controls