UI Components Hard
Availability Calendar
A Calendly-style availability picker for scheduling meetings. Shows available time slots across a monthly calendar with time zone support and booking confirmation flow.
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: center;
justify-content: center;
padding: 2rem 1rem;
}
/* ── Card ── */
.av-card {
background: #1e293b;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 1.25rem;
width: min(760px, 100%);
overflow: hidden;
}
/* ── Person header ── */
.av-person {
display: flex;
align-items: center;
gap: 0.875rem;
padding: 1.25rem 1.5rem 0;
}
.av-avatar {
width: 3rem;
height: 3rem;
border-radius: 50%;
background: linear-gradient(135deg, #38bdf8, #a78bfa);
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
font-weight: 800;
color: #0f172a;
flex-shrink: 0;
}
.av-person-name {
font-size: 1rem;
font-weight: 700;
color: #f1f5f9;
}
.av-person-meta {
display: flex;
align-items: center;
gap: 0.3rem;
font-size: 0.8125rem;
color: #64748b;
margin-top: 0.2rem;
}
.av-title {
font-size: 0.9375rem;
font-weight: 600;
color: #94a3b8;
padding: 0.5rem 1.5rem 1.25rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
/* ── Content area ── */
.av-content {
position: relative;
overflow: hidden;
}
.av-screen {
animation: slideIn 0.25s ease forwards;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(12px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* ── Two columns layout ── */
.av-cols {
display: flex;
align-items: flex-start;
flex-wrap: wrap;
}
/* ── Calendar column ── */
.av-cal-col {
flex: 1;
padding: 1.5rem;
min-width: 260px;
}
.av-cal-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
}
.av-cal-title {
font-size: 0.9375rem;
font-weight: 700;
color: #f1f5f9;
}
.av-nav-btn {
display: flex;
align-items: center;
justify-content: center;
width: 1.875rem;
height: 1.875rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 0.4375rem;
color: #94a3b8;
cursor: pointer;
transition: background 0.12s, color 0.12s;
}
.av-nav-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: #f1f5f9;
}
.av-dow-header {
display: grid;
grid-template-columns: repeat(7, 1fr);
margin-bottom: 0.25rem;
}
.av-dow-header span {
text-align: center;
font-size: 0.6875rem;
font-weight: 600;
color: #475569;
padding: 0.25rem 0;
text-transform: uppercase;
}
/* ── Calendar grid ── */
.av-cal-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
}
.av-day {
display: flex;
align-items: center;
justify-content: center;
aspect-ratio: 1;
font-size: 0.8125rem;
font-weight: 500;
border-radius: 50%;
cursor: default;
transition: background 0.12s, color 0.12s;
position: relative;
color: #475569;
}
.av-day.available {
color: #e2e8f0;
cursor: pointer;
font-weight: 600;
}
.av-day.available:hover {
background: rgba(56, 189, 248, 0.15);
color: #f1f5f9;
}
.av-day.selected {
background: #38bdf8;
color: #0f172a;
font-weight: 700;
}
.av-day.selected:hover {
background: #7dd3fc;
}
.av-day.today::after {
content: "";
position: absolute;
bottom: 3px;
left: 50%;
transform: translateX(-50%);
width: 4px;
height: 4px;
border-radius: 50%;
background: #38bdf8;
}
.av-day.selected.today::after {
background: #0f172a;
}
.av-day.other-month {
color: #334155;
}
/* ── Divider ── */
.av-divider {
width: 1px;
background: rgba(255, 255, 255, 0.06);
align-self: stretch;
margin: 1.5rem 0;
}
/* ── Slots column ── */
.av-slots-col {
flex: 1;
min-width: 200px;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.av-slots-header {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.av-slots-day {
font-size: 0.9375rem;
font-weight: 700;
color: #f1f5f9;
}
.av-tz-select-wrap {
display: flex;
align-items: center;
gap: 0.375rem;
color: #64748b;
}
.av-tz-select {
background: transparent;
border: none;
color: #64748b;
font-size: 0.75rem;
font-family: inherit;
cursor: pointer;
outline: none;
}
.av-tz-select option {
background: #1e293b;
color: #f1f5f9;
}
.av-slots-list {
display: flex;
flex-direction: column;
gap: 0.4375rem;
max-height: 340px;
overflow-y: auto;
padding-right: 0.25rem;
}
/* ── Slot button ── */
.av-slot {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.av-slot-btn {
width: 100%;
padding: 0.5625rem 1rem;
background: rgba(56, 189, 248, 0.08);
border: 1px solid rgba(56, 189, 248, 0.2);
border-radius: 0.5rem;
color: #38bdf8;
font-size: 0.875rem;
font-weight: 600;
text-align: center;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
.av-slot-btn:hover {
background: rgba(56, 189, 248, 0.18);
}
.av-slot-btn.selected {
background: #38bdf8;
color: #0f172a;
border-color: #38bdf8;
}
.av-slot-btn.booked {
background: rgba(255, 255, 255, 0.04);
border-color: rgba(255, 255, 255, 0.06);
color: #334155;
cursor: not-allowed;
}
.av-slot-btn.booked:hover {
background: rgba(255, 255, 255, 0.04);
}
.av-slot-confirm {
display: none;
animation: slideDown 0.18s ease forwards;
}
.av-slot-confirm.visible {
display: block;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.av-confirm-btn {
width: 100%;
padding: 0.5rem;
background: #38bdf8;
border: none;
border-radius: 0.4375rem;
color: #0f172a;
font-size: 0.8125rem;
font-weight: 700;
cursor: pointer;
transition: background 0.15s;
}
.av-confirm-btn:hover {
background: #7dd3fc;
}
/* ── Booking form screen ── */
.av-booking-info {
padding: 1.25rem 1.5rem 0;
background: rgba(56, 189, 248, 0.05);
border-bottom: 1px solid rgba(56, 189, 248, 0.1);
display: flex;
gap: 1.5rem;
flex-wrap: wrap;
}
.av-booking-detail {
display: flex;
align-items: center;
gap: 0.4rem;
font-size: 0.8125rem;
color: #64748b;
padding-bottom: 1.25rem;
}
.av-booking-detail span {
color: #e2e8f0;
font-weight: 600;
}
.av-form-title {
font-size: 1rem;
font-weight: 700;
color: #f1f5f9;
padding: 1.25rem 1.5rem 0;
}
.av-form {
padding: 1rem 1.5rem 1.5rem;
display: flex;
flex-direction: column;
gap: 0.875rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.form-label {
font-size: 0.8125rem;
font-weight: 500;
color: #94a3b8;
}
.optional {
color: #475569;
font-weight: 400;
}
.form-input {
width: 100%;
padding: 0.5625rem 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-textarea {
resize: vertical;
min-height: 80px;
}
.av-form-actions {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 0.25rem;
}
/* ── Buttons ── */
.btn-primary {
padding: 0.625rem 1.25rem;
background: #38bdf8;
border: none;
border-radius: 0.5rem;
color: #0f172a;
font-size: 0.875rem;
font-weight: 700;
cursor: pointer;
transition: background 0.15s;
}
.btn-primary:hover {
background: #7dd3fc;
}
.btn-ghost {
padding: 0.5rem 0.75rem;
background: transparent;
border: none;
color: #64748b;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: color 0.15s;
}
.btn-ghost:hover {
color: #f1f5f9;
}
/* ── Success screen ── */
.av-success {
display: flex;
flex-direction: column;
align-items: center;
padding: 3rem 1.5rem 2.5rem;
gap: 1rem;
text-align: center;
}
.av-success-check {
width: 72px;
height: 72px;
border-radius: 50%;
background: #38bdf8;
display: flex;
align-items: center;
justify-content: center;
animation: popIn 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
@keyframes popIn {
from {
transform: scale(0);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
.av-success-title {
font-size: 1.375rem;
font-weight: 800;
color: #f1f5f9;
}
.av-success-subtitle {
font-size: 0.875rem;
color: #64748b;
}
.av-success-details {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 0.75rem;
padding: 1rem 1.25rem;
width: 100%;
max-width: 360px;
text-align: left;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.av-detail-row {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
color: #94a3b8;
}
.av-detail-row span {
color: #e2e8f0;
font-weight: 600;
}
.av-success-actions {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
justify-content: center;
}
/* ── Scrollbar ── */
.av-slots-list::-webkit-scrollbar {
width: 4px;
}
.av-slots-list::-webkit-scrollbar-track {
background: transparent;
}
.av-slots-list::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
}(function () {
"use strict";
/* ── Config ── */
const TODAY = new Date(2026, 2, 2); // Mar 2, 2026
const MONTHS = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const LONG_DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
const SHORT_MONTHS = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
// Blocked specific dates (0-indexed month, day): unavailable weekdays
const BLOCKED = [
"2026-02-13",
"2026-02-20",
"2026-03-06",
"2026-03-13",
"2026-03-20",
"2026-03-27",
];
// Pre-booked time slots (stored in UTC-5 base)
const BOOKED_SLOTS = {
"2026-02-18": ["11:00", "14:30", "16:00"],
"2026-02-24": ["09:00", "10:30"],
"2026-03-09": ["13:00", "15:00"],
"2026-03-10": ["09:00", "10:30"],
"2026-03-11": ["11:00", "14:00"],
"2026-03-16": ["09:30", "13:00", "15:30"],
"2026-03-17": ["10:00"],
"2026-03-18": ["11:00", "16:00"],
};
let calYear = 2026;
let calMonth = 2; // March
let selectedDate = null;
let selectedSlot = null;
let tzOffset = -5; // default UTC-5
/* ── Screens ── */
function showScreen(id) {
["screen-slots", "screen-form", "screen-success"].forEach((s) => {
const el = document.getElementById(s);
el.style.display = s === id ? "block" : "none";
});
}
/* ── Calendar render ── */
function renderCalendar() {
document.getElementById("av-cal-title").textContent = `${MONTHS[calMonth]} ${calYear}`;
const grid = document.getElementById("av-cal-grid");
grid.innerHTML = "";
// Week starts Monday (ISO)
const firstDayOfMonth = new Date(calYear, calMonth, 1);
let startDay = firstDayOfMonth.getDay(); // 0=Sun
startDay = startDay === 0 ? 6 : startDay - 1; // convert to Mon=0
const daysInMonth = new Date(calYear, calMonth + 1, 0).getDate();
const prevDays = new Date(calYear, calMonth, 0).getDate();
for (let i = 0; i < 42; i++) {
const btn = document.createElement("button");
btn.className = "av-day";
let d;
if (i < startDay) {
d = new Date(calYear, calMonth - 1, prevDays - startDay + i + 1);
btn.classList.add("other-month");
btn.textContent = d.getDate();
btn.disabled = true;
} else if (i >= startDay + daysInMonth) {
d = new Date(calYear, calMonth + 1, i - startDay - daysInMonth + 1);
btn.classList.add("other-month");
btn.textContent = d.getDate();
btn.disabled = true;
} else {
const dayNum = i - startDay + 1;
d = new Date(calYear, calMonth, dayNum);
btn.textContent = dayNum;
const dateStr = formatDateISO(d);
const isWeekend = d.getDay() === 0 || d.getDay() === 6;
const isPast = d < new Date(TODAY.getFullYear(), TODAY.getMonth(), TODAY.getDate());
const isBlocked = BLOCKED.includes(dateStr);
if (!isWeekend && !isPast && !isBlocked) {
btn.classList.add("available");
btn.addEventListener("click", () => selectDate(d));
}
if (isSameDay(d, TODAY)) btn.classList.add("today");
if (selectedDate && isSameDay(d, selectedDate)) btn.classList.add("selected");
}
grid.appendChild(btn);
}
}
/* ── Select date ── */
function selectDate(d) {
selectedDate = d;
selectedSlot = null;
renderCalendar();
const slotsCol = document.getElementById("av-slots-col");
const divider = document.getElementById("av-divider");
slotsCol.style.display = "flex";
divider.style.display = "block";
const DAYS_LONG = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
];
document.getElementById("av-slots-day").textContent =
`${DAYS_LONG[d.getDay()]}, ${SHORT_MONTHS[d.getMonth()]} ${d.getDate()}`;
renderSlots();
}
/* ── Render slots ── */
function renderSlots() {
const list = document.getElementById("av-slots-list");
list.innerHTML = "";
const dateStr = selectedDate ? formatDateISO(selectedDate) : "";
const booked = BOOKED_SLOTS[dateStr] || [];
// Generate 9am-5pm in 30min intervals in base UTC-5
for (let hour = 9; hour < 17; hour++) {
for (let min of [0, 30]) {
const baseH = hour;
const baseM = min;
// Apply tz offset (tzOffset relative to -5 base)
const diff = tzOffset - -5;
let displayH = baseH + diff;
let displayM = baseM;
if (displayH < 0) displayH += 24;
if (displayH >= 24) displayH -= 24;
const slotKey = `${String(baseH).padStart(2, "0")}:${String(baseM).padStart(2, "0")}`;
const isBooked = booked.includes(slotKey);
const ampm = displayH < 12 ? "AM" : "PM";
const h12 = displayH % 12 || 12;
const displayLabel = `${h12}:${String(displayM).padStart(2, "0")} ${ampm}`;
const slotWrap = document.createElement("div");
slotWrap.className = "av-slot";
const slotBtn = document.createElement("button");
slotBtn.className = `av-slot-btn${isBooked ? " booked" : ""}`;
slotBtn.textContent = displayLabel;
slotBtn.disabled = isBooked;
slotBtn.dataset.slot = slotKey;
slotBtn.dataset.label = displayLabel;
const confirmWrap = document.createElement("div");
confirmWrap.className = "av-slot-confirm";
const confirmBtn = document.createElement("button");
confirmBtn.className = "av-confirm-btn";
confirmBtn.textContent = "Confirm";
confirmWrap.appendChild(confirmBtn);
if (!isBooked) {
slotBtn.addEventListener("click", () => {
// Deselect all other slots
document
.querySelectorAll(".av-slot-btn")
.forEach((b) => b.classList.remove("selected"));
document
.querySelectorAll(".av-slot-confirm")
.forEach((c) => c.classList.remove("visible"));
const alreadySelected = selectedSlot === slotKey;
if (alreadySelected) {
selectedSlot = null;
} else {
selectedSlot = slotKey;
slotBtn.classList.add("selected");
confirmWrap.classList.add("visible");
}
});
confirmBtn.addEventListener("click", () => {
openBookingForm(slotBtn.dataset.label);
});
}
slotWrap.appendChild(slotBtn);
slotWrap.appendChild(confirmWrap);
list.appendChild(slotWrap);
}
}
}
/* ── Booking form ── */
function openBookingForm(slotLabel) {
const info = document.getElementById("av-booking-info");
const DAYS_LONG = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
];
const dateLabel = selectedDate
? `${DAYS_LONG[selectedDate.getDay()]}, ${SHORT_MONTHS[selectedDate.getMonth()]} ${selectedDate.getDate()}, ${selectedDate.getFullYear()}`
: "";
info.innerHTML = `
<div class="av-booking-detail">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>
<span>${dateLabel}</span>
</div>
<div class="av-booking-detail">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 8v4l3 3"/></svg>
<span>${slotLabel}</span> · 30 min
</div>
`;
showScreen("screen-form");
}
/* ── Form submit ── */
document.getElementById("av-form").addEventListener("submit", (e) => {
e.preventDefault();
const name = document.getElementById("av-name").value.trim();
const email = document.getElementById("av-email").value.trim();
if (!name || !email) return;
showSuccessScreen(name);
});
document.getElementById("av-back").addEventListener("click", () => showScreen("screen-slots"));
/* ── Success screen ── */
function showSuccessScreen(name) {
const DAYS_LONG = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
];
const dateLabel = selectedDate
? `${DAYS_LONG[selectedDate.getDay()]}, ${SHORT_MONTHS[selectedDate.getMonth()]} ${selectedDate.getDate()}, ${selectedDate.getFullYear()}`
: "";
const slotLabel =
document.querySelector(".av-slot-btn.selected")?.dataset.label || selectedSlot || "";
const details = document.getElementById("av-success-details");
details.innerHTML = `
<div class="av-detail-row">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#64748b" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>
<span>${dateLabel}</span>
</div>
<div class="av-detail-row">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#64748b" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 8v4l3 3"/></svg>
<span>${slotLabel}</span>
</div>
<div class="av-detail-row">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#64748b" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="m12 8-3.5 4 3.5 2.5 3.5-2.5z"/></svg>
30-minute call with James Doe
</div>
`;
showScreen("screen-success");
}
document.getElementById("av-add-cal").addEventListener("click", () => {
alert("In a real app, this would download an .ics file or open Google Calendar.");
});
document.getElementById("av-schedule-another").addEventListener("click", () => {
selectedDate = null;
selectedSlot = null;
document.getElementById("av-slots-col").style.display = "none";
document.getElementById("av-divider").style.display = "none";
document.getElementById("av-name").value = "";
document.getElementById("av-email").value = "";
document.getElementById("av-notes").value = "";
renderCalendar();
showScreen("screen-slots");
});
/* ── Navigation ── */
document.getElementById("av-prev").addEventListener("click", () => {
calMonth--;
if (calMonth < 0) {
calMonth = 11;
calYear--;
}
renderCalendar();
});
document.getElementById("av-next").addEventListener("click", () => {
calMonth++;
if (calMonth > 11) {
calMonth = 0;
calYear++;
}
renderCalendar();
});
/* ── Timezone ── */
document.getElementById("av-tz-select").addEventListener("change", (e) => {
tzOffset = parseInt(e.target.value);
if (selectedDate) renderSlots();
});
/* ── Helpers ── */
function isSameDay(a, b) {
return (
a.getFullYear() === b.getFullYear() &&
a.getMonth() === b.getMonth() &&
a.getDate() === b.getDate()
);
}
function formatDateISO(d) {
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
}
/* ── Init ── */
renderCalendar();
showScreen("screen-slots");
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Availability Calendar</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="av-card">
<!-- Person info header -->
<div class="av-person">
<div class="av-avatar">JD</div>
<div class="av-person-info">
<div class="av-person-name">James Doe</div>
<div class="av-person-meta">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 8v4l3 3"/></svg>
30-min call
</div>
</div>
</div>
<h2 class="av-title">Schedule a Meeting</h2>
<!-- Main content area -->
<div class="av-content" id="av-content">
<!-- ── SCREEN 1: Calendar + Slots ── -->
<div class="av-screen" id="screen-slots">
<div class="av-cols">
<!-- Left: Calendar -->
<div class="av-cal-col">
<div class="av-cal-header">
<button class="av-nav-btn" id="av-prev" aria-label="Previous month">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="m15 18-6-6 6-6"/></svg>
</button>
<span class="av-cal-title" id="av-cal-title">February 2026</span>
<button class="av-nav-btn" id="av-next" aria-label="Next month">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="m9 18 6-6-6-6"/></svg>
</button>
</div>
<div class="av-dow-header">
<span>Mo</span><span>Tu</span><span>We</span>
<span>Th</span><span>Fr</span><span>Sa</span><span>Su</span>
</div>
<div class="av-cal-grid" id="av-cal-grid"></div>
</div>
<!-- Divider -->
<div class="av-divider" id="av-divider" style="display:none"></div>
<!-- Right: Time slots -->
<div class="av-slots-col" id="av-slots-col" style="display:none">
<div class="av-slots-header">
<div class="av-slots-day" id="av-slots-day"></div>
<div class="av-tz-select-wrap">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 2a15 15 0 0 1 0 20M12 2a15 15 0 0 0 0 20M2 12h20"/></svg>
<select class="av-tz-select" id="av-tz-select">
<option value="-5">UTC-5 / New York</option>
<option value="-6">UTC-6 / Chicago</option>
<option value="-7">UTC-7 / Denver</option>
<option value="-8">UTC-8 / Los Angeles</option>
<option value="0">UTC+0 / London</option>
<option value="1">UTC+1 / Paris</option>
</select>
</div>
</div>
<div class="av-slots-list" id="av-slots-list"></div>
</div>
</div>
</div>
<!-- ── SCREEN 2: Booking form ── -->
<div class="av-screen" id="screen-form" style="display:none">
<div class="av-booking-info" id="av-booking-info"></div>
<h3 class="av-form-title">Your Details</h3>
<form class="av-form" id="av-form" novalidate>
<div class="form-group">
<label class="form-label" for="av-name">Name</label>
<input class="form-input" type="text" id="av-name" placeholder="Your full name" required />
</div>
<div class="form-group">
<label class="form-label" for="av-email">Email</label>
<input class="form-input" type="email" id="av-email" placeholder="you@example.com" required />
</div>
<div class="form-group">
<label class="form-label" for="av-notes">Notes <span class="optional">(optional)</span></label>
<textarea class="form-input form-textarea" id="av-notes" placeholder="Anything you'd like to discuss…"></textarea>
</div>
<div class="av-form-actions">
<button type="button" class="btn-ghost" id="av-back">← Back</button>
<button type="submit" class="btn-primary">Schedule Event</button>
</div>
</form>
</div>
<!-- ── SCREEN 3: Success ── -->
<div class="av-screen" id="screen-success" style="display:none">
<div class="av-success">
<div class="av-success-check" id="av-success-check">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#0f172a" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 6 9 17l-5-5"/>
</svg>
</div>
<h3 class="av-success-title">Meeting Confirmed!</h3>
<p class="av-success-subtitle">A calendar invite has been sent to your email.</p>
<div class="av-success-details" id="av-success-details"></div>
<div class="av-success-actions">
<button class="btn-primary" id="av-add-cal">Add to Calendar</button>
<button class="btn-ghost" id="av-schedule-another">Schedule Another</button>
</div>
</div>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>Availability Calendar
A Calendly-style booking widget for scheduling 30-minute calls. The left panel shows a monthly calendar with available weekdays highlighted; clicking a date reveals the right panel with half-hour time slots from 9am to 5pm. Selecting a slot expands an inline “Confirm” button. Confirming transitions to a booking form, and submitting the form animates into a success state with meeting details and add-to-calendar prompt.
Features
- Monthly calendar with clickable available days (weekdays only, some blocked)
- Half-hour time slots 9am–5pm with pre-booked slots shown as unavailable
- Slot selection expands an inline confirm button with smooth animation
- Time zone selector that recalculates and re-labels all displayed slot times
- Three-screen booking flow: slot selection, details form, success confirmation
- Success screen shows meeting details and animated checkmark
- Smooth slide transitions between booking flow screens