Skip to content
ShineUI
Install

TimePicker

Time of day, typed or picked.

<sui-time-picker>Advanced inputsStable

About

Time-of-day field: a stepped list of options plus a box for typing, in the page's locale and on its clock. Pick a row, or type 2:47 PM or 14:47 and press Enter for a time off the step. Reach for it for appointment and reminder entry, where half-hour slots are faster than typing but must not be the only way.

Usage

<sui-time-picker label="Reminder time" name="remind" value="2:30 PM" min="8:00 AM" max="6:00 PM"></sui-time-picker> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-time-picker"); el.addEventListener("suiChange", (e) => console.log(e.detail)); </script>
import { SuiTimePicker } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { return <SuiTimePicker label="Reminder time" name="remind" value="2:30 PM" min="8:00 AM" max="6:00 PM" onSuiChange={(e) => console.log(e.detail)} />; }
<script setup> import { SuiTimePicker } from "@shineui/vue"; import "@shineui/core/shineui.css"; const onChange = (e) => console.log(e.detail); </script> <template> <SuiTimePicker label="Reminder time" name="remind" value="2:30 PM" min="8:00 AM" max="6:00 PM" @sui-change="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiTimePicker } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiTimePicker], template: `<sui-time-picker label="Reminder time" name="remind" value="2:30 PM" min="8:00 AM" max="6:00 PM" [(open)]="open" (suiChange)="onChange($event)"></sui-time-picker>`, }) export class Example { open = false; onChange(e: CustomEvent) { console.log(e.detail); } }

Basic

The trigger is aria-haspopup="listbox". The panel opens with the typing box focused and the selected row scrolled to the center.

<div style="max-width:220px"> <sui-time-picker label="Reminder time" name="remind" value="2:30 PM" min="8:00 AM" max="6:00 PM" help="Office hours only."></sui-time-picker> </div>

Open list and typed entry

Typing filters the list. Type 2:47 PM or 14:47 and press Enter to commit an off-step time; either spelling is accepted whatever the display clock is.

<div style="max-width:220px"> <sui-time-picker id="tm-open" label="Reminder time" value="2:30 PM" min="8:00 AM" max="6:00 PM" step="30" open></sui-time-picker> </div>

Clock and step

hour-format sets the clock the options, the trigger and the typing box use; unset, the field takes the locale's own clock. value stays HH:mm on either, as a native time input reports it. A bare 2:30 typed on the 12-hour clock takes the meridiem of the current value, or the one inside the bounds.

<div style="display:grid;gap:14px;max-width:220px"> <sui-time-picker id="tm-24" label="Batch run" value="14:00" min="00:00" max="23:45" step="15" hour-format="24" help="Fifteen minute steps."></sui-time-picker> <sui-time-picker id="tm-hour" label="Cutoff" value="5:00 PM" min="9:00 AM" max="9:00 PM" step="60"></sui-time-picker> <sui-time-picker id="tm-empty" label="Follow-up call" placeholder="Pick a time"></sui-time-picker> </div>

Seconds

seconds adds a third part to the options, the value and what posts. A typed time may leave the seconds off, and with seconds off a value that carries them is rounded down to the minute.

<div style="display:grid;gap:14px;max-width:220px"> <sui-time-picker id="tm-sec" label="Job start" seconds hour-format="24" value="14:30:00" min="00:00" max="23:59:59" step="15" help="Type 14:30:15 for a time between the steps."></sui-time-picker> <sui-time-picker id="tm-sec-12" label="Log entry" seconds value="9:05:30 AM" min="8:00 AM" max="6:00 PM" step="60"></sui-time-picker> </div>

Sizes

size is the same sm, md, lg vocabulary and the same 28, 34, 42 heights as the rest of the control family, so a time field lines up in a row with a Select or an InputText. The panel keeps its own metrics.

