f-ui
Components

Time Range Picker

Range picker with integrated segmented start/end field, dual time spinners, and optional confirm flow using plain Date tuples.

Time Range Picker combines a segmented start/end time field and clock icon in one bordered shell, with dual column spinners in a popover. Values are a plain [Date, Date] | null tuple — only clock portions are read or written.

Interactions

EventBehavior
Focus the start or end fieldDoes not open the popover (open via clock trigger).
Click the clock iconToggles the popover open or closed.
Select or type a timeUpdates the start or end value.
Escape or click outsideCloses the popover.

Design guide

  • Range over two pickers — Use one control for start/end so ordering and validation stay in sync.
  • Format drives columns — Same format / use12Hours rules as Time Picker; both spinners share the template.
  • Order enforcementorder (default true) keeps the end time at or after the start when either side changes.

Installing

pnpm dlx shadcn@latest add https://ui.isaacfei.com/r/time-range-picker.json

Usage

import { useState } from "react";
import type { TimeRangeTuple } from "@/components/f-ui/date-internals/use-segmented-time-range-field";
import { TimeRangePicker } from "@/components/f-ui/time-range-picker/time-range-picker";

export function Example() {
  const [range, setRange] = useState<TimeRangeTuple | null>(null);
  return (
    <TimeRangePicker
      label="Hours"
      value={range}
      onChange={setRange}
    />
  );
}

Examples

Start and End

900AM
500PM

Segmented start/end field with dual spinners in the popover.

"use client";

import { useState } from "react";

import type { TimeRangeTuple } from "@/components/f-ui/date-internals/use-segmented-time-range-field";
import { TimeRangePicker } from "@/components/f-ui/time-range-picker/time-range-picker";

export function TimeRangePickerDemo() {
  const [range, setRange] = useState<TimeRangeTuple | null>(() => [
    new Date(2026, 4, 21, 9, 0, 0, 0),
    new Date(2026, 4, 21, 17, 0, 0, 0),
  ]);

  return (
    <div className="max-w-md w-full">
      <TimeRangePicker
        label="Office hours"
        value={range}
        onChange={setRange}
        description="Segmented start/end field with dual spinners in the popover."
        order
      />
    </div>
  );
}

API Reference

PropTypeDefault
value[Date, Date] | null
onChange(range: [Date, Date] | null) => void
formatstringHH:mm or hh:mm a
use12Hoursbooleanfalse
step{ hour?, minute?, second? }1 per unit
disabledTime(current: Date) => { hours?, minutes?, seconds? }
orderbooleantrue
changeOnScrollbooleanfalse
disabledbooleanfalse
isInvalidbooleanfalse
labelReactNode
description / errorMessageReactNode
localestringi18n provider
classNames{ root?, field?, shell?, … }

Each Date in the tuple uses only its clock portion (normalized to today in the local timezone).

On this page