Files
Htmx/docs/Components/TimePicker.md
T
2026-05-04 19:57:48 +05:00

7.6 KiB
Raw Blame History

TimePicker

A styled time picker. The user selects hours, minutes, and optionally AM/PM. The component always writes the selected time as HH:MM (24-hour) to the hidden input, regardless of whether 12-hour display mode is used. Optionally renders a visible label and description.


HTML structure

div.flex.flex-col.gap-1.5
  label.text-sm.font-medium                    ← omitted when empty
    {label}
  div.flex.items-center.gap-1.rounded-md.border.border-input.bg-background.px-3.py-2
    select.timepicker-h[name={name}-h]         ← hour select (112 or 023)
    span.text-muted-foreground :
    select.timepicker-m[name={name}-m]         ← minute select (0059)
    select.timepicker-ampm[name={name}-ampm]   ← AM/PM (12h mode only)
    input.sr-only[type=hidden, name={name}]    ← hidden input holding HH:MM
  p.text-sm.text-muted-foreground              ← omitted when empty
    {description}

CSS mechanics

Class Effect
rounded-md border border-input bg-background Consistent styling with other form fields
sr-only on hidden input Hidden visually but included in form submission
appearance-none on <select> elements Removes native browser dropdown arrow for uniform style
focus:outline-none on selects Focus ring deferred to the wrapper div

JavaScript (syncTime in components.js)

Runs on DOMContentLoaded and htmx:afterSwap.

syncTime(wrapper)

  1. Finds .timepicker-h, .timepicker-m, .timepicker-ampm, and the hidden input
  2. On any change event across the three visible selects:
    • Reads hour, minute, and AM/PM values
    • Converts 12h → 24h if AM/PM select is present
    • Writes HH:MM to the hidden input

Constructor signature

public TimePicker(
    string  name,
    string? selected    = null,
    string  label       = "",
    string  description = "",
    bool    use12h      = false)
Parameter Description
name Form field name; hidden input gets this name, visible selects get {name}-h, {name}-m, {name}-ampm
selected Pre-selected time as "HH:MM" (24h format); defaults to current time
label Optional visible label
description Optional helper text
use12h If true, shows AM/PM select and hour range 112

Usage examples

Basic time picker (24h)

new TimePicker(name: "startTime", label: "Start time")

12-hour mode

new TimePicker(
    name:   "meetingTime",
    label:  "Meeting time",
    use12h: true)

Pre-selected time

new TimePicker(
    name:     "alarmTime",
    selected: "07:30",
    label:    "Alarm",
    use12h:   true)

Inside a form

<!-- ScheduleForm.htmx -->
<form method="post" action="/schedule">
  $$AntiforgeryToken$$
  $$StartTime$$
  $$EndTime$$
  <button type="submit">Save</button>
</form>
public ScheduleForm()
{
    StartTime = new TimePicker(name: "startTime", label: "Start", selected: "09:00");
    EndTime   = new TimePicker(name: "endTime",   label: "End",   selected: "17:00");
}

Reading the submitted values:

public record Command(
    [property: FromForm] string StartTime,  // "09:00"
    [property: FromForm] string EndTime     // "17:00"
);

var start = TimeOnly.ParseExact(command.StartTime, "HH:mm");
var end   = TimeOnly.ParseExact(command.EndTime,   "HH:mm");

Tips and tricks

  • The hidden input always stores 24h HH:MM regardless of use12h — parse with "HH:mm" format on the server.
  • selected defaults to the current server time if not specified — pass "00:00" if you want the picker to start at midnight.
  • The visible hour/minute selects are independent form fields ({name}-h, {name}-m) — only the hidden input with name is needed in your command record. Ignore the -h, -m, and -ampm fields server-side.
  • For a date+time combination, pair Calendar (for date) with TimePicker (for time) and combine their values in the handler.
  • For a date+time combination, pair Calendar (for date) with TimePicker (for time) and combine their values in the handler.

Complete page example

Templates/ScheduleMeetingPage.htmx

<div class="max-w-lg mx-auto py-10">
  <h1 class="text-2xl font-bold mb-2">Schedule a meeting</h1>
  <p class="text-sm text-muted-foreground mb-8">Pick a date and time that works for you.</p>
  <form method="post" action="/meetings/new">
    $$AntiforgeryToken$$
    <div class="grid grid-cols-2 gap-6 mb-8">
      <div>
        <label class="block text-sm font-medium mb-2">Date</label>
        $$DatePicker$$
      </div>
      <div>
        <label class="block text-sm font-medium mb-2">Time</label>
        $$TimePicker$$
      </div>
    </div>
    $$TitleInput$$
    <div class="mt-6">
      $$SubmitBtn$$
    </div>
  </form>
  $$SuccessAlert$$
</div>

Templates/ScheduleMeetingPage.htmx.cs

namespace Htmx.ApiDemo.Templates;

public sealed class ScheduleMeetingPage : ScheduleMeetingPageBase
{
    private readonly IHtmxComponent _calendar;
    private readonly IHtmxComponent _timePicker;
    private readonly IHtmxComponent _title;
    private readonly IHtmxComponent _submit;
    private readonly IHtmxComponent _success;
    private readonly byte[] _afToken;

    public ScheduleMeetingPage(
        IAntiforgery af,
        HttpContext ctx,
        DateOnly?   selectedDate = null,
        string      selectedTime = "",
        bool        booked       = false)
    {
        var tokens = af.GetAndStoreTokens(ctx);
        _afToken = $"""<input type="hidden" name="{tokens.FormFieldName}" value="{tokens.RequestToken}">""".ToUtf8Bytes();

        _calendar   = new Components.Calendar(name: "date", selectedDate: selectedDate);
        _timePicker = new Components.TimePicker(name: "time", value: selectedTime, placeholder: "09:00");
        _title      = new Components.Input(id: "title", name: "title", label: "Meeting title", placeholder: "Sync call");
        _submit     = new Components.Button("Book meeting", type: "submit");
        _success    = booked
            ? new Components.Alert(title: "Meeting booked!", description: $"Scheduled for {selectedDate:d} at {selectedTime}.")
            : HtmxEmpty.Instance;
    }

    protected override void RenderAntiforgeryToken(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_afToken);
    protected override void RenderDatePicker(HtmxRenderContext ctx)       => _calendar.Render(ctx.Next());
    protected override void RenderTimePicker(HtmxRenderContext ctx)       => _timePicker.Render(ctx.Next());
    protected override void RenderTitleInput(HtmxRenderContext ctx)       => _title.Render(ctx.Next());
    protected override void RenderSubmitBtn(HtmxRenderContext ctx)        => _submit.Render(ctx.Next());
    protected override void RenderSuccessAlert(HtmxRenderContext ctx)     => _success.Render(ctx.Next());
}

POST handler

[Handler]
[MapPost("/meetings/new")]
public static partial class PostScheduleMeetingHandler
{
    public record Command(
        [property: FromForm] DateOnly Date,
        [property: FromForm] string   Time,
        [property: FromForm] string   Title);

    private static Task<IResult> HandleAsync(
        [AsParameters] Command cmd, HttpContext ctx, IAntiforgery af, CancellationToken ct)
    {
        // Persist meeting…
        return ctx.WriteHtmxPage(
            new ScheduleMeetingPage(af, ctx, cmd.Date, cmd.Time, booked: true),
            title: "Schedule meeting");
    }
}

AppJsonSerializerContext.cs

[JsonSerializable(typeof(PostScheduleMeetingHandler.Command), TypeInfoPropertyName = "ScheduleMeetingCommand")]