Skip to content

[RFC] Add composable & accessible TimePicker component (@reui/time-picker) #144

Description

@focus0802

RFC / PR Proposal: Composable & Accessible TimePicker for ReUI

Target Repository: keenthemes/reui / ReUI Registry
Component Name: time-picker
Category: Form / Pickers
License: MIT / Free Tier
Status: Ready for Upstream Submission


1. Motivation & Problem Statement

ReUI currently provides the powerful date-selector (@reui/date-selector) and event-calendar, but lacks a dedicated Time Picker primitive. While developers frequently need precise time input (e.g. appointment scheduling, business hours, attendance logs, meeting agendas), existing shadcn/community time pickers suffer from significant shortcomings:

  1. Tight coupling to JavaScript Date objects: Most existing solutions force time into full Date objects (requiring arbitrary dates, timezone headaches, and heavy serialization logic) rather than clean "HH:mm" / "HH:mm:ss" string primitives.
  2. Monolithic & Non-composable: Lacking Radix/Base UI-style composable primitives, making it nearly impossible to customize columns, inject custom actions, or render inline without a popover.
  3. Inaccessible keyboard UX: Missing proper WAI-ARIA listbox/option roles and 2D keyboard navigation (jumping between hour/minute columns and navigating items with arrow keys).
  4. Visual jitter & imperfect alignment: Common scroll pickers exhibit clipping numbers, misaligned dividers, or awkward gaps between the columns and action footers.

The ReUI TimePicker solves all of these problems with an enterprise-grade, headless-composable architecture inspired by Base UI and shadcn design standards.


2. Visual Preview (Light & Dark Modes)

Variant Light Mode Dark Mode
Standard 24-Hour
• 2 columns (Hour / Minute)
• Actions: Now / Clear / OK
Standard 24-Hour (Light) Standard 24-Hour (Dark)
12-Hour + Seconds
• 4 columns (Hour / Minute / Sec / AM-PM)
• Second-level precision
12-Hour with Seconds (Light) 12-Hour with Seconds (Dark)
15m Step & Bounds
• 15-minute intervals (00, 15, 30, 45)
• Operating bounds (08:30–17:30 disabled slots)
15m Step & Bounds (Light) 15m Step & Bounds (Dark)
Composable Raw
• Custom trigger styling
• Selective footer without Clear
Composable Raw (Light) Composable Raw (Dark)

3. Component Highlights

  • Composable Primitives & Shorthand Support: Use either the one-line <TimePicker value={time} onChange={setTime} /> shorthand or break down into sub-components (TimePickerTrigger, TimePickerContent, TimePickerGroup, TimePickerColumn, TimePickerItem, TimePickerFooter, TimePickerNow, TimePickerClear, TimePickerConfirm).
  • Precision & Step Intervals: Configurable minuteStep, hourStep, secondStep, and showSeconds toggle.
  • 12/24 Hour Formats: Seamless support for standard 24h mode and 12h mode with AM/PM column.
  • WAI-ARIA APG Compliant: Full keyboard support (ArrowUp/ArrowDown for list items, ArrowLeft/ArrowRight for column jumping, Home/End, Enter/Space for selection).
  • Pixel-perfect Scroll & Alignment:
    • Exact 5-item visible height (h-[178px]) with CSS scroll snap (snap-y snap-mandatory).
    • Seamless T-junction divider line extending vertically from top to bottom footer without gaps.
    • Zero opacity fade distortion on visible numbers.
  • Bounds & Constraints: Support for minTime, maxTime, and custom disabling predicates (disabledHours, disabledMinutes, disabledSeconds, disabledTime).

4. Primitives & Anatomy

import {
  TimePicker,
  TimePickerTrigger,
  TimePickerContent,
  TimePickerGroup,
  TimePickerColumn,
  TimePickerItem,
  TimePickerFooter,
  TimePickerNow,
  TimePickerClear,
  TimePickerConfirm,
  useTimePicker,
} from "@/components/ui/time-picker";

Anatomy Tree

<TimePicker>
  <TimePickerTrigger />
  <TimePickerContent>
    <TimePickerGroup>
      <TimePickerColumn type="hours" />
      <TimePickerColumn type="minutes" />
      <TimePickerColumn type="seconds" />
      <TimePickerColumn type="period" />
    </TimePickerGroup>
    <TimePickerFooter>
      <TimePickerClear />
      <TimePickerNow />
      <TimePickerConfirm />
    </TimePickerFooter>
  </TimePickerContent>
</TimePicker>

5. API Reference

<TimePicker /> (Root)

When used without children, <TimePicker /> renders a complete out-of-the-box popover trigger and content. When children are provided, it acts as a context provider.

