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 2 npx 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' 2 import { useState } from 'react' 3 import { DatePicker } from '@/components/ui/date-picker' 4 5 export 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.
1 const 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 />
Install Drivn in one command
Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.
npx drivn@latest createRequires Node 18+. Works with npm, pnpm, and yarn.
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.

