Components · Data display & feedback
Date picker
A month grid for picking a date — 32px day nodes on the popover surface. The selected day takes a primary fill, today wears a hairline-strong ring, and days outside the current month dim back. Pair it with an input to form the full date-picker field.
Preview
June 2026
S
M
T
W
T
F
S
Build with the skill
No package to install — hand this to your AI to generate or migrate the component in your stack.
Apply the Zetta "Date picker" component to this project using the zetta-design-md skill.
WHEN TO USE
- Migrating or fixing existing code (the common case): audit the current implementation against the `date-picker` spec (variants: day, day-selected, day-today, day-disabled, day-outside) and CORRECT every deviation — replace off-spec values, don't only add new ones.
- New component: build it to the `date-picker` spec in this project's stack.
AUDIT & FIX (when the component already exists)
Compare it to the skill's spec and fix, don't just append: hardcoded colors/hex, off-scale radius, wrong fonts, dark/navy fills, shadows used in place of borders (or missing borders), brand lime used as a CTA or interaction (must be primary indigo), and any missing or wrong hover / focus / active / disabled / invalid state or missing 2px focus ring. Replace each with the correct Zetta token.
ALWAYS
- Pull the authoritative spec and tokens from the skill — never guess or hardcode values.
- Use this project's own component conventions and framework idioms.
- Honor the Zetta guardrails (brand = identity only, primary = interaction, shadows for overlays) and verify in Light, Dark, and Accessibility. Anatomy & rules
[ 2024-01-15 📅 ] ← trigger input (standard input token)
┌───────────────────┐
│ < January 2024 > │ ← calendar-header
│ Mo Tu We Th Fr Sa │ ← calendar-weekday-label
│ 1 2 3 4 5 6 │
│ 7 8 9 10 11 12 │ ← calendar-day (selected: primary fill)
│ ... │
└───────────────────┘
Parts:
- Trigger input — standard
inputtoken; trailingcalendar_todayicon (16px,{colors.muted}) - Popover —
calendar-container:{colors.popover}surface, 24px padding,{elevation.shadow.overlay} - Header — prev/next arrows (
button-ghost), month+year label (clickable → drill-down) - Weekday labels — abbreviated (Mon–Sun), 12px / 500 /
{colors.muted} - Day nodes — 32×32px; hover, selected, today, disabled, outside-month states
| State | Input border | Fill | Notes |
|---|---|---|---|
| Default (empty) | {colors.border-strong} |
{colors.surface-card} |
Placeholder visible |
| Default (has value) | {colors.border-strong} |
{colors.surface-card} |
Date value shown |
| Hover | {colors.muted} |
{colors.surface-card} |
— |
| Focus | {colors.primary} + glow |
{colors.surface-card} |
Calendar opens |
| Open | {colors.primary} + glow |
{colors.surface-card} |
Calendar panel visible |
| Selected date | — | {colors.primary} on calendar day |
Day fills with primary |
| Today (unselected) | — | {colors.primary-bg} on calendar day |
Subtle today indicator |
| Disabled | {colors.disabled-border} |
{colors.disabled-bg} |
No calendar opens |
| Error | {colors.danger} + glow |
{colors.surface-card} |
Validation message shown |
| Read-only | {colors.hairline} |
{colors.surface-muted} |
No calendar opens |
When to use
- Any form field that requires a single date input.
When not to use
- When selecting a date range — use Date Range Picker.
- When selecting both date and time — use Date Time Picker.
Do
- Always pair with a text field so users can type dates directly as an alternative to the calendar.
- Disable past dates when a future date is required (or vice versa) using
min/max.
Don't
- Don't embed the calendar grid inline in a form — use the popover trigger pattern.
- Placeholder format matches the expected input format: "DD/MM/YYYY" or "MM/DD/YYYY".
- Error messages: be specific. "Date must be after today." not "Invalid date."
- Label: always visible — never use placeholder as label.
- Date range context: if part of a range, label as "Start date" / "End date".
Keyboard
| Key | Behavior |
|---|---|
Tab |
Move focus to the date input |
Enter / Space |
Open / close the calendar panel |
Arrow keys |
Navigate days in the calendar grid |
Enter |
Select the focused date, closing the calendar |
Page Up / Page Down |
Navigate months |
Home / End |
First / last day of the current week |
Escape |
Close the calendar without selecting |
Accessibility
- ARIA: trigger uses
aria-haspopup="dialog"andaria-expanded. Calendar usesrole="dialog"witharia-label="Choose date". - Day nodes:
role="gridcell",aria-selectedfor selected date,aria-disabledfor disabled dates. - Accessibility theme: day nodes require 44×44px override — pending per
zetta-accessibility-v1_6.md.
Spec
Values and token references resolved straight from the Zetta spec.
calendar-container
- backgroundColor
- {colors.popover}
- textColor
- {colors.popover-foreground}
- borderColor
- {colors.hairline}
- borderWidth
- 1px
- borderRadius
- {rounded.base}
- shadow
- {elevation.shadow.overlay}
- padding
- {spacing.page}
- display
- grid
- gap
- {spacing.sm}
- headerHeight
- 32px
- dayNodeSize
- 32px
- dayNodeRadius
- {rounded.base}
- dayNodeFontFamily
- Geist
- dayNodeFontSize
- 14px
calendar-header
- height
- 32px
- display
- flex
- alignItems
- center
- justifyContent
- space-between
calendar-day
- textColor
- {colors.ink}
- fontWeight
- 400
- display
- flex
- alignItems
- center
- justifyContent
- center
- cursor
- pointer
calendar-day-hover
- backgroundColor
- {colors.primary-bg}
- textColor
- {colors.primary}
- borderRadius
- {rounded.base}
calendar-day-focus
- outline
- 2px solid {colors.border-focus}
- outlineOffset
- 2px
- borderRadius
- {rounded.base}
calendar-day-selected
- backgroundColor
- {colors.primary}
- textColor
- {colors.primary-text}
- fontWeight
- 600
- borderRadius
- {rounded.base}
calendar-day-today
- borderColor
- {colors.border-strong}
- borderWidth
- 1px
- borderRadius
- {rounded.base}
calendar-day-disabled
- textColor
- {colors.disabled-text}
- cursor
- not-allowed
calendar-day-outside
- textColor
- {colors.disabled-text}
- opacity
- 0.4
- cursor
- default
calendar-weekday-label
- textColor
- {colors.muted}
- fontFamily
- Geist
- fontSize
- 12px
- fontWeight
- 500
- textAlign
- center
calendar-month-grid
- display
- grid
- gridTemplateColumns
- repeat(3, 1fr)
- gap
- {spacing.sm}
- padding
- {spacing.md}
calendar-month-cell
- height
- 40px
- borderRadius
- {rounded.base}
- fontFamily
- Geist
- fontSize
- 14px
- fontWeight
- 400
- textColor
- {colors.ink}
- display
- flex
- alignItems
- center
- justifyContent
- center
- cursor
- pointer
calendar-month-cell-hover
- backgroundColor
- {colors.primary-bg}
- textColor
- {colors.primary}
calendar-month-cell-selected
- backgroundColor
- {colors.primary}
- textColor
- {colors.primary-text}
- fontWeight
- 600
calendar-month-cell-current
- borderColor
- {colors.border-strong}
- borderWidth
- 1px
calendar-month-cell-focus
- outline
- 2px solid {colors.border-focus}
- outlineOffset
- 2px
calendar-year-grid
- display
- grid
- gridTemplateColumns
- repeat(3, 1fr)
- gap
- {spacing.sm}
- padding
- {spacing.md}
- maxHeight
- 200px
- overflowY
- auto
calendar-year-cell
- height
- 40px
- borderRadius
- {rounded.base}
- fontFamily
- Geist
- fontSize
- 14px
- fontWeight
- 400
- textColor
- {colors.ink}
- display
- flex
- alignItems
- center
- justifyContent
- center
- cursor
- pointer
calendar-year-cell-hover
- backgroundColor
- {colors.primary-bg}
- textColor
- {colors.primary}
calendar-year-cell-selected
- backgroundColor
- {colors.primary}
- textColor
- {colors.primary-text}
- fontWeight
- 600
calendar-year-cell-current
- borderColor
- {colors.border-strong}
- borderWidth
- 1px
calendar-year-cell-focus
- outline
- 2px solid {colors.border-focus}
- outlineOffset
- 2px
Known gaps
- Calendar trigger input — uses standard
inputtoken; no separate trigger token needed. - Invalid range (passes through disabled day) error state — not yet specified.
- Accessibility theme 44×44px day node override — pending.
- Month picker drill-down animation spec — not yet specified.