Components
A date picker with range and presets. A pattern — not a @workspace/ui component — composing Popover and Calendar.
Playground
DatePicker root component: a trigger button opens a Calendar inside a Popover, and your own state holds the selected date. Both building blocks ship in @workspace/ui — nothing to install.Popover
├── PopoverTrigger // Button showing the current value
└── PopoverContent // w-auto p-0 — the calendar brings its own padding
└── Calendar // selected + onSelect from your stateonSelect stores a value. Strip the popover padding with w-auto p-0 and pass defaultMonth so reopening lands on the selected month."use client"
import * as React from "react"
import { CalendarBlankIcon } from "@phosphor-icons/react"
import { Button } from "@workspace/ui/components/button"
import { Calendar } from "@workspace/ui/components/calendar"
import { Field, FieldLabel } from "@workspace/ui/components/field"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@workspace/ui/components/popover"
export function DatePicker() {
const [date, setDate] = React.useState<Date | undefined>(undefined)
return (
<Field className="w-56">
<FieldLabel htmlFor="date-picker">Date</FieldLabel>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
id="date-picker"
className="justify-start font-normal"
>
<CalendarBlankIcon className="text-muted-foreground" />
{date ? (
date.toLocaleDateString("en-US", { dateStyle: "long" })
) : (
<span className="text-muted-foreground">Pick a date</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={date}
onSelect={setDate}
defaultMonth={date}
/>
</PopoverContent>
</Popover>
</Field>
)
}mode="range" and hold a DateRange from react-day-picker instead of a single date. The trigger formats from and to as they arrive, and numberOfMonths={2} gives the selection room to breathe.import { type DateRange } from "react-day-picker"
const [range, setRange] = React.useState<DateRange | undefined>({
from: new Date(2026, 7, 9),
to: new Date(2026, 7, 26),
})
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="justify-start font-normal">
<CalendarBlankIcon className="text-muted-foreground" />
{range?.from ? (
range.to ? (
`${formatDate(range.from)} – ${formatDate(range.to)}`
) : (
formatDate(range.from)
)
) : (
<span className="text-muted-foreground">Pick a date range</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="range"
selected={range}
onSelect={setRange}
defaultMonth={range?.from}
numberOfMonths={2}
/>
</PopoverContent>
</Popover>open state so a selection can close it, and switch the caption to captionLayout="dropdown" with startMonth / endMonth bounding the year list.const [open, setOpen] = React.useState(false)
const [date, setDate] = React.useState<Date | undefined>(undefined)
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="outline" className="justify-between font-normal">
{date ? (
date.toLocaleDateString("en-US", { dateStyle: "long" })
) : (
<span className="text-muted-foreground">Select date</span>
)}
<CaretDownIcon className="text-muted-foreground" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto overflow-hidden p-0" align="start">
<Calendar
mode="single"
selected={date}
defaultMonth={date ?? new Date(1996, 5, 1)}
captionLayout="dropdown"
startMonth={new Date(1940, 0)}
endMonth={new Date(2026, 11)}
onSelect={(selected) => {
setDate(selected)
setOpen(false)
}}
/>
</PopoverContent>
</Popover>month / onMonthChange, picking a day writes back into the field, and ArrowDown opens the popover from the keyboard.function formatDate(date: Date | undefined) {
if (!date) {
return ""
}
return date.toLocaleDateString("en-US", {
day: "2-digit",
month: "long",
year: "numeric",
})
}
function isValidDate(date: Date | undefined) {
if (!date) {
return false
}
return !isNaN(date.getTime())
}
const [open, setOpen] = React.useState(false)
const [date, setDate] = React.useState<Date | undefined>(new Date(2026, 7, 1))
const [month, setMonth] = React.useState<Date | undefined>(date)
const [value, setValue] = React.useState(formatDate(date))
<InputGroup>
<InputGroupInput
value={value}
placeholder="August 01, 2026"
onChange={(event) => {
const parsed = new Date(event.target.value)
setValue(event.target.value)
if (isValidDate(parsed)) {
setDate(parsed)
setMonth(parsed)
}
}}
onKeyDown={(event) => {
if (event.key === "ArrowDown") {
event.preventDefault()
setOpen(true)
}
}}
/>
<InputGroupAddon align="inline-end">
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<InputGroupButton
variant="ghost"
size="icon-xs"
aria-label="Select date"
>
<CalendarBlankIcon />
</InputGroupButton>
</PopoverTrigger>
<PopoverContent
className="w-auto overflow-hidden p-0"
align="end"
alignOffset={-8}
sideOffset={10}
>
<Calendar
mode="single"
selected={date}
month={month}
onMonthChange={setMonth}
onSelect={(selected) => {
setDate(selected)
setValue(formatDate(selected))
setOpen(false)
}}
/>
</PopoverContent>
</Popover>
</InputGroupAddon>
</InputGroup>type="time"Input side by side in a horizontal FieldGroup. The webkit picker indicator is hidden so the time field matches the design system's input styling.<FieldGroup className="max-w-xs flex-row">
<Field>
<FieldLabel htmlFor="date-picker">Date</FieldLabel>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
id="date-picker"
className="w-36 justify-between font-normal"
>
{date ? (
date.toLocaleDateString("en-US", { dateStyle: "medium" })
) : (
<span className="text-muted-foreground">Select date</span>
)}
<CaretDownIcon className="text-muted-foreground" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto overflow-hidden p-0" align="start">
<Calendar
mode="single"
selected={date}
captionLayout="dropdown"
defaultMonth={date}
onSelect={(selected) => {
setDate(selected)
setOpen(false)
}}
/>
</PopoverContent>
</Popover>
</Field>
<Field className="w-32">
<FieldLabel htmlFor="time-picker">Time</FieldLabel>
<Input
type="time"
id="time-picker"
step="1"
defaultValue="10:30:00"
className="appearance-none bg-background [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
/>
</Field>
</FieldGroup>chrono-node— "tomorrow", "in 2 days" or "next friday" all resolve to a date as you type, and the calendar mirrors whatever was understood. Add the dependency to the consuming app: yarn add chrono-node.import { parseDate } from "chrono-node"
const [open, setOpen] = React.useState(false)
const [value, setValue] = React.useState("In 2 days")
const [date, setDate] = React.useState<Date | undefined>(
() => parseDate(value) ?? undefined
)
<Field className="max-w-xs">
<FieldLabel htmlFor="schedule-date">Schedule date</FieldLabel>
<InputGroup>
<InputGroupInput
id="schedule-date"
value={value}
placeholder="Tomorrow or next week"
onChange={(event) => {
setValue(event.target.value)
const parsed = parseDate(event.target.value)
if (parsed) {
setDate(parsed)
}
}}
onKeyDown={(event) => {
if (event.key === "ArrowDown") {
event.preventDefault()
setOpen(true)
}
}}
/>
<InputGroupAddon align="inline-end">
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<InputGroupButton
variant="ghost"
size="icon-xs"
aria-label="Select date"
>
<CalendarBlankIcon />
</InputGroupButton>
</PopoverTrigger>
<PopoverContent
className="w-auto overflow-hidden p-0"
align="end"
sideOffset={8}
>
<Calendar
mode="single"
selected={date}
captionLayout="dropdown"
defaultMonth={date}
onSelect={(selected) => {
setDate(selected)
setValue(formatDate(selected))
setOpen(false)
}}
/>
</PopoverContent>
</Popover>
</InputGroupAddon>
</InputGroup>
<div className="px-1 text-sm text-muted-foreground">
Your post will be published on{" "}
<span className="font-medium">{formatDate(date)}</span>.
</div>
</Field>dir="rtl" to the trigger, content and calendar, plus a locale from react-day-picker/locale. The grid, nav chevrons and range styling mirror via logical properties, and the trigger renders the value with a localized formatter.import { ar } from "react-day-picker/locale"
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className="w-56 justify-between font-normal"
dir="rtl"
>
{date ? (
date.toLocaleDateString("ar", { dateStyle: "long" })
) : (
<span className="text-muted-foreground">اختر تاريخًا</span>
)}
<CaretDownIcon className="text-muted-foreground" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start" dir="rtl">
<Calendar
mode="single"
selected={date}
onSelect={setDate}
defaultMonth={date}
dir="rtl"
locale={ar}
/>
</PopoverContent>
</Popover>Tokens used
17 design tokens consumed by the Date Picker pattern.
--background
Background
--popover
Popover
--popover-foreground
Popover Foreground
--primary
Primary
--primary-foreground
Primary Foreground
--muted
Muted
--foreground
Foreground
--muted-foreground
Muted Foreground
--accent
Accent
--accent-foreground
Accent Foreground
--border
Border
--input
Input
--ring
Ring
--radius-md
Radius 0.8× (8px)
--text-sm
Text Small (14px)
--font-weight-medium
Font weight Medium (500)
--spacing
Spacing unit (4px, via --cell-size)