@mshafiqyajid/react-time-picker
Headless time picker hook and styled component. Text input with dropdown columns for hours, minutes, and optional seconds. 12h and 24h formats, AM/PM toggle, minute step, min/max bounds, full keyboard navigation, ARIA listbox, portal to document.body. Zero dependencies.
Playground #
Props
TSX
import { TimePickerStyled } from "@mshafiqyajid/react-time-picker/styled";
import "@mshafiqyajid/react-time-picker/styles.css";
<TimePickerStyled
value={value}
onChange={setValue}
/>Install #
npm install @mshafiqyajid/react-time-picker Quick start #
import { TimePickerStyled } from "@mshafiqyajid/react-time-picker/styled";
import "@mshafiqyajid/react-time-picker/styles.css";
const [time, setTime] = useState("09:00");
<TimePickerStyled value={time} onChange={setTime} label="Meeting time" />
// 12h format with seconds
<TimePickerStyled
value={time}
onChange={setTime}
format="12h"
showSeconds
step={15}
label="Alarm time"
/> Inline mode #
Set inline to always show the column picker without a text input or floating dropdown. Useful for embedding in forms or panels.
<TimePickerStyled
value={time}
onChange={setTime}
inline
label="Pick a time"
/> Clearable #
Set clearable to show a Γ button when a value is set. Clicking it calls onChange("").
<TimePickerStyled
value={time}
onChange={setTime}
clearable
label="Meeting time"
/> Headless #
import { useTimePicker } from "@mshafiqyajid/react-time-picker";
const {
inputProps,
hourProps,
minuteProps,
periodProps,
hours,
minutes,
period,
isOpen,
open,
close,
clear,
} = useTimePicker({
defaultValue: "14:30",
format: "24h",
clearable: true,
onChange: (v) => console.log(v),
});
return (
<div>
<input {...inputProps} placeholder="HH:mm" />
{isOpen && (
<div>
{/* custom column UI */}
<button onClick={() => hourProps.onChange(10)}>10</button>
<button onClick={() => minuteProps.onChange(30)}>30</button>
</div>
)}
</div>
); Min / max bounds #
Pass min and max as "HH:mm" strings to restrict selectable times. Values outside the range are rejected in both the dropdown columns and keyboard arrow-key increments.
{/* Business hours only: 09:00β17:00 */}
<TimePickerStyled
value={time}
onChange={setTime}
min="09:00"
max="17:00"
label="Appointment time"
/> API #
| Prop | Type | Default | Description |
|---|---|---|---|
| value / defaultValue | string | β | Controlled or uncontrolled time value. "HH:mm", "HH:mm:ss", or "hh:mm AM" |
| onChange | (v: string) => void | β | Called on every change. Always emits 24h canonical "HH:mm" or "HH:mm:ss" |
| format | "12h" | "24h" | "24h" | Display format. Input accepts both; output is always 24h canonical |
| showSeconds | boolean | false | Show seconds column in dropdown and include seconds in output |
| step | number | 1 | Minute step for the dropdown column (1, 5, 15, 30 are common) |
| min / max | string | β | Minimum / maximum time "HH:mm". Values outside bounds are rejected |
| size | "sm" | "md" | "lg" | "md" | Input height: 32 / 40 / 48 px |
| tone | "neutral" | "primary" | "success" | "danger" | "neutral" | Focus ring color |
| disabled / readOnly / required / invalid | boolean | false | Form-control states |
| label / hint / error | string | β | Field label, hint text, and error message. error sets tone="danger" automatically |
| id / name / placeholder | string | β | Native input attributes |
| className / style | string / CSSProperties | β | Applied to the root wrapper |
| ref | Ref<HTMLInputElement> | β | Forwarded to the primary input element |
| clearable | boolean | false | Show a Γ clear button inside the input when a value is set. Calls onChange("") |
| inline | boolean | false | Always render the column picker without a floating dropdown or text input trigger |
| prefix | ReactNode | β | Decorative node rendered before the input text (e.g. a clock icon) |
| suffix | ReactNode | β | Decorative node rendered after the input text (e.g. a timezone label) |
| onFocus | () => void | β | Called when the input gains focus |
| onBlur | () => void | β | Called when the input loses focus |
| locale | string | "en-US" | BCP 47 locale tag used to localise AM/PM labels in 12h mode (e.g. "fr-FR", "de-DE") |
Keyboard #
| Key | Action |
|---|---|
| Click / Focus | Opens dropdown |
| ArrowDown (on input, closed) | Opens dropdown |
| ArrowUp / ArrowDown (on input, focused) | Increments / decrements the segment under the cursor (hours, minutes, seconds, or period) |
| Escape | Closes dropdown |
| Tab | Closes dropdown and moves focus |
| ArrowUp / ArrowDown (in column) | Navigate options |
| Enter / Space (on option) | Selects the option |
| Click outside | Closes dropdown |
CSS variables #
:root {
--rtp-bg: #ffffff;
--rtp-border: #e4e4e7;
--rtp-border-hover: #a1a1aa;
--rtp-border-focus: #6366f1;
--rtp-fg: #18181b;
--rtp-label-fg: #3f3f46;
--rtp-hint-fg: #71717a;
--rtp-icon-fg: #71717a;
--rtp-placeholder: #a1a1aa;
--rtp-sep-fg: #a1a1aa;
--rtp-dropdown-bg: #ffffff;
--rtp-dropdown-border: #e4e4e7;
--rtp-dropdown-shadow: 0 4px 16px rgba(0, 0, 0, 0.10);
--rtp-option-active-bg: #eef2ff;
--rtp-option-active-fg: #4338ca;
--rtp-option-hover-bg: #f4f4f5;
--rtp-height-sm: 2rem;
--rtp-height-md: 2.5rem;
--rtp-height-lg: 3rem;
--rtp-font-size-sm: 0.8125rem;
--rtp-font-size-md: 0.875rem;
--rtp-font-size-lg: 1rem;
--rtp-radius-sm: 6px;
--rtp-radius-md: 8px;
--rtp-radius-lg: 10px;
--rtp-duration: 150ms;
--rtp-ease: cubic-bezier(0.4, 0, 0.2, 1);
}