Skip to content
Drivn
5 min read

Drivn vs shadcn/ui — Date Picker Component Compared

Side-by-side comparison of Drivn DatePicker vs the shadcn/ui Popover and Calendar recipe — single import, range mode, format prop, and locale wiring.

Reach for a date input in shadcn/ui and you do not find a component — you find a recipe. The shadcn docs publish the date picker as a block: roughly fifty lines that wire Popover, Button, Calendar, and a date-fns format call together inside your own form file. Every form that needs a date gets its own copy of that plumbing, and every copy drifts a little from the last one. Drivn ships DatePicker as an installed component, so the trigger button, popover open state, outside-click handling, and Escape dismissal already exist behind one import.

Under the hood the two are closer than the API suggests. Both wrap react-day-picker, so the calendar grid, the matcher API, and locale objects behave identically. The divergence is entirely at the call site. shadcn asks you to hold open state, format the selected day inline, and nest the Calendar inside a PopoverContent. Drivn asks for <DatePicker selected={date} onSelect={setDate} /> and keeps the composition inside the component file you own.

Everything below compares the two on the details that actually decide which one you pick: API shape, range selection, year-dropdown captions, custom formatting, and locale wiring. The shadcn code in each section is the published block recipe, not an invented wrapper. If you currently paste that recipe into every form, Drivn's Calendar and DatePicker collapse it into props you set once.

Side-by-side comparison

FeatureDrivnshadcn/ui
Underlying libraryreact-day-pickerreact-day-picker
Component shapeSingle importBlock recipe (5 imports)
Trigger stylingBuilt-inCompose with Button + Popover
Range mode<DatePicker.Range />Duplicate recipe with mode="range"
Year dropdown shortcutfromYear / toYearManual startMonth / endMonth
Custom formatformatDate propformat() from date-fns inline
Outside-click + EscapeBuilt-inInherited from Popover
Locale supportreact-day-picker/localereact-day-picker/locale + date-fns/locale
LicenseMITMIT
Copy-paste install

API side-by-side

shadcn/ui's date picker is a block recipe — copy these five components into your form file and wire them yourself. The Popover handles open state, the Button is the trigger, the Calendar lives inside PopoverContent, and you call format(date, 'PPP') from date-fns to render the selected day inside the button. Drivn collapses that into a single DatePicker import. The trigger is a plain <button> styled to match the Input component, with a CalendarDays icon on the left and the formatted date in the middle. Open state lives inside the component; outside-click and Escape dismiss the popover automatically.

Both approaches render the same calendar grid because they both wrap react-day-picker. The difference is what you write per form — with Drivn the call site stays one element and the format function is a prop, not an inline import.

1// shadcn/ui — block recipe
2import { useState } from 'react'
3import { format } from 'date-fns'
4import { CalendarIcon } from 'lucide-react'
5import { Button } from '@/components/ui/button'
6import { Calendar } from '@/components/ui/calendar'
7import {
8 Popover,
9 PopoverContent,
10 PopoverTrigger,
11} from '@/components/ui/popover'
12import { cn } from '@/lib/utils'
13
14export function DatePicker() {
15 const [date, setDate] = useState<Date>()
16 return (
17 <Popover>
18 <PopoverTrigger asChild>
19 <Button
20 variant="outline"
21 className={cn(
22 'w-[240px] justify-start text-left font-normal',
23 !date && 'text-muted-foreground'
24 )}
25 >
26 <CalendarIcon />
27 {date ? format(date, 'PPP') : <span>Pick a date</span>}
28 </Button>
29 </PopoverTrigger>
30 <PopoverContent className="w-auto p-0" align="start">
31 <Calendar
32 mode="single"
33 selected={date}
34 onSelect={setDate}
35 />
36 </PopoverContent>
37 </Popover>
38 )
39}
40
41// Drivn — single import, trigger built-in
42import { useState } from 'react'
43import { DatePicker } from '@/components/ui/date-picker'
44
45export default function Page() {
46 const [date, setDate] = useState<Date | undefined>()
47 return (
48 <DatePicker selected={date} onSelect={setDate} />
49 )
50}

Range selection with DatePicker.Range

The range variant mounts on the same DatePicker import as DatePicker.Range. The selected prop narrows to DateRange | undefined from react-day-picker, and the popover stays open until both from and to are set — clicking the same day twice does not collapse the range. The trigger label formats as Apr 1 – Apr 7 using the shared formatter, with the en dash inserted by formatRangeLabel inside the component.

shadcn's published recipe for range selection is a separate block file that copies the single-date recipe and changes mode from single to range. You end up with two near-identical files in the components directory — date-picker.tsx and date-picker-range.tsx — each duplicating the same Popover-Button-Calendar plumbing. Drivn keeps both behind a single dot-notation API, and the Calendar source the two share lives once in your repo.

1import { useState } from 'react'
2import {
3 DatePicker,
4 type DateRange,
5} from '@/components/ui/date-picker'
6
7export default function Page() {
8 const [range, setRange] = useState<DateRange | undefined>()
9
10 return (
11 <DatePicker.Range
12 selected={range}
13 onSelect={setRange}
14 />
15 )
16}

Year dropdown and custom formatting

Drivn's DatePicker forwards a variant prop straight through to the Calendar it wraps, and the variant map has exactly three entries: default, weekNumbers, and dropdown. Choosing dropdown sets react-day-picker's captionLayout to 'dropdown' and derives the month range from two plain numbers — internally startMonth: new Date(fromYear, 0) and endMonth: new Date(toYear, 11). The defaults are currentYear - 20 and currentYear + 20, which already covers most scheduling forms with no configuration at all. The native <select> elements render at opacity-0 layered over the visible caption label, so the control reads as text but opens the platform picker on tap.

shadcn does not expose that shortcut. Switching its recipe to dropdown captions means editing the copied file: pass captionLayout="dropdown", hand-build startMonth={new Date(1950, 0)} and endMonth={new Date(2030, 11)}, then widen the trigger to fit the taller caption row. Formatting splits the same way — Drivn takes a formatDate function prop that feeds both the single-date trigger label and the range label, while shadcn expects you to import format from date-fns and call it inline inside the Button. The Date Picker docs list every prop.

1// Drivn — variant prop + formatDate prop
2<DatePicker
3 variant="dropdown"
4 fromYear={1950}
5 toYear={2030}
6 selected={date}
7 onSelect={setDate}
8 formatDate={(d) =>
9 d.toLocaleDateString('en-GB', {
10 day: '2-digit',
11 month: '2-digit',
12 year: 'numeric',
13 })
14 }
15/>
16
17// shadcn — edit the recipe to pass captionLayout + format inline
18<Calendar
19 mode="single"
20 captionLayout="dropdown"
21 startMonth={new Date(1950, 0)}
22 endMonth={new Date(2030, 11)}
23 selected={date}
24 onSelect={setDate}
25/>
26{date ? format(date, 'dd/MM/yyyy') : 'Pick a date'}

Locale and internationalization

Both libraries pass locale objects from react-day-picker/locale straight through to the underlying DayPicker. Drivn forwards the locale prop through DatePicker into Calendar so the trigger label, weekday headers, month names, and first-day-of-week all update in one prop. Import cs from react-day-picker/locale and pass it in — no provider needed.

shadcn's recipe accepts the locale on the inner <Calendar locale={...} /> only — the trigger Button label still calls format(date, 'PPP') from date-fns, so localizing the trigger means swapping date-fns to format(date, 'PPP', { locale: cs }) from date-fns/locale. Drivn passes one prop and the trigger label, calendar caption, and weekday row update together, because the same formatDate and locale props feed both surfaces. The Calendar comparison covers every other locale trade-off they share.

1import { useState } from 'react'
2import { DatePicker } from '@/components/ui/date-picker'
3import { cs } from 'react-day-picker/locale'
4
5export default function Page() {
6 const [date, setDate] = useState<Date | undefined>()
7
8 return (
9 <DatePicker
10 locale={cs}
11 selected={date}
12 onSelect={setDate}
13 />
14 )
15}
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

Not as a component — shadcn publishes a date picker as a block recipe in its documentation. You copy a roughly fifty-line file that imports Popover, Button, Calendar, and date-fns, and you paste it into your project as components/date-picker.tsx. Drivn ships the same composition as a built-in component you install with npx drivn add date-picker, so the call site stays one import.

Use DatePicker.Range. It mounts on the same import as the default DatePicker via dot notation, accepts a DateRange | undefined selected prop, and reports back when the user picks both from and to dates. The popover stays open between the two clicks and dismisses automatically once the range completes — no second component to install or import.

Yes — pass placeholder, formatDate, and className for the most common changes. For larger structural edits, the trigger lives in your components/ui/date-picker.tsx file after install, so any styling change is a local edit. The styles.trigger cn() call defines border, padding, and focus styling — change one line and rebuild.

In Drivn, no — pass fromYear and toYear as plain numbers and the component converts them into the startMonth and endMonth Date objects react-day-picker needs. The defaults are today's year plus and minus twenty, suitable for typical near-future scheduling. shadcn's recipe leaves both as Date props you assemble at every call site.

Part of the DatePicker. The component manages its own open state with useState(false), listens for outside clicks via a mousedown listener on the document, and dismisses on Escape via a keyboard listener. There is no Popover primitive to install. If you already use the Drivn Popover elsewhere, the DatePicker is independent of it.