Eunomia v1.0.0-beta.2
AllAngularReactNext.jsJavaScriptVue
EunomiaSalmonForestVioletOceanGoldFireCustom…
🇬🇧 English🇫🇷 Français

Time picker

Time picker is a time field entered across segmented numeric hour and minute inputs, plus a toggle AM/PM segment at a 12-hour format, matching the date picker's own segmented-field pattern, with a trailing clock icon that opens a panel to pick the value instead. Two panel styles are available: the default is a panel of hour, minute, and period columns, which falls back to a mobile-friendly modal below a small viewport, showing either the same arrow-button columns as desktop or a scrollable, swipeable wheel per column. The alternative is a single scrollable list of every time of day at a set interval, where picking an entry both commits it and closes the panel immediately, exactly like a select field. That list style renders identically on every viewport, and never falls back to the mobile modal.

Dependencies

eun-icon · for the clock trigger eun-modal · for the mobile fallback, below a small viewport eun-label · if it has a label
Overview API Examples Accessibility

When to use

Use time picker whenever someone needs to pick a time of day, such as a meeting slot, a delivery window, an alarm, or an opening hour. Typing goes through independent hour and minute (and period) segments rather than a single free-text field, exactly like date picker's day, month, and year segments. A trailing clock icon opens a panel of the same columns, laid out as arrow-button spinners on desktop and in the mobile fallback, or a scrollable wheel per column by default on mobile.

The picked time is stored as a full point in time, not just an hour and a minute on their own, exactly like date picker. That's what makes its timezone setting meaningful: it names the zone the hour and minute are read and written in, defaulting to the viewer's own when unset. See "Timezones" further down for the full contract.

Also see date picker's own combined date-and-time mode, which embeds this same hour, minute, and period panel under its own calendar grid for a single field covering both.

Install & usage

npm install @eunomia/elements
import "@eunomia/elements/time-picker.js";
<eun-time-picker label="Meeting time" name="meeting-time"></eun-time-picker>

<script>
  document
    .querySelector("eun-time-picker")
    .addEventListener("eunchange", (event) => {
      console.log(event.target.value); // a Date, or null
    });
</script>

Importing the file registers <eun-time-picker> as a custom element, along with eun-icon/eun-modal/eun-label it depends on internally (see the dependencies listed above). Import those too if you're registering components individually rather than through a barrel that already does it. value is entirely yours to read from eunchange, and the component never submits anywhere on its own beyond native form participation (name + value, exactly like eun-input).

Guidance

  • Let locale drive the hour format (12h/24h) and segment order rather than hardcoding one: en-US and fr-FR genuinely disagree on both, and some locales (e.g. zh-CN) even put the AM/PM segment before the hour
  • Set timezone explicitly whenever the picked time means something in a zone other than the viewer's own (e.g. "the meeting is 14:00 New York time, whoever's looking at this"), and never re-derive it from the viewer's clock
  • Reach for step-minute (e.g. 15) instead of validating the minute yourself after the fact whenever only certain minute values make sense (a booking slot, a delivery window)
  • Reading value as anything but a Date or null: parse/format it yourself at the boundary (an API call, a display string) rather than expecting a pre-formatted string back
  • Assuming an unset timezone means UTC: it means the viewer's own local zone, matching a plain Date's usual behavior, so set timezone explicitly whenever you actually mean UTC or another specific zone
  • Mixing timezone-qualified and unqualified eun-time-pickers on the same page for what's conceptually the same value: pick one zone (or the viewer's own) and stay consistent, or the same instant will visibly read differently side by side

Live testing

Properties

TimePicker <eun-time-picker>

Attributes

NameTypeDefaultDescription
hour-format12 | 24Overrides the hour format. Auto-detected from locale when unset
step-minutenumber1The minute column's increment step, and which minute values the mobile wheel enumerates
min-timestringThe earliest selectable time of day, as an HH:mm string. A value outside minTime or maxTime reports as invalid rather than being blocked
max-timestringThe latest selectable time of day, as an HH:mm string. When earlier than minTime, the valid range wraps past midnight instead of being empty
mobile-mode'wheel' | 'modal''wheel'Which content the mobile modal fallback shows: the same arrow-button columns as desktop, or a scroll-snap wheel per column. Only relevant when panelMode is columns
panel-mode'columns' | 'list''columns'Which panel the trailing clock trigger opens: the hour, minute, and period spin button columns, or a single scrollable listbox of every time of day at a listStepMinutes gap. Picking an entry from the list both commits it and closes the panel immediately. The list renders identically on every viewport, and never switches to the mobile modal fallback that columns uses on a small viewport
list-step-minutesnumber30When panelMode is list, the gap in minutes between consecutive listbox entries, such as 15 for quarter-hour granularity or 60 for hourly
apply-labelstring'Apply'The label of the mobile modal's confirm button
cancel-labelstring'Cancel'The label of the mobile modal's cancel button
aria-label-clockstring'Open clock'The accessible label of the trailing clock button
hour-placeholderstring'hh'The placeholder shown in the empty hour segment
minute-placeholderstring'mm'The placeholder shown in the empty minute segment
hide-timezone-labelbooleanfalseHides the small offset label normally shown next to the field whenever timezone differs from the viewer's own
no-animationbooleanfalseDisables the popover's open and close transition and the mobile modal's slide-in, both set explicitly and whenever the browser prefers reduced motion
timezonestringthe runtime's ownThe time zone the hour and minute are displayed and edited in. Defaults to the runtime's own
default-valueDate | nullThe default value applied when the field connects, and restored on form reset

Import the exact TypeScript type behind any property above, see Types.

Properties

JS-only — no matching HTML attribute, set these from a script or a template binding.

NameTypeDefaultDescription
checkLocalValidatorsReads local validation state : a partially/invalidly typed segment group reports as invalid, matching native constraint validation's `badInput` — mirrors `eun-date-picker`'s identical contract. Once a value is committed, also checks it against minTime/ maxTime, reporting `rangeUnderflow`/`rangeOverflow` exactly like a native `<input type="time" min max>` would.
variant'outline' | 'fill' | 'underline''outline'The visual variant to apply to the field
valueDate | nullThe picked instant
localestringThe locale used for the hour format auto-detection, segment order, and the modal's time preview. Defaults to the runtime's own
labelstringThe title label of the field, rendered through the label component
instructionsstringInstructions displayed below the label
namestringThe field name
disabledbooleanfalseWhether the field is disabled
readonlybooleanfalseWhether the field is read only
requiredbooleanfalseWhether the field is required
hideErrorbooleanfalseWhether the errors are hidden
validatorsArray<Validators<Date | null>>The list of custom validation rules applied to the value

Events

NameTypeDescription
eunchangeChangeEventFired whenever the committed value changes: immediately on completing the typed segments, immediately on every column adjustment in the desktop popover, or on the mobile modal's Apply

Every event above follows the same naming convention, covered in Events.

CSS custom properties

NameDescription
--field-border-colorSets the border color of the field. The same variable the input, select, and date picker use
--field-border-color-hoverSets the border color on hover
--field-border-color-focusSets the border color on focus
--field-backgroundSets the background color of the field
--field-text-colorSets the text color of the field
--field-placeholder-colorSets the color of the placeholder for empty segments
--field-icon-colorSets the color of the trailing clock icon
--field-error-colorSets the color of the error message
--field-hint-colorSets the color of the hint message and timezone label
--field-focus-outline-colorSets the focus outline color used throughout the field and panel
--time-picker-hover-backgroundSets the background color of hovered column arrow buttons and list items
--time-picker-selected-backgroundSets the background color of the selected item in the list panel mode
--time-picker-selected-colorSets the text color of the selected item in the list panel mode
--time-picker-panel-backgroundSets the background color of the popover panel
--time-picker-panel-transition-durationSets the duration of the popover panel's open and close transition

Locale driven

