Time Picker
stimeo--time-picker
A time entered as separate hour, minute and AM/PM parts, always submitted in 24-hour form.
Enters a time as separate parts: hours, minutes, and optionally seconds and AM or PM. The arrow keys step the part you are on and wrap at its ends, and with carrying turned on a full turn of the minutes moves the hour along. The left and right keys move between parts, Home and End jump to the ends, and typing digits enters them directly, moving on after two. Even when you edit in 12-hour form, the value your form submits is always 24-hour. Each part's current value is announced. It brings no styling.
Keyboard
| Key | Action |
|---|---|
| ↑ / ↓ | Step the focused segment up / down (this demo sets step=5, so minutes move in 5-minute increments; wrapping at the bounds). |
| ← / → | Move to the previous / next segment. Under RTL the two swap. |
| Home / End | Jump the focused segment to its minimum / maximum. |
| 0–9 | Type a value directly; advances after two digits. |
<%# Markup for the time-picker (time selection) demo.
stimeo--time-picker treats each segment (hour / minute / AM·PM) as an APG Spinbutton,
handling arrow-key increment/decrement, edge wrap and carry (minute→hour), left/right
segment movement, and direct numeric entry, and reflects the composed 24-hour value
(HH:MM) into a hidden input. Each segment is an independent tab stop. %>
<div
class="time-picker"
data-controller="stimeo--time-picker"
data-stimeo--time-picker-hour-cycle-value="12"
data-stimeo--time-picker-step-value="5"
role="group"
aria-label="<%= t("components.time_picker.demo.group_label") %>">
<span class="time-picker__segment" role="spinbutton"
aria-label="<%= t("components.time_picker.demo.hours") %>"
tabindex="0" aria-valuenow="9" aria-valuemin="1" aria-valuemax="12" aria-valuetext="09"
data-segment="hour" data-stimeo--time-picker-target="segment"
data-action="keydown->stimeo--time-picker#onKeydown">09</span>
<span class="time-picker__colon" aria-hidden="true">:</span>
<span class="time-picker__segment" role="spinbutton"
aria-label="<%= t("components.time_picker.demo.minutes") %>"
tabindex="0" aria-valuenow="30" aria-valuemin="0" aria-valuemax="59" aria-valuetext="30"
data-segment="minute" data-stimeo--time-picker-target="segment"
data-action="keydown->stimeo--time-picker#onKeydown">30</span>
<span class="time-picker__segment time-picker__segment--meridiem" role="spinbutton"
aria-label="<%= t("components.time_picker.demo.meridiem") %>"
tabindex="0" aria-valuenow="0" aria-valuemin="0" aria-valuemax="1" aria-valuetext="AM"
data-segment="meridiem" data-stimeo--time-picker-target="segment"
data-action="keydown->stimeo--time-picker#onKeydown">AM</span>
<input type="hidden" name="time" data-stimeo--time-picker-target="field" />
</div>
/*
* Presentation-only styles for the time-picker demo.
* Each segment is an independent spinbutton (tab stop). Formatting and composition are
* the library's.
*/
.time-picker {
display: inline-flex;
align-items: center;
gap: 0.125rem;
padding: 0.5rem 0.75rem;
border: 1px solid var(--border);
border-radius: 0.5rem;
background: var(--surface, var(--surface-card));
font-variant-numeric: tabular-nums;
font-size: 1.25rem;
}
.time-picker__segment {
min-width: 1.75rem;
padding: 0.125rem 0.25rem;
text-align: center;
border-radius: 0.25rem;
cursor: ns-resize;
user-select: none;
}
.time-picker__segment--meridiem {
margin-left: 0.375rem;
cursor: pointer;
}
/* Invert the focused segment on :focus (not just :focus-visible) so a mouse click —
not only keyboard focus — clearly shows which segment the arrow keys / typing act on. */
.time-picker__segment:focus {
outline: none;
background: var(--accent);
color: var(--white);
}
.time-picker__segment:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.time-picker__colon {
color: var(--text-muted, var(--color-text-muted));
}
// Demo that subscribes to the time-picker change event to inspect the composed 24-hour
// value (HH:MM). Shows that even when operated in 12-hour mode (AM/PM), the hidden input
// holds the 24-hour value.
document.querySelectorAll('[data-controller~="stimeo--time-picker"]').forEach((root) => {
root.addEventListener('stimeo--time-picker:change', (e) => {
// In real use, e.detail.value ("HH:MM") feeds form submission, etc.
console.log('time value (24h):', e.detail.value);
});
});
These demo styles use shared design tokens (light + dark). Copy the shared styles too, then toggle data-theme on your root element for dark mode.
The data-* attributes you add to your own HTML to wire this component. Put the data-controller below on a root element, then place its targets / values / actions inside that element.
On the root element
data-controller="stimeo--time-picker"
Targets
| Name | Description | Attribute |
|---|---|---|
segment
required
|
A time segment (hour/minute/second/meridiem) as an APG spinbutton; its own Tab stop. | data-stimeo--time-picker-target="segment" |
field
|
Hidden input holding the composed 24-hour HH:MM[:SS] value. |
data-stimeo--time-picker-target="field" |
Values
| Name | Description | Attribute |
|---|---|---|
hourCycle
|
12 or 24-hour cycle (default 24); 12 enables the meridiem segment. | data-stimeo--time-picker-hour-cycle-value |
step
|
Minute step increment (default 1); other segments step by 1. | data-stimeo--time-picker-step-value |
seconds
|
When true, includes a seconds segment in the value (default false). | data-stimeo--time-picker-seconds-value |
wrap
|
When true (default), stepping past a bound wraps and carries into the larger unit. | data-stimeo--time-picker-wrap-value |
Actions
| Name | Description | Action |
|---|---|---|
onKeydown
|
Handles Up/Down stepping, Left/Right segment moves, Home/End jumps, and digit direct-entry. | stimeo--time-picker#onKeydown |
Events
| Name | Description | Event |
|---|---|---|
change
|
Dispatched when the composed value actually changes, with the value in detail. | stimeo--time-picker:change |
reconcile
|
stimeo--time-picker:reconcile |
State hooks
The library only manages these ARIA/data attributes and custom properties. Your CSS reads them to render the look — selectors like [aria-selected], [aria-expanded], or var(--stimeo--…) hook into this state.
| Hook | Target | Meaning |
|---|---|---|
aria-valuenow |
Segment | The segment's current numeric value. |
aria-valuetext |
Segment | The zero-padded display text (e.g. 09), or AM / PM. |
value |
Hidden field | The composed 24-hour HH:MM[:SS] value. |