Light

Calendar

A date field component that allows users to enter and edit date.

Spec · from metadata

When to use

  • Selecting a single date or date range
  • Inline calendar display for scheduling UI
  • Date picker inside a Popover for form fields

When not to use

  • For time-only selection — use a time input or TimePicker
  • For selecting relative dates (e.g., 'last 7 days') — use a preset dropdown with date range
  • For full calendar/event views — use a dedicated calendar/scheduler library

Anti-patterns

Avoid<Calendar />
Prefer<Calendar mode="single" selected={date} onSelect={setDate} />

Always specify mode and selection handlers; an uncontrolled calendar has no way to report selection

Avoid<Calendar mode="range" selected={startDate} />
Prefer<Calendar mode="range" selected={{ from: startDate, to: endDate }} onSelect={setDateRange} />

Range mode expects a DateRange object ({ from, to }), not a single Date

Accessibility

  • Required ARIAaria-disabled for disabled days
  • Min touch target32px
  • Screen readerNavigation buttons include previous/next month labels; day buttons announce their date via content

Token bindings

TokenCategoryUsage
bg-primarycolorSelected day and range start/end background
text-primary-foregroundcolorSelected day text color
bg-accentcolorToday highlight and range middle background
text-muted-foregroundcolorOutside days and weekday header text
--cell-size (--spacing(8))spacingDay cell dimensions (width and height)

Import

import { Calendar } from "@timelycare/helix-ui"

Props

interface CalendarProps {
  mode?: "single" | "range" | "multiple"
  selected?: Date | DateRange | Date[]
  onSelect?: (date: Date | DateRange | Date[] | undefined) => void
  disabled?: Date[] | ((date: Date) => boolean)
  fromDate?: Date
  toDate?: Date
  defaultMonth?: Date
  numberOfMonths?: number
  showOutsideDays?: boolean
  weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6
  captionLayout?: "buttons" | "dropdown" | "dropdown-buttons"
  fromYear?: number
  toYear?: number
  className?: string
}

interface DateRange {
  from: Date
  to?: Date
}

Variants

VariantUse ForProps
singleSimple date selectionDefault mode
rangeDate range selection (bookings, filters)mode="range" numberOfMonths={2}
multipleSelecting multiple individual datesmode="multiple"

Display Variants

TypeUse ForProps
BasicStandard date pickerDefault
Month/Year SelectorQuick jump to distant dates, birth datescaptionLayout="dropdown-buttons"
Multi-MonthRange selection, quarter viewsnumberOfMonths={2} or numberOfMonths={3}
MobileTouch-friendly targetsCustom day cell sizing

Choosing a Variant

  • Single: Default for most date inputs
  • Range: Booking systems, date filters, vacation planning
  • Month/Year Dropdown: Birth date selection, historical dates

Sizes

SizeDay CellUse For
Defaultsize-8 (32×32px)Standard desktop
Mobilesize-12 (48×48px)Touch-friendly mobile

Styling

Day Cell Button

  • Size: size-8 (32×32px)
  • Border radius: rounded-md (8px)
  • Typography: Adelle Sans Regular, text-sm (14px), leading-5 (20px)

Day Header (Weekday labels)

  • Width: w-8 (32px)
  • Height: 21px
  • Typography: Adelle Sans Regular, text-xs (12px), leading-4 (16px)
  • Color: text-muted-foreground

Month Caption

  • Typography: Adelle Sans Semibold, text-sm (14px), leading-5 (20px)
  • Color: text-foreground

Navigation Buttons

  • Size: size-8 (32×32px)
  • Border radius: rounded-md (8px)
  • Icon: size-4 (16px), text-primary

Layout

  • Row gap: gap-2 (8px)
  • Container: shadow-sm, rounded-md

States

StateBackgroundText ColorBorder
Defaulttransparenttext-foregroundnone
Outsidetransparenttext-muted-foregroundnone
Todaybg-accenttext-accent-foregroundnone
Hoverbg-accenttext-accent-foregroundnone
Selectedbg-primarytext-primary-foregroundnone
Selected + Focusbg-primarytext-primary-foregroundring-3 ring-ring
Range (middle)bg-accenttext-accent-foregroundnone
Range Startbg-primarytext-primary-foregroundleft: rounded-md, right: rounded-none
Range Endbg-primarytext-primary-foregroundleft: rounded-none, right: rounded-md
Disabledtransparentopacity-50none
Unavailabletransparentline-through opacity-50none

Icons

Size: size-4 (16×16px) Color: text-primary

IconUse For
ChevronLeftPrevious month navigation
ChevronRightNext month navigation
ChevronDownMonth/year dropdown indicator
import { ChevronLeft, ChevronRight, ChevronDown } from "lucide-react"

Common Patterns

Basic Date Picker

import { Calendar } from "@timelycare/helix-ui"
import { useState } from "react"

const [date, setDate] = useState<Date | undefined>()

<Calendar
  mode="single"
  selected={date}
  onSelect={setDate}
/>

Date Range Selection

import { Calendar } from "@timelycare/helix-ui"
import { DateRange } from "react-day-picker"
import { useState } from "react"

const [dateRange, setDateRange] = useState<DateRange | undefined>()

<Calendar
  mode="range"
  selected={dateRange}
  onSelect={setDateRange}
  numberOfMonths={2}
/>

With Month/Year Dropdowns

<Calendar
  mode="single"
  captionLayout="dropdown-buttons"
  fromYear={1900}
  toYear={2100}
/>

With Disabled Dates

<Calendar
  mode="single"
  selected={date}
  onSelect={setDate}
  disabled={(date) =>
    date > new Date() || date < new Date("1900-01-01")
  }
/>

Mobile-Friendly Calendar

<Calendar
  mode="single"
  selected={date}
  onSelect={setDate}
  classNames={{
    day: "size-12 text-base",
    head_cell: "w-12",
  }}
/>

Three-Month Range Calendar

<Calendar
  mode="range"
  selected={dateRange}
  onSelect={setDateRange}
  numberOfMonths={3}
/>

Accessibility

  • Arrow keys navigate between days
  • Enter/Space to select
  • Month navigation via buttons
  • Screen readers announce date selections
  • Focus ring visible on keyboard navigation: ring-3 ring-ring

Gotchas

ProblemSolution
Range selection only shows one monthAdd numberOfMonths={2}
Past dates still selectableAdd disabled function or fromDate
Need quick year navigationUse captionLayout="dropdown-buttons" with fromYear/toYear
Mobile touch targets too smallUse classNames to increase day cell size to size-12
Today's date not highlightedEnsure today class applies bg-accent text-accent-foreground
Range endpoints look wrongCheck that rounded corners apply: start gets left radius, end gets right radius

See Also


Last updated: February 9, 2026