<div style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;align-items:end"> <sui-time-picker label="Small" size="sm" value="9:00 AM" min="8:00 AM" max="6:00 PM"></sui-time-picker> <sui-time-picker label="Medium" value="9:00 AM" min="8:00 AM" max="6:00 PM"></sui-time-picker> <sui-time-picker label="Large" size="lg" value="9:00 AM" min="8:00 AM" max="6:00 PM"></sui-time-picker> </div>

States

A trigger the user cannot open cannot be filled in, so a disabled picker never holds the form on valueMissing. An author-set error still applies and paints the trigger in the danger tone.

<div style="display:grid;gap:14px;max-width:220px"> <sui-time-picker id="tm-req" label="Call time" required min="8:00 AM" max="6:00 PM" help="Required before the reminder is scheduled."></sui-time-picker> <sui-time-picker id="tm-err" label="End time" value="9:00 AM" error="End time must be after the start time."></sui-time-picker> <sui-time-picker id="tm-invalid" label="Cutoff" value="11:30 PM" invalid></sui-time-picker> <sui-time-picker id="tm-off" label="Posted at" value="4:15 PM" disabled></sui-time-picker> </div>

Read-only

readonly keeps the trigger named and in the tab order, opens nothing, and the time still posts.

<div style="display:grid;gap:14px;max-width:320px"> <sui-time-picker id="ro-sui-time-picker" label="Reminder time" name="sui-time-picker" value="2:30 PM" readonly required min="8:00 AM" max="6:00 PM"></sui-time-picker> </div>

In a form

The picker posts HH:mm whatever clock it shows, with :ss when seconds are on, so a 12-hour and a 24-hour field post the same string. For a date and a time together, sui-date-picker with show-time posts one YYYY-MM-DDTHH:mm value, as the second field here does.

Submit to see what posts.
<form id="tm-form" style="display:grid;gap:14px;max-width:280px"> <sui-time-picker label="Daily reminder" name="reminder" value="09:30" min="08:00" max="18:00"></sui-time-picker> <sui-date-picker label="Follow-up call" name="followUp" show-time value="2026-08-24T14:30" today-date="2026-08-22"></sui-date-picker> <sui-button type="submit" variant="brand" label="Submit"></sui-button> <output id="tm-form-out" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Submit to see what posts.</output> </form>
// setup(root): properties and events setup(root) { const form = root.querySelector('#tm-form'); const out = root.querySelector('#tm-form-out'); form.addEventListener('submit', (e) => { e.preventDefault(); out.textContent = [...new FormData(form)].map(([k, v]) => `${k}=${v || '(empty)'}`).join(' '); }); }

Events

suiOpen fires after the element opens itself, and the cancelable suiClose fires before it closes and says why: escape, outside, trigger or pick. Neither bubbles past the picker. suiChange carries the committed HH:mm value and fires only when the time actually moved.

Idle.
<div style="display:flex;gap:16px;align-items:flex-start;flex-wrap:wrap"> <div style="width:220px"><sui-time-picker id="tm-events" label="Reminder time" name="remind" value="2:30 PM" min="8:00 AM" max="6:00 PM"></sui-time-picker></div> <span id="tm-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Idle.</span> </div>
// setup(root): properties and events setup(root) { const el = root.querySelector('#tm-events'); const log = root.querySelector('#tm-log'); el.addEventListener('suiOpen', () => { log.textContent = 'suiOpen. List open.'; }); el.addEventListener('suiClose', (e) => { log.textContent = `suiClose (${e.detail}).`; }); el.addEventListener('suiChange', (e) => { log.textContent = `suiChange ${e.detail}.`; }); }

Keyboard

KeyAction
ArrowDown ArrowUpOpens the list, then moves the highlight; it wraps at either end.
EnterOpens the list when closed. When open, commits the typed time if it parses, otherwise the highlighted row, and closes.
SpaceOn the trigger, opens or closes the list.
Home EndJumps the highlight to the first or the last row, unless focus is in the typing box.
EscapeCloses the list and returns focus to the trigger.
TabCloses the list and moves on.

API

<sui-time-picker>

Properties

PropertyAttributeTypeDefaultDescription
disableddisabledbooleanfalseDisables the trigger. A disabled control posts nothing and never holds the form.
errorerrorstringstring | undefinedError message; paints the trigger in the danger tone.
helphelpstringstring | undefinedHelp text under the trigger. Replaced by error when set.
hourFormathour-formatSuiTimeFormat"12" | "24" | undefinedClock the options, the trigger and the typing box are written in: "12" (2:30 PM) or "24" (14:30). Unset, the locale's own clock: 12-hour in en-US, 24-hour in de-DE. The value and the form post stay HH:mm on either clock.
invalidinvalidbooleanfalseDanger border without a message.
labellabelstringstring | undefinedLabel above the trigger; also names the option list.
localelocalestringstring | undefinedBCP 47 tag the times are written and read in, e.g. "de-DE": digits, day-period words (AM, 午後) and the default clock. Unset, the page's locale (setLocale() or <html lang>), else en-US.
maxmaxstring'23:30'Latest option, e.g. "18:00". 6:00 PM reads too.
minminstring'00:00'Earliest option, e.g. "08:00". 8:00 AM reads too.
namenamestringstring | undefinedForm control name. Posts the HH:mm value (HH:mm:ss with seconds) whatever clock the field shows; an empty picker posts an empty string and a disabled one posts nothing. Reflected: entry construction reads the content attribute, and the three framework bindings set props, so an unreflected name would post nothing.
openopenbooleanfalseOpens the panel. Mutable; the element closes itself on pick, outside pointer and Escape.
placeholderplaceholderstringstring | undefinedTrigger text while nothing is picked.
readOnlyreadonlybooleanfalseDisplay only: the trigger keeps its name and its tab stop, opens nothing, and the time still posts. Neither a read-only nor a disabled picker holds the form on valueMissing.
requiredrequiredbooleanfalseA time must be picked for the surrounding form to be valid. A disabled picker never holds the form, since the user could not open it.
secondssecondsbooleanfalseShows and posts seconds: 2:30:00 PM, or 14:30:00 on the 24-hour clock. A typed time may leave them off. Off, a value with seconds is rounded down to the minute.
sizesize"lg" | "md" | "sm"'md'Trigger height: sm 28, md 34 (default), lg 42, from the --control-* tokens, the same three the rest of the control family takes. The option panel keeps its own metrics.
stepstepnumber30Minutes between options. A time off the step is typed.
valuevaluestring''Selected time as HH:mm on the 24-hour clock (14:30), with :ss when seconds is on, whatever clock the field shows, the way <input type="time"> reports its value; 2:30 PM is accepted and normalized on the way in. Mutable; updated before suiChange fires.

Events

EventDetailReactVueDescription
suiChangestringonSuiChange@sui-changeFires when a time commits; detail is the HH:mm value, e.g. "14:30" ("14:30:15" with seconds).
suiCloseSuiCloseReason"cancel" | "close-button" | "escape" | "outside" | "pick" | "scrim" | "trigger"onSuiClose@sui-closeFires before the element closes the panel itself; detail is 'escape' | 'outside' | 'trigger' | 'pick'. Cancelable: preventDefault() keeps it open. Does not bubble: a preventDefault() in a dialog's own suiClose listener cannot veto this one.
suiOpenvoidonSuiOpen@sui-openFires after the element opens the panel itself. Does not bubble, so a dialog around the picker never takes it for its own.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Moves keyboard focus to the trigger.

Types

SuiCloseReason typeWhy a dismissible surface is closing. Detail of the cancelable suiClose. A consumer expresses "a stray backdrop click must not close this" by calling preventDefault() on the reason it does not accept, which is why none of these needs a prop of its own.
No members.
SuiTimeFormat typeClock display: 2:30 PM or 14:30.
No members.

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

See also