Time Picker
stimeo--time-picker
Segmented hour / minute / AM-PM spinbuttons composing a 24-hour value.
The stimeo--time-picker controller treats each segment (hours, minutes, optional seconds, optional AM/PM) as an APG Spinbutton and composes them into an HH:MM[:SS] value in a hidden field — always 24-hour, even when edited in 12-hour mode. Arrow keys step the focused segment and wrap at the bounds; when wrapping is enabled the overflow carries from minutes into hours (and seconds into minutes). Left/Right move between segments, Home/End jump to the bounds, and typing digits enters a value directly, advancing after two digits. Every segment is its own Tab stop. Behavior only.
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. |
| 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__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
required
|
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 |
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. |