Prop Type Default Description
value string undefined Controlled time string ("HH:mm" or "HH:mm:ss").
defaultValue string undefined Uncontrolled initial value.
onChange (time: string) => void undefined Callback invoked when time changes.
use12Hour boolean false Whether to display 12-hour format with AM/PM column.
showSeconds boolean false Whether to display seconds column.
hourStep number 1 Interval between selectable hours.
minuteStep number 1 Interval between selectable minutes (e.g. 5, 15, 30).
secondStep number 1 Interval between selectable seconds.
minTime string undefined Earliest selectable time ("HH:mm").
maxTime string undefined Latest selectable time ("HH:mm").
disabled boolean false Disables trigger and interaction.
size "sm" | "default" | "lg" "default" Size variant for trigger.
locale "en" | "zh" "en" Language preset for column headers, buttons, and ARIA labels.
i18n Partial<TimePickerI18nConfig> undefined Custom label and ARIA overrides for complete internationalization.
placeholder string "Select time" Placeholder displayed when value is empty.
clearable boolean true Shows clear button in footer.
open boolean undefined Controlled popover open state.
onOpenChange (open: boolean) => void undefined Callback when popover opens/closes.

Sub-primitives

  • <TimePickerTrigger />: Interactive trigger button supporting custom icons, classes, and sizing variants.
  • <TimePickerContent />: Popover content container with normalized border, shadow, and backdrop.
  • <TimePickerGroup />: Container for columns with cohesive vertical border division.
  • <TimePickerColumn />: Column listbox (type="hours" | "minutes" | "seconds" | "period").
  • <TimePickerItem />: Individual selectable time option item.
  • <TimePickerFooter />: Bottom actions container.
  • <TimePickerNow />: Shortcut button to select current local time.
  • <TimePickerClear />: Shortcut button to clear selection.
  • <TimePickerConfirm />: Action button to commit and close popover.

6. Worked Examples (c-time-picker-*)

Example 1: Basic 24-Hour Time Picker (c-time-picker-1)

import { useState } from "react";
import { TimePicker } from "@/components/ui/time-picker";

export default function BasicTimePickerExample() {
  const [time, setTime] = useState<string>("09:30");

  return (
    <div className="flex flex-col gap-2 max-w-xs">
      <label className="text-sm font-medium">Meeting Time</label>
      <TimePicker
        value={time}
        onChange={setTime}
        placeholder="Select time"
        clearable
      />
    </div>
  );
}

Example 2: 12-Hour Format with Seconds (c-time-picker-2)

import { useState } from "react";
import { TimePicker } from "@/components/ui/time-picker";

export default function TimePicker12HourExample() {
  const [time, setTime] = useState<string>("02:45:00");

  return (
    <div className="flex flex-col gap-2 max-w-xs">
      <label className="text-sm font-medium">Broadcast Schedule</label>
      <TimePicker
        value={time}
        onChange={setTime}
        use12Hour
        showSeconds
      />
    </div>
  );
}

Example 3: Stepped Intervals & Operating Hours (c-time-picker-3)

import { useState } from "react";
import { TimePicker } from "@/components/ui/time-picker";

export default function AppointmentTimePickerExample() {
  const [time, setTime] = useState<string>("10:00");

  return (
    <div className="flex flex-col gap-2 max-w-xs">
      <label className="text-sm font-medium">Appointment Slot (15-min intervals)</label>
      <TimePicker
        value={time}
        onChange={setTime}
        minuteStep={15}
        minTime="08:00"
        maxTime="18:00"
      />
    </div>
  );
}

Example 4: Composable Raw Custom Layout (c-time-picker-4)

import { useState } from "react";
import {
  TimePicker,
  TimePickerTrigger,
  TimePickerContent,
  TimePickerGroup,
  TimePickerColumn,
  TimePickerFooter,
  TimePickerNow,
  TimePickerConfirm,
} from "@/components/ui/time-picker";
import { Button } from "@/components/ui/button";

export default function ComposedTimePickerExample() {
  const [time, setTime] = useState<string>("14:00");

  return (
    <TimePicker value={time} onChange={setTime}>
      <TimePickerTrigger asChild>
        <Button variant="outline" className="font-mono text-sm">
          {time || "Custom Trigger"}
        </Button>
      </TimePickerTrigger>
      <TimePickerContent align="start">
        <TimePickerGroup>
          <TimePickerColumn type="hours" label="Hour" />
          <TimePickerColumn type="minutes" label="Minute" />
        </TimePickerGroup>
        <TimePickerFooter>
          <TimePickerNow />
          <TimePickerConfirm />
        </TimePickerFooter>
      </TimePickerContent>
    </TimePicker>
  );
}

7. Verification & Quality Gates

  • TypeScript: Fully typed props with strict null checks and generics.
  • Accessibility: WAI-ARIA APG role="listbox", role="option", aria-selected, aria-disabled, aria-activedescendant.
  • Keyboard Navigation: Arrows Up/Down/Left/Right, Home/End, Enter, Space.
  • Themes: Dark/Light mode tested; strictly utilizes semantic CSS variables (background, foreground, border, primary, muted-foreground).
  • Tailwind CSS v4 & v3: No custom arbitrary values that break when imported into external registries.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions