Skip to content
Glint UI

TimePicker

An hour-and-minute grid in a popup, opened by a trigger you own and walked entirely from the arrow keys.

Usage

import { TimePicker } from "@glint/components/time-picker.slint";
import { Button } from "@glint/components/button.slint";
export component AppWindow inherits Window {
in-out property <int> hour;
in-out property <int> minute;
in property <string> label;
picker := TimePicker {
hour <=> root.hour;
minute <=> root.minute;
changed(hour, minute) => { /* store it */ }
Button {
text: root.label;
haspopup: true;
clicked => { picker.show(); }
}
}
}

Slint has no Time type, so the time is yours. Bind hour (0 … 23) and minute (0 … 59) to your model and the picker renders the grid, moves the selection and reports the whole time through changed(hour, minute). It formats nothing beyond the two-digit cells in the grid itself.

The trigger is yours too — the same contract every Glint overlay keeps. A pointer click anywhere on the trigger opens the popup for free; wire your trigger’s keyboard activation to show(), and bind to is-open if the trigger should reflect that it is up. show() and close() mirror PopupWindow’s API (ADR-0006).

The selection is live. Every arrow key already writes hour / minute and fires changed — there is no draft the popup is holding back, the way RadioGroup and Tabs move their value on the arrow keys. Enter is therefore accept-and-close rather than commit, and Esc closes on the value already stored. Both return the keyboard to your trigger.

changed means changed: landing back on the value already selected — by re-clicking it, or by wrapping onto it — fires nothing, so a consumer that persists on the callback does not write for nothing.

Examples

A 12-hour clock

twelve-hour labels the hour cells “12 AM … 11 PM”; hour stays a 0 … 23 value either way, so nothing downstream has to know which clock the user was shown. The wider labels need a wider popup, which is what content-width defaults account for — override it if your am-label / pm-label are longer than the English defaults.

Granularity

minute-step is the grid’s granularity in both directions: it decides how many minute cells exist and how far one arrow press travels. 5 — the default — gives :00 :05 … :55; 15 gives four cells, which is the shape a booking form wants.

A minute off the step grid is never rewritten behind your back: it rounds to the nearest cell for navigation only, so a time restored from storage stays exactly what it was until the user moves it.

Naming the two halves

hour-heading and minute-heading are the section headings inside the popup, and am-label / pm-label the suffixes a 12-hour clock spells. All four are properties with English defaults, so translating the picker is setting them — the component does no other formatting.

API Reference

Properties

PropertyTypeDefaultDescription
hourin-out int0Two-way; the selected hour on a 24-hour clock (0 … 23).
minutein-out int0Two-way; the selected minute (0 … 59).
minute-stepin int5Granularity of the popup's minute grid; 5 gives :00 :05 … :55.
twelve-hourin boolfalseShow hours as 12-hour clock labels; hour stays a 0 … 23 value.
hour-headingin string@tr("Hour")Section headings and 12-hour suffixes inside the popup; override them to localize.
minute-headingin string@tr("Minute")
am-labelin string@tr("AM")
pm-labelin string@tr("PM")
content-widthin lengthroot.twelve-hour ? 316px : 250pxPixel width of the popup panel. The default fits six columns of the grid's cells; the 12-hour labels need the wider ones. Override it if your am-label / pm-label are longer than the English defaults.
is-openout boolno defaultTrue while the popup is on screen. The trigger is @children, so reflecting the open state in it is yours to do — bind to this.

Callbacks

CallbackDescription
changed(int, int)Fired with (hour, minute) whenever either changes.

Functions

FunctionDescription
show()The overlay API mirrors PopupWindow's, per ADR-0006. Wire your trigger's keyboard activation to show().
close()Hide the popup. Focus returns to whatever held it before.

Accessibility

  • The trigger’s accessibility is yours, because the trigger is. Name it, and mark it as opening a popup — haspopup: true on a Button is that.
  • The grid takes the keyboard on its own way in. Nothing outside a PopupWindow can focus what is inside it, so the grid grabs the keyboard as the popup is built, and the arrows work without a Tab first.
  • Keyboard. / step one cell along the half you are in, wrapping; / step a row through the hour rows and the minute rows below them, so off the last hour row lands in the minutes. Enter accepts and closes, Esc closes.
  • Pointer and keyboard share one cursor. Clicking a cell hands the grid the keyboard focus and the arrows continue from there — no intervening Tab, and no second cursor drifting behind the first.
  • Both selections stay readable. The half the arrows are not in keeps its value drawn in a softer surface rather than losing it, so the whole time is visible while either half is being changed.
  • Closing returns the focus to whatever held it before — your trigger — so the next Enter opens the picker again.
  • Each half is a list, and a time is one of its items. The hours are a list named by hour-heading and the minutes another named by minute-heading, so a cell announces itself as “Hour 09”, says it is the 10th of 24, and says whether it is the selection. Naming the cell after its section is what tells the two “00”s apart.
  • Taking a cell is its default action, which is what lets a screen reader pick a time without a pointer — the same path Calendar’s day cells offer, and it keeps working inside the popup.
  • A blank is nothing at all. The cells filling the tail of the last minute row when the step does not divide the row evenly are holes that keep the columns aligned: they take no click and are in no accessibility tree.