Zetta v1.6

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

Build with the skill

No package to install — hand this to your AI to generate or migrate the component in your stack.

Prompt for your AI
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 input with trailing schedule icon (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 columnmaxHeight: 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

Do

  • Use minuteStep: 15 or 30 for 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 uses role="dialog" with aria-label="Choose time".
  • Columns: role="listbox", each option role="option", aria-selected for 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.