Co-authored-by: Copilot <copilot@github.com>
7.0 KiB
Calendar
A date picker that lets the user click to select a single date. The selected date is stored in a hidden form input and submitted with the form. Think of it as a fancy <input type="date"> that looks the same on every browser.
Quick example
new Calendar(id: "dob", name: "dateOfBirth")
That renders a calendar starting at today's date. When the user clicks a day, the hidden input is updated and the date is included in the form submission.
All the options
public Calendar(
string id,
string name = "date",
DateOnly? selected = null)
| Parameter | What it does |
|---|---|
id |
A unique identifier for this calendar. The root element gets id="cal-{id}". |
name |
The form field name for the hidden input. Use this name in your Command record on the server. |
selected |
The date to pre-select on render. Defaults to today. |
Real-world examples
Appointment booking form
<!-- Templates/BookingForm.htmx -->
<form method="post" action="/book" class="space-y-6">
$$Token$$
<div>
<label class="text-sm font-medium">Pick a date</label>
$$DatePicker$$
</div>
<button type="submit">Book appointment</button>
</form>
// Templates/BookingForm.htmx.cs
public sealed class BookingForm : BookingFormBase
{
private readonly IHtmxComponent _datePicker;
private readonly byte[] _tokenData;
public BookingForm(string antiforgeryToken)
{
_tokenData = $"""<input type="hidden" name="__RequestVerificationToken" value="{System.Web.HttpUtility.HtmlAttributeEncode(antiforgeryToken)}" />""".ToUtf8Bytes();
_datePicker = new Calendar(id: "booking", name: "bookingDate");
}
protected override void RenderToken(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_tokenData);
protected override void RenderDatePicker(HtmxRenderContext ctx) => _datePicker.Render(ctx.Next());
}
Reading the submitted date on the server:
public record Command(
[property: FromForm] string BookingDate // arrives as "yyyy-MM-dd"
);
var date = DateOnly.ParseExact(command.BookingDate, "yyyy-MM-dd");
Pre-selected date (e.g. editing an existing booking)
new Calendar(
id: "appointment",
name: "appointmentDate",
selected: existingBooking.Date)
Reacting to date selection in JavaScript
When the user picks a date, the calendar fires a calendarChange custom event:
document.getElementById('cal-booking').addEventListener('calendarChange', e => {
console.log(e.detail.date); // "2026-09-15"
// update price estimates, availability, etc.
});
How it works
The calendar is rendered as static HTML by the server, with the current month's grid pre-built as <button> elements. JavaScript in components.js (initCalendar) takes over after the page loads:
- Clicking a day updates the hidden input and highlights the selected date.
- Clicking the month/year label in the navigation row drills down: days → months → years. This lets the user jump to a different year quickly without clicking through months one at a time.
- Prev/Next arrows move through the current view (month by month, year by year, or decade by decade).
All state is stored in data-* attributes on the root element — not in JavaScript closures. This means the calendar is fully re-initialised correctly when HTMX swaps it in.
- If you need to clear the selection client-side, set
document.querySelector('#cal-myid .cal-hidden-input').value = ''and removecal-day-selectedfrom any button. - To restrict available dates (e.g. no past dates), post-process the rendered buttons in JS using the
calendarChangeevent or override the generated grid with a custominitCalendarextension. - To restrict available dates (e.g. no past dates), post-process the rendered buttons in JS using the
calendarChangeevent or override the generated grid with a custominitCalendarextension.
Complete page example
Templates/AppointmentPage.htmx
<div class="max-w-sm mx-auto py-10">
<h1 class="text-2xl font-bold mb-6">Book an appointment</h1>
<form method="post" action="/appointments">
$$AntiforgeryToken$$
<div class="mb-4">
<label class="text-sm font-medium block mb-2">Select a date</label>
$$DatePicker$$
</div>
$$SubmitBtn$$
</form>
$$Confirmation$$
</div>
Templates/AppointmentPage.htmx.cs
namespace Htmx.ApiDemo.Templates;
public sealed class AppointmentPage : AppointmentPageBase
{
private readonly IHtmxComponent _datePicker;
private readonly IHtmxComponent _submitBtn;
private readonly IHtmxComponent _confirmation;
private readonly byte[] _afToken;
public AppointmentPage(IAntiforgery af, HttpContext ctx, string? confirmedDate = null)
{
var tokens = af.GetAndStoreTokens(ctx);
_afToken = $"""<input type="hidden" name="{tokens.FormFieldName}" value="{tokens.RequestToken}">""".ToUtf8Bytes();
_datePicker = new Components.Calendar(id: "appt", name: "appointmentDate");
_submitBtn = new Components.Button("Book", type: "submit");
_confirmation = confirmedDate is not null
? new Components.Alert(title: "Booked!", description: $"Your appointment is on {confirmedDate}.")
: HtmxEmpty.Instance;
}
protected override void RenderAntiforgeryToken(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_afToken);
protected override void RenderDatePicker(HtmxRenderContext ctx) => _datePicker.Render(ctx.Next());
protected override void RenderSubmitBtn(HtmxRenderContext ctx) => _submitBtn.Render(ctx.Next());
protected override void RenderConfirmation(HtmxRenderContext ctx) => _confirmation.Render(ctx.Next());
}
GET + POST handlers
[Handler]
[MapGet("/appointments/new")]
public static partial class GetAppointmentHandler
{
public record Query();
private static Task<IResult> HandleAsync(
Query _, HttpContext ctx, IAntiforgery af, CancellationToken ct)
=> ctx.WriteHtmxPage(new AppointmentPage(af, ctx), title: "Book appointment");
}
[Handler]
[MapPost("/appointments")]
public static partial class PostAppointmentHandler
{
public record Command([property: FromForm] string AppointmentDate);
private static Task<IResult> HandleAsync(
[AsParameters] Command cmd, HttpContext ctx, IAntiforgery af, CancellationToken ct)
{
var date = DateOnly.ParseExact(cmd.AppointmentDate, "yyyy-MM-dd");
var page = new AppointmentPage(af, ctx, confirmedDate: date.ToLongDateString());
return ctx.WriteHtmxPage(page, title: "Book appointment");
}
}
AppJsonSerializerContext.cs — add the new command
[JsonSerializable(typeof(PostAppointmentHandler.Command), TypeInfoPropertyName = "AppointmentCommand")]