Leaving hour-format unset auto-detects 12h vs. 24h from locale (or the viewer's own runtime locale). An en-US field renders hour/minute/period, an fr-FR field renders just hour/minute.

<eun-time-picker
  locale="en-US"
  label="12-hour (en-US)"
  name="format-12"
></eun-time-picker>
<eun-time-picker
  locale="fr-FR"
  label="24-hour (fr-FR)"
  name="format-24"
></eun-time-picker>

Set hour-format="12"/"24" explicitly to override the locale-driven default.

Minute step

step-minute controls the minute column's increment (both the arrow buttons and the mobile wheel). 15 only ever lands on :00/:15/:30/ :45.

<eun-time-picker
  step-minute="15"
  label="Booking slot"
  name="booking-slot"
></eun-time-picker>

Min and max time

min-time/max-time bound the selectable time of day, as "HH:mm" strings, matching a native <input type="time" min max>. A value outside the bounds isn't blocked from being typed or picked. It reports as invalid (rangeUnderflow/rangeOverflow) exactly like that native input does, with required's own error styling.

<eun-time-picker
  min-time="09:00"
  max-time="18:00"
  label="Office hours"
  name="office-hours"
></eun-time-picker>

When min-time is set after max-time (e.g. min-time="22:00" max-time="06:00"), the valid range wraps past midnight instead of being empty: anything at or after min-time, or at or before max-time, is valid.

<eun-time-picker
  min-time="22:00"
  max-time="06:00"
  label="Overnight shift"
  name="overnight-shift"
></eun-time-picker>

Mobile layout: wheel or modal

Below a 768px viewport, the panel opens inside a eun-modal. The default mobile-mode="wheel" renders each column as a scrollable, scroll-snap wheel (flick or drag to spin it, tap an item, or use the arrow buttons/keyboard, all three stay equivalent). mobile-mode="modal" instead reuses the exact same arrow-button columns as desktop.

The rest of this page's demos run at your actual browser width, so they'll only show this if you resize your browser below 768px. The previews below are instead pinned to real 375px-wide iframes, so they always render the mobile layout live, right here. Tap the clock icon to open either.

<eun-time-picker
  label="Wheel (default)"
  name="mobile-wheel-demo"
></eun-time-picker>
<eun-time-picker
  mobile-mode="modal"
  label="Modal (arrow buttons)"
  name="mobile-modal-demo"
></eun-time-picker>

List panel

panel-mode="list" swaps the hour/minute/[period] columns for a single scrollable listbox of every time of day, spaced list-step-minutes apart (default 30), anywhere from minute granularity (1) up through hourly or coarser (60, 120, …). Picking an entry both commits it and closes the panel immediately, exactly like eun-select's own options list, so there's no Apply/Cancel step. Unlike "columns", "list" renders the same listbox on every viewport. It never falls back to the mobile eun-modal.

<eun-time-picker
  panel-mode="list"
  list-step-minutes="30"
  locale="en-US"
  label="Booking slot (12h)"
  name="list-panel-12h"
></eun-time-picker>
<eun-time-picker
  panel-mode="list"
  list-step-minutes="30"
  locale="fr-FR"
  label="Booking slot (24h)"
  name="list-panel-24h"
></eun-time-picker>

At a 12-hour format, every entry's AM/PM is resolved by Intl.DateTimeFormat off the entry's own hour/minute. Midnight always reads "12:00 AM" and noon "12:00 PM", not "0:00 AM"/"0:00 PM".

Timezones

value is always an absolute instant (Date). What varies with timezone is which wall-clock hour/minute that instant is displayed and edited as. Left unset, timezone defaults to the viewer's own runtime zone, so an untouched eun-time-picker behaves exactly like a plain Date's getHours()/getMinutes() always have.

Example 1: no timezone (local behavior). Nothing special: the field reads/writes whatever the viewer's own device clock says.

<eun-time-picker label="Reminder time" name="reminder"></eun-time-picker>

Example 2: an explicit zone. Set timezone and assign value a known instant. The field displays that instant's wall-clock hour/minute in that zone, regardless of where the viewer actually is, plus a small offset label (e.g. "GMT-5") next to the field so it's never ambiguous which zone is in play.

<eun-time-picker
  id="tz-demo"
  timezone="America/New_York"
  label="Call time (New York)"
  name="call-time"
></eun-time-picker>

<script type="module">
  customElements.whenDefined("eun-time-picker").then(() => {
    document.querySelector("#tz-demo").value = new Date(
      "2026-01-15T19:00:00.000Z",
    );
    // Displays 14:00 (America/New_York is UTC-5 in January) — not 19:00.
  });
</script>

Example 3: the same value, two different timezones. A given instant reads as a different wall-clock time depending on which zone you ask, including its offset, which itself shifts across the year under daylight saving:

<eun-time-picker
  id="tz-paris"
  timezone="Europe/Paris"
  label="Paris"
  name="tz-paris"
></eun-time-picker>
<eun-time-picker
  id="tz-tokyo"
  timezone="Asia/Tokyo"
  label="Tokyo"
  name="tz-tokyo"
></eun-time-picker>

<script type="module">
  customElements.whenDefined("eun-time-picker").then(() => {
    const instant = new Date(); // the same instant, shared by both fields
    document.querySelector("#tz-paris").value = instant;
    document.querySelector("#tz-tokyo").value = instant;
  });
</script>

How it works. There's no timezone-aware date/time type built into the platform, and this library has no date/time library dependency, so every conversion is done by hand in timezone.utils.ts via Intl.DateTimeFormat, using the same technique timezone libraries built on Intl use internally: format a known instant in the target zone to read its current UTC offset, then resolve a wall-clock hour/minute back to an instant by correcting for that offset (re-checked once, since the correction can itself cross a daylight-saving transition). This is fully DST-safe: picking a time on the day clocks change still resolves to the correct instant. The one known, documented limitation is at the transition itself: a wall-clock time that never occurs (the "spring forward" gap) or occurs twice (the "fall back" overlap) resolves to a deterministic, stable result without a guarantee of which of the two real occurrences that is, the same pragmatic tradeoff new Date(y, m, d, h, min) already makes for the runtime's own local zone.

Disabled and readonly

disabled blocks every interaction and greys the field out. readonly keeps the value focusable/submittable but blocks editing and the clock trigger.

<eun-time-picker
  disabled
  label="Disabled"
  name="disabled-demo"
  default-value="2026-01-15T09:00:00"
></eun-time-picker>
<eun-time-picker
  readonly
  label="Readonly"
  name="readonly-demo"
  default-value="2026-01-15T09:00:00"
></eun-time-picker>

Hint, required, and error

hint shows helper text below the field. required (combined with a native form.reset() or reportValidity()) shows an error in its place instead, as soon as the field is empty or a typed time is invalid (e.g. a partially typed hour, or a 12-hour field missing its AM/PM segment).

<eun-time-picker
  label="Alarm"
  name="hint-demo"
  hint="24-hour format"
></eun-time-picker>
<eun-time-picker
  label="Meeting time"
  name="error-demo"
  required
></eun-time-picker>

Set hide-error to suppress the error/hint text while keeping the invalid styling (the border, the outline, aria-invalid), useful when a form shows errors in a summary elsewhere instead of inline. A native form.reset() restores value to default-value and re-validates from there, exactly like every other Eunomia field.

Variants

variant matches every other text-like field (eun-input/eun-select/ eun-date-picker): outline (default), fill, underline.

<eun-time-picker
  variant="outline"
  label="Outline"
  name="outline-demo"
></eun-time-picker>
<eun-time-picker variant="fill" label="Fill" name="fill-demo"></eun-time-picker>
<eun-time-picker
  variant="underline"
  label="Underline"
  name="underline-demo"
></eun-time-picker>

Part of the shared vocabulary covered in Variants, alongside every other component that reuses it.

Custom

Override the --time-picker-*/--field-* CSS variables, listed in full in the API tab.

<eun-time-picker
  class="custom-time-picker"
  label="Custom theme"
  name="custom-demo"
></eun-time-picker>
.custom-time-picker {
  --field-border-color-focus: #be185d;
  --field-focus-outline-color: #be185d;
  --time-picker-hover-background: #fce7f3;
}

Keyboard interactions: segmented field

Key Action
Tab / Shift+Tab Moves focus between segments and the clock trigger
0-9 (hour/minute segments) Types a digit, auto-advances to the next segment once full
A / P (period segment) Sets AM/PM directly
ArrowLeft/ArrowRight Moves to the previous/next segment
ArrowUp/ArrowDown Increments/decrements the focused segment (hour/minute), or toggles AM/PM
Backspace (on an empty segment) Moves to the previous segment
Enter / Space (on the trigger) Opens the panel

Keyboard interactions: hour, minute, and period panel

There's no dedicated WAI-ARIA APG composite pattern for a time picker. Each column is its own role="spinbutton" (the APG building-block pattern), grouped under a labeled role="group". Both the arrow-button and wheel visuals share the exact same keyboard contract below.

Key Action
Tab / Shift+Tab Moves focus between columns
ArrowUp/ArrowDown Increments/decrements the focused column by 1 step
PageUp/PageDown Increments/decrements the focused column by 5 steps
Home / End Jumps to the column's minimum/maximum value
Escape Closes the popover

Keyboard interactions: list panel

A standard role="listbox"/role="option" pattern. Each option itself is focusable via a roving tabindex, rather than eun-select's combobox-plus-aria-activedescendant one, since this panel's trigger is a plain icon button, not a text input keeping focus for itself.

Key Action
ArrowUp/ArrowDown Moves focus to the previous/next entry
PageUp/PageDown Moves focus 5 entries up/down
Home / End Moves focus to the first/last entry
Enter / Space Picks the focused entry, committing and closing
Escape Closes the popover

Aria attributes and rules

  • Each hour/minute segment is a native <input inputmode="numeric"> with its own aria-label ("Hour"/"Minute"), and the period segment is a <button> labeled "AM/PM", grouped under a single role="group" aria-label="…", matching eun-date-picker's day/month/year segment group.
  • The clock trigger is aria-haspopup="dialog" at panel-mode="columns", or aria-haspopup="listbox" at panel-mode="list", with aria-expanded tracking the popover/modal's open state, and returns focus to itself on close.
  • At panel-mode="columns", each hour/minute/[period] column carries aria-valuemin/aria-valuemax/aria-valuenow/aria-valuetext, updated live as it's adjusted, and a screen reader announces the new value on every change, whether triggered by the arrow buttons, the keyboard, or (in mobile-mode="wheel") a pointer scroll/tap. The wheel's individual scrollable items are aria-hidden and excluded from the tab order, since they're a pointer-only affordance layered behind the same accessible spinbutton, not a separate interactive listbox.
  • At panel-mode="list", the panel is a role="listbox" of role="option" entries, each carrying aria-selected. Opening the panel moves focus directly onto the currently selected entry (or the first, if none matches exactly), scrolled into view.
  • Below the 768px breakpoint, panel-mode="columns" renders the panel inside eun-modal (role="dialog", labelled by the field's own label via aria-label) instead of the popover, with an explicit Apply/Cancel footer, matching eun-date-picker's identical mobile fallback rationale. Like every eun-modal, focus isn't currently trapped inside it, so treat that as a known gap rather than an intentional design choice. panel-mode="list" never uses this fallback, and renders the same popover listbox on every viewport.
  • prefers-reduced-motion: reduce (or the explicit no-animation attribute) disables the popover's open/close transition and the mobile wheel's smooth-scroll snapping alike. The wheel still snaps to the nearest item, just instantly rather than with an animated glide.

Reference links

WAI-ARIA Authoring Practices: Spinbutton
MDN: Intl.DateTimeFormat