Westy Design Systemv0.2.7
OverviewComponentsExamplesStorybook

Foundations

  • Typography
  • Colors
  • Spacing
  • Radius
  • Shadows
  • Icons

Components

  • Accordion
  • Alert
  • Alert Dialog
  • Aspect Ratio
  • Avatar
  • Badge
  • Breadcrumb
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Data Table
  • Date Picker
  • Dialog
  • Direction
  • Drawer
  • Dropdown Menu
  • Empty
  • Field
  • Hover Card
  • Input
  • Input Group
  • Input OTP
  • Item
  • Kbd
  • Label
  • Menubar
  • Native Select
  • Navigation Menu
  • Pagination
  • Popover
  • Progress
  • Questionnaire
  • Radio Group
  • Resizable
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Skeleton
  • Slider
  • Sonner
  • Spinner
  • Switch
  • Table
  • Tabs
  • Textarea
  • Toggle
  • Toggle Group
  • Tooltip

Chat

  • Attachment
  • Bubble
  • Marker
  • Message
  • Message Scroller

Components

Date Picker

A date picker with range and presets. A pattern — not a @workspace/ui component — composing Popover and Calendar.

Playground

—mode
—captionLayout
—numberOfMonths
—setOpen(false)
—open
Setup
There is no 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 state
Usage
Hold the date in state and render it on the trigger — the button shows a placeholder until onSelect 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>
  )
}
Range
Switch the calendar to 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>
Date of birth
For far-away dates, control the popover's 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>
Input
Pair the calendar with an InputGroup so the date can also be typed: valid input moves the calendar via 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>
Date and time
Lay a date picker and a native 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>
Natural language
Parse free-form input with 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.
Your post will be published on September 26, 2026.
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>
RTL
Pass 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

    bg-background
  • --popover

    Popover

    bg-popover
  • --popover-foreground

    Popover Foreground

    text-popover-foreground
  • --primary

    Primary

    bg-primary
  • --primary-foreground

    Primary Foreground

    text-primary-foreground
  • --muted

    Muted

    hover:bg-muted
  • --foreground

    Foreground

    text-foreground
  • --muted-foreground

    Muted Foreground

    text-muted-foreground
  • --accent

    Accent

    bg-accent
  • --accent-foreground

    Accent Foreground

    text-accent-foreground
  • --border

    Border

    border-border
  • --input

    Input

    border-inputdark:bg-input/30
  • --ring

    Ring

    focus-visible:border-ringfocus-visible:ring-ring/50
  • --radius-md

    Radius 0.8× (8px)

    rounded-mdrounded-(--cell-radius)
  • --text-sm

    Text Small (14px)

    text-sm
  • --font-weight-medium

    Font weight Medium (500)

    font-medium
  • --spacing

    Spacing unit (4px, via --cell-size)

    size-(--cell-size)w-56p-3gap-1.5