Skip to content
Drivn
5 min read

Next.js Time Picker for the App Router

Add a Next.js time picker with 12/24-hour clocks, seconds, and a native time input for Server Actions. Drivn ships one client component, zero runtime deps.

Time selection is where a Next.js form gets fiddly fast: a raw <input type="time"> looks different in every browser, and rolling your own dropdown of hours and minutes means writing scroll logic, twelve-versus-twenty-four-hour math, and AM/PM handling by hand. A time picker packages all of that into one control — a trigger showing the chosen time and a popover of scrollable columns you tap to set it. In the App Router the only question is where it runs: the picker holds popover and scroll state, so it lives on the client, while the page around it stays a Server Component.

Drivn's Time Picker is built for that boundary. After install it lives in src/components/ui/time-picker.tsx as a 'use client' component that composes the Drivn Popover and Input — no date library, no Radix, just React and the local cn() helper. It is fully controlled: you hold the selected Date in state and pass selected and onSelect, and the component renders Hr, Min, and optional Sec columns plus an AM/PM switch for the twelve-hour format. A TimePicker.Input sub-component gives you a native <input type="time"> for forms that post to a Server Action.

This guide installs Drivn in a Next.js 16 project, renders the Time Picker in a client component, switches between twelve- and twenty-four-hour clocks with seconds, wires the native input variant into a form, and restyles the one styles object. For the full reference see the Time Picker docs; for the shadcn/ui comparison see Drivn vs shadcn/ui Time Picker.

Install in a Next.js 16 project

Drivn installs through a small CLI that writes the component source straight into your repository — there is no runtime package to version-lock. From the root of your Next.js 16 project run npx drivn add time-picker. The Time Picker builds on the Drivn Popover and Input, so the CLI resolves those dependencies and writes time-picker.tsx alongside popover.tsx and input.tsx if they are not already present. The CLI reference documents every flag, including targeting a custom directory or adding several components at once. After install you own the files; a future Drivn release will not overwrite them. Commit the change for a clean baseline, confirm the files landed under your UI directory, and you are ready to render the picker.

1# from the root of your Next.js 16 project
2npx drivn add time-picker

Render the Time Picker in a client component

Unlike the Textarea, the Time Picker holds state: it opens a Popover and scrolls the selected cell into view with a React.useEffect, so its source begins with a 'use client' directive. That means it belongs in a client component, and because it is fully controlled you hold the selected Date yourself. Mark the wrapper 'use client', keep the value in React.useState<Date | undefined>(), and pass selected and onSelect — the component calls onSelect with a fresh Date each time the reader taps an hour, minute, or period. You can still import that wrapper into a Server Component page, so only the picker hydrates while the rest of the route stays on the server. The trigger shows a Clock icon and the formatted time, falling back to the placeholder when nothing is selected. See the installation guide for project setup.

1'use client'
2
3import * as React from 'react'
4import { TimePicker } from '@/components/ui/time-picker'
5
6export function MeetingTime() {
7 const [time, setTime] = React.useState<Date | undefined>()
8
9 return <TimePicker selected={time} onSelect={setTime} />
10}

Switch between 12- and 24-hour clocks

The Time Picker defaults to a twenty-four-hour clock, rendering hours 0023 in the first column. Pass format="12h" and it swaps to a twelve-hour clock — hours 112 plus an AM/PM switch beside the columns — and formats the trigger label with hour12: true. Add showSeconds to render a third Sec column for precision down to the second. When the built-in toLocaleTimeString label is not what you want, pass a formatTime function that receives the selected Date and returns your own string, which is how you localize the trigger to a cs-CZ or de-DE locale. Each of these is a prop on the same component, so moving a picker from twenty-four-hour to a localized twelve-hour clock with seconds is three attributes, not a re-implementation. See the Time Picker examples for more layouts.

1<TimePicker
2 selected={time}
3 onSelect={setTime}
4 format="12h"
5 showSeconds
6/>

A native time input for forms and Server Actions

When you would rather post a value than manage a popover, the TimePicker.Input sub-component renders the Drivn Input as a native <input type="time">. It takes the same selected and onSelect pair, deriving its HH:MM value from the Date and parsing the browser's time control back into a Date on change. Because it is a native input, giving it a name means its value posts with the surrounding <form> — drop it into a Server Action form and read the time off FormData on the server. It also accepts native min, max, and step attributes to constrain the allowed range, for example step={900} for fifteen-minute increments. The wrapper still owns the Date in state, so mark it 'use client', but the field itself submits like any other form control.

1'use client'
2
3import * as React from 'react'
4import { TimePicker } from '@/components/ui/time-picker'
5
6export function StartTime() {
7 const [time, setTime] = React.useState<Date | undefined>()
8
9 return (
10 <TimePicker.Input
11 name="start"
12 selected={time}
13 onSelect={setTime}
14 />
15 )
16}

Customize the one styles object

Every class the Time Picker renders lives in one const styles object at the top of the file you own — the trigger, the scrollable columns, each cell, and the active cell all read from a named key. There are no variant maps: to restyle the picker you edit the key. The active hour, minute, and AM/PM cells share styles.cellActive, which paints the selected value with bg-foreground text-background; the trigger reads border-input and the ring-ring focus ring, so recoloring your Tailwind tokens in theming re-themes the picker for dark and light mode automatically. Because the picker composes the Drivn Popover, the dropdown surface inherits that component's border and shadow — style it once and every popover-backed control follows. The styles keys below are copied verbatim from time-picker.ts.

1const styles = {
2 base: 'relative block max-w-xs',
3 trigger: cn(
4 'flex items-center gap-2 w-full h-10 px-3',
5 'border border-input rounded-[10px] text-sm',
6 'text-foreground transition-[color,box-shadow]',
7 'font-normal hover:scale-100',
8 'focus:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50',
9 'focus-visible:border-ring',
10 'disabled:opacity-50 disabled:cursor-default'
11 ),
12 cell: cn(
13 'flex items-center justify-center w-14 h-12',
14 'text-base rounded-lg hover:bg-accent',
15 'transition-colors cursor-pointer'
16 ),
17 cellActive: cn(
18 'bg-foreground text-background font-medium',
19 'hover:bg-foreground hover:text-background'
20 ),
21}
Get started

Install Drivn in one command

Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.

npx drivn@latest create

Requires Node 18+. Works with npm, pnpm, and yarn.

Enjoying Drivn?
Star the repo on GitHub to follow new component releases.
Star →

Frequently asked questions

The Time Picker itself is a client component — its source opens with 'use client' because it opens a Popover and scrolls the selected cell into view with useEffect. You render it inside a client wrapper that owns the selected Date in useState, and you can import that wrapper straight into a Server Component page, so only the picker hydrates while the rest of the route stays on the server. For a form control that renders on the server, use the native TimePicker.Input variant instead.

Pass format="12h" on the TimePicker. The hour column then renders 1 through 12, an AM/PM switch appears beside the columns, and the trigger label formats with hour12: true. The default is format="24h", which renders hours 00 through 23 with no period switch. Add showSeconds alongside it for a third Sec column, and both are plain props on the same component — no separate build.

Use the TimePicker.Input sub-component, which renders a native <input type="time">. Give it a name and it posts its HH:MM value with the surrounding <form>, so a Server Action reads it off FormData on the server. The field is still controlled through selected and onSelect, so its wrapper is a client component, but the value submits natively like any other form control. Native min, max, and step attributes constrain the allowed range.

No. The Time Picker works entirely with the native JavaScript Date object — it reads getHours, getMinutes, and getSeconds, and builds a new Date on each selection. There is no date-fns, dayjs, or Luxon dependency and no Radix. Its only imports past React are the Drivn Popover, the Drivn Input, and the local cn helper, so nothing extra lands in your bundle when you add it.