Files
Htmx/docs/Components/Calendar.md
T
2026-05-05 23:55:26 +05:00

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 remove cal-day-selected from any button.
  • To restrict available dates (e.g. no past dates), post-process the rendered buttons in JS using the calendarChange event or override the generated grid with a custom initCalendar extension.
  • To restrict available dates (e.g. no past dates), post-process the rendered buttons in JS using the calendarChange event or override the generated grid with a custom initCalendar extension.

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")]