Skip to content
Drivn
5 min read

Next.js Date Picker for the App Router

Add a Next.js Date Picker with a popover calendar, range mode, and custom formatting. Drivn ships it as one file — no Radix, no floating-ui, no glue.

Date inputs are one of the fussier pieces of a Next.js App Router form. The picker has to open a floating panel, close on an outside click or the Escape key, track a selected date on the client, and still sit inside a page that mostly renders on the server. Wire that by hand and you end up with a popover library, a calendar library, and a pile of glue between them.

Drivn's Date Picker collapses that into one file. After install it lives in src/components/ui/date-picker.tsx, marks itself 'use client', and composes the local Calendar inside a trigger button — no Radix Popover, no headless floating-ui, no second state machine. It imports React, the Calendar, lucide-react for the trigger icon, and the cn utility, and nothing else. Single-date selection, range selection via DatePicker.Range, custom label formatting, dropdown year navigation, and locale support are all driven through props.

This guide installs Drivn in a Next.js 16 project, renders the Date Picker as a controlled client island, formats the trigger label, switches to range mode, and bounds the year dropdown. Every snippet comes from the component's real API. For the full reference see the Date Picker docs; for the shadcn/ui comparison see Drivn vs shadcn/ui Date Picker.

Install in a Next.js 16 project

Drivn installs through a small CLI that writes the component source directly into your repository — there is no runtime npm package to version-lock. From the root of your Next.js 16 project run npx drivn add date-picker. Because the Date Picker imports the Calendar, the CLI pulls that file in too, and adds the two underlying dependencies — react-day-picker for the grid and lucide-react for the trigger icon — to your package.json if they are missing. The CLI reference documents every flag, including targeting a custom directory or installing several components at once. After install you own both files; future Drivn releases will not overwrite them. Commit the change for a clean baseline before customizing.

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

Render as a controlled client island

An App Router page is a server component by default, so it cannot call useState. The Date Picker is marked 'use client' at the top of its source because it tracks open state and the selected date with hooks. The clean pattern is to keep the picker and its state inside a small client component, then render that island in an otherwise server-rendered page. Import it from @/components/ui/date-picker, hold the date in useState, and bind selected and onSelect. No dynamic() import and no SSR-disable flag are needed — Next.js inserts the client boundary at the import. The installation guide covers project setup.

1'use client'
2import { useState } from 'react'
3import { DatePicker } from '@/components/ui/date-picker'
4
5export function DueDate() {
6 const [date, setDate] = useState<Date | undefined>()
7 return (
8 <DatePicker
9 selected={date}
10 onSelect={setDate}
11 />
12 )
13}

How the panel opens and closes

Unlike most date inputs, the Drivn picker does not pull in a popover primitive. The trigger is a plain <button> and the calendar sits in an absolutely positioned panel directly beneath it. Opening and closing is handled by two effects in the source: one listens for a mousedown outside the component ref and closes the panel, the other closes it on the Escape key. The panel animates with a transition-[opacity,scale] and toggles pointer-events-none while hidden, so it never intercepts clicks when closed. The relevant slice of the styles object is below, copied from date-picker.tsx — edit these classes to reposition or restyle the panel.

1const styles = {
2 base: 'relative',
3 // ...trigger, placeholder, icon, text
4 content: cn(
5 'absolute top-full left-0 mt-1 z-50',
6 'transition-[opacity,scale] duration-150 ease-out'
7 ),
8}
9
10// open ? 'opacity-100 scale-100'
11// : 'opacity-0 scale-95 pointer-events-none'

Format the trigger label

By default the trigger shows the selected date formatted with toLocaleDateString as a short month, numeric day, and year — Jun 14, 2026. To match a different convention, pass a formatDate function that receives the Date and returns the string you want in the trigger. This is a pure formatting hook; it does not change the value stored in state, only the label. The same function applies to both endpoints in range mode. Pair it with the placeholder prop to set the text shown before any date is chosen. See the Date Picker docs for the full prop table.

1<DatePicker
2 selected={date}
3 onSelect={setDate}
4 formatDate={(d) =>
5 d.toLocaleDateString('en-GB', {
6 day: '2-digit',
7 month: '2-digit',
8 year: 'numeric',
9 })
10 }
11/>

Range mode and dropdown year navigation

For check-in/check-out and report-window inputs, use the DatePicker.Range sub-component — it is attached to the root via Object.assign in the source, so one import gives you both modes. Range mode holds a { from, to } value; import the DateRange type to type your state. For single-date pickers where users jump across years — a date of birth field, say — pass variant="dropdown" to swap the calendar caption for month and year <select> menus, and bound them with fromYear and toYear. The picker also forwards a locale prop straight to react-day-picker, so importing a locale from react-day-picker/locale re-labels the grid. See Drivn vs shadcn/ui Date Picker for how this compares.

1'use client'
2import { useState } from 'react'
3import {
4 DatePicker,
5 type DateRange,
6} from '@/components/ui/date-picker'
7
8export function StayRange() {
9 const [range, setRange] = useState<DateRange | undefined>()
10 return (
11 <DatePicker.Range
12 selected={range}
13 onSelect={setRange}
14 />
15 )
16}
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 Date Picker itself is a client component — it is marked 'use client' because it tracks open state and the selected date with hooks, and composes a react-day-picker calendar. Your surrounding page and layout can still render on the server. The standard pattern is a small client component holding the picker plus its useState, rendered as an island inside an otherwise server-rendered route. Next.js inserts the client boundary at the import automatically.

No. The Date Picker manages its own open state and positions the calendar in an absolutely positioned panel below the trigger. Two effects in the source handle dismissal — one closes the panel on a mousedown outside the component, the other on the Escape key. There is no Radix Popover, no floating-ui, and no third state machine; the entire behavior lives in the one file the CLI writes to your repo.

Use the DatePicker.Range sub-component exposed through the same import. It holds a selected value shaped as { from, to } and calls onSelect with that shape. Import the DateRange type alongside the component to type your state. The trigger shows both endpoints joined with an en dash once a full range is chosen, formatted by the same formatDate function the single-date picker uses.

Yes. Pass a formatDate function that takes the selected Date and returns a string. By default the picker formats with toLocaleDateString as a short month, day, and year. Your function only changes the label in the trigger button, not the Date value stored in state, and it applies to both endpoints in range mode. Combine it with placeholder to control the empty-state text.