Components · Forms & selection
Time picker
Scrollable columns for hour, minute and period on the popover surface. Options are mono-spaced so every digit lines up; the selected option fills Primary. Snap-scrolling keeps the wheels tidy.
Preview
Hr
08
09
10
11
12
Min
00
15
30
45
AM/PM
AM
PM
Build with the skill
No package to install — hand this to your AI to generate or migrate the component in your stack.
Apply the Zetta "Time picker" component to this project using the zetta-design-md skill.
WHEN TO USE
- Migrating or fixing existing code (the common case): audit the current implementation against the `time-picker` spec (variants: option, option-selected) and CORRECT every deviation — replace off-spec values, don't only add new ones.
- New component: build it to the `time-picker` spec in this project's stack.
AUDIT & FIX (when the component already exists)
Compare it to the skill's spec and fix, don't just append: hardcoded colors/hex, off-scale radius, wrong fonts, dark/navy fills, shadows used in place of borders (or missing borders), brand lime used as a CTA or interaction (must be primary indigo), and any missing or wrong hover / focus / active / disabled / invalid state or missing 2px focus ring. Replace each with the correct Zetta token.
ALWAYS
- Pull the authoritative spec and tokens from the skill — never guess or hardcode values.
- Use this project's own component conventions and framework idioms.
- Honor the Zetta guardrails (brand = identity only, primary = interaction, shadows for overlays) and verify in Light, Dark, and Accessibility. Anatomy & rules
[ 09:30 🕐 ] ← trigger input
┌─────────────────────┐
│ Hr Min AM/PM │ ← time-picker-column-header
│ ─── │ ─── │ ───── │
│ 08 │ 28 │ AM │
│ [09]│ [30]│ [PM] │ ← selected: primary fill
│ 10 │ 32 │ │
└─────────────────────┘
Parts:
- Trigger input — standard
inputwith trailingscheduleicon (16px,{colors.muted}) - Container —
{colors.popover}surface, flex row,{elevation.shadow.overlay} - Column header — 12px / 500 Geist,
{colors.muted}label (Hr / Min / AM·PM) - Scroll column —
maxHeight: 216px(6 visible at 36px each),scroll-snap-type: y mandatory - Option — 36×52px min, Geist Mono 14px for alignment; selected =
{colors.primary}fill
| State | Input border | Fill | Notes |
|---|---|---|---|
| Default (empty) | {colors.border-strong} |
{colors.surface-card} |
Placeholder visible |
| Default (has value) | {colors.border-strong} |
{colors.surface-card} |
Time value shown |
| Hover | {colors.muted} |
{colors.surface-card} |
— |
| Focus / Open | {colors.primary} + glow |
{colors.surface-card} |
Time picker panel opens |
| Column item hover | {colors.surface-muted} |
— | Within panel |
| Column item selected | {colors.primary} fill |
{colors.primary-text} |
— |
| Disabled | {colors.disabled-border} |
{colors.disabled-bg} |
No panel opens |
| Error | {colors.danger} + glow |
{colors.surface-card} |
— |
When to use
- Any form field requiring time input.
When not to use
- When combined with date selection — use Date Time Picker.
Do
- Use
minuteStep: 15or30for scheduling contexts where minute precision isn't needed. - Default to 24-hour format unless the product is consumer-facing in a 12-hour locale.
Don't
- Don't allow free-text time entry as the only input — pair with the scroll picker.
- Placeholder format: matches expected format — "HH:MM" (24h) or "HH:MM AM/PM" (12h).
- Format: use 24-hour by default for international contexts. 12-hour for consumer products.
- Error: "Time is required." or "Time must be between 09:00 and 17:00."
- Label: always visible — "Start time", "Meeting time", not just "Time".
Keyboard
| Key | Behavior |
|---|---|
Tab |
Move focus to the time input |
Enter / Space |
Open / close the time picker panel |
Up / Down Arrow |
Increment / decrement the focused time unit |
Tab (panel open) |
Move between hour and minute columns |
Enter |
Select the focused option |
Escape |
Close the panel without selecting |
Accessibility
- ARIA: trigger uses
aria-haspopup="dialog". Container usesrole="dialog"witharia-label="Choose time". - Columns:
role="listbox", each optionrole="option",aria-selectedfor selected. - Announce: selected value on close — "Time selected: 09:30 PM".
Spec
Values and token references resolved straight from the Zetta spec.
time-picker-container
- backgroundColor
- {colors.popover}
- textColor
- {colors.popover-foreground}
- borderColor
- {colors.hairline}
- borderWidth
- 1px
- borderRadius
- {rounded.base}
- shadow
- {elevation.shadow.overlay}
- display
- flex
- flexDirection
- row
- padding
- {spacing.md}
- gap
- {spacing.sm}
- minWidth
- 180px
time-picker-column-header
- textColor
- {colors.muted}
- fontFamily
- Geist
- fontSize
- 12px
- fontWeight
- 500
- textAlign
- center
- paddingBottom
- {spacing.xs}
time-picker-scroll-column
- maxHeight
- 216px
- overflowY
- auto
- scrollSnapType
- y mandatory
time-picker-option
- height
- 36px
- minWidth
- 52px
- borderRadius
- {rounded.base}
- fontFamily
- Geist Mono
- fontSize
- 14px
- fontWeight
- 400
- textColor
- {colors.ink}
- display
- flex
- alignItems
- center
- justifyContent
- center
- cursor
- pointer
- scrollSnapAlign
- start
time-picker-option-hover
- backgroundColor
- {colors.primary-bg}
- textColor
- {colors.primary}
time-picker-option-selected
- backgroundColor
- {colors.primary}
- textColor
- {colors.primary-text}
- fontWeight
- 500
time-picker-option-focus
- outline
- 2px solid {colors.border-focus}
- outlineOffset
- 2px
time-picker-option-disabled
- textColor
- {colors.disabled-text}
- cursor
- not-allowed
Known gaps
- Minute step interval — runtime config, not a token.
- Timezone display and conversion — product-level concern, not in scope for tokens.