Time picker
A time field made of separate segments for hour, minute and, if you want them, seconds and AM or PM. It has no popup: you type or use the arrow keys.
Hour and minute, with a starting time
Each part is its own segment. Type digits, or use the arrow keys. The form sends the time as HH:MM under data-name.
<div class="time-picker" data-name="start" value="09:30" aria-label="Start time"></div>
An empty field
Without a value the segments show -- and the form sends nothing. The first change fills the other segments with 00.
<div class="time-picker" data-name="opens" aria-label="Opening time"></div>
data-seconds adds seconds
The value becomes HH:MM:SS.
<div class="time-picker" data-name="logged" data-seconds value="14:05:09" aria-label="Logged at"></div>
data-hour12 shows 1 to 12 and AM or PM
Only what is shown changes: the value is still 24 hour, so 3:15 PM is sent as 15:15. Press A or P on the last segment.
<div class="time-picker" data-name="meeting" data-hour12 value="15:15" aria-label="Meeting time"></div>
data-min, data-max and data-step
Minutes move in steps of data-step. A time outside the limits is moved to the nearest one when focus leaves the field.
<div class="time-picker" data-name="slot" data-min="09:00" data-max="17:30" data-step="15" value="09:00" aria-label="Appointment time"></div>
Date and time in one input group
Put a Date picker and a time picker in one .input-group. The date field uses data-variant="ghost" so the group draws the only box.
<div class="input-group" style="max-width: 24rem">
<span><i class="icon icon-calendar" aria-hidden="true"></i></span>
<div class="date-picker" data-min="2026-01-01">
<input type="text" class="input" data-variant="ghost" placeholder="YYYY-MM-DD" aria-label="Date" />
<div data-popover aria-hidden="true"></div>
<input type="hidden" name="date" />
</div>
<div class="time-picker" data-name="time" value="10:00" aria-label="Time"></div>
</div>
Sizes, invalid and disabled
data-size sets the height like an input. aria-invalid="true" draws the red border. disabled dims the field and turns the segments off.
<div class="stack" data-direction="row" data-gap="lg" style="flex-wrap: wrap">
<div class="time-picker" data-size="sm" data-name="a" value="08:00" aria-label="Small"></div>
<div class="time-picker" data-size="lg" data-name="b" value="08:00" aria-label="Large"></div>
<div class="time-picker" data-name="c" value="25:00" aria-invalid="true" aria-label="Invalid"></div>
<div class="time-picker" data-name="d" value="08:00" disabled aria-label="Disabled"></div>
</div>
Behavior
.time-picker is an empty element. The script fills it
with the segments and a hidden input named by
data-name, and keeps that input in the form's
format: HH:MM, or HH:MM:SS with
data-seconds. It is "" while a segment is
empty. You can also write your own
<input type="hidden"> inside; its name and
value are used. A form reset puts the starting time back.
Arrow keys wrap inside a segment and never carry into the next
one. Typed digits are limited to the segment's range, and a
segment moves on once it cannot take another digit. Pasting
HH:MM into any segment fills the whole field.
API
The script starts every .time-picker on the page,
also one added later. On the element, value is read
and set ("" or an invalid time clears it).
Methods
| Method | Description |
|---|---|
window.tsunami.timePicker.get(el) | Returns the value, HH:MM or HH:MM:SS, or "". |
window.tsunami.timePicker.set(el, value) | Sets the time and sends the events. An empty or invalid value clears it. |
window.tsunami.timePicker.mount(el) | Starts one element, for one made with a script before the page finished loading. |
window.tsunami.timePicker.destroy(el) | Removes the segments. |
Events
| Event | On | Description |
|---|---|---|
tsunami:timechange | .time-picker, bubbles | Sent when the value changes. detail.value is the new value, "" when cleared. |
input, change | the hidden input, bubble | Sent with it, so a form and the save bar hear the change. |
Keyboard
| Key | Description |
|---|---|
| Tab | Moves to the next segment, then out of the field. |
| ←, →, : | Move to the previous or next segment. |
| ↑, ↓ | Add or take one, wrapping. Minutes follow data-step. On an empty segment, start from the lowest or the highest. |
| 0–9 | Type the value. A segment moves on after two digits, or one that cannot take a second. |
| Home, End | Lowest or highest value of the segment. |
| A, P | On the AM or PM segment, set AM or PM. |
| Backspace, Delete | Empty the segment. |
Accessibility
The field is a role="group" named by its
aria-label (default Time). Each segment
is a role="spinbutton" with its own
aria-label (Hour, Minute, Second, AM or PM),
aria-valuemin, aria-valuemax,
aria-valuenow and aria-valuetext
(Empty while empty). Give the field an
aria-label or a label that says what the time is for.
Attributes
| Attribute / element | Values | Description |
|---|---|---|
.time-picker | an empty div | The field. The script adds the segments. |
data-name | a name | The name of the hidden input a form sends. |
value | HH:MM or HH:MM:SS | The starting time. Without it the field is empty. |
data-seconds | present or absent | Adds a seconds segment. |
data-hour12 | present or absent | Shows 1 to 12 and an AM or PM segment. The value stays 24 hour. |
data-min, data-max | HH:MM | The earliest and latest time. A time outside is moved to the nearest limit when focus leaves, or on an arrow key. |
data-step | minutes, such as 15 | How far the arrow keys move the minute segment. |
data-size | sm, lg | The height of the field: 28px, 36px (default) or 40px. |
aria-invalid | true | Draws the red border. |
disabled | present or absent | Dims the field and turns the segments off. |
aria-label | text | Names the group. Defaults to Time. |