ComponentsDate Picker
Date Picker
componentA date field whose popover calendar is pure kit — themed, not OS chrome.
Due date
Single-date picker + time input — popover for the day, typed cells for the time.
Remind me at
Composes: Button
Best practices
Do
- Pick the variant for the job: the month grid for picking, .calendar-week for scheduling, the range states for periods.
- Keep today marked even while another day is selected — the two states are independent.
- Fix the grid at six rows so a picker popover never changes height between months.
Don't
- Don't rebuild a date picker from a table — the calendar owns the cell states and keyboard flow.
- Don't use the bare month grid for read-only event display; that is the week/agenda pattern.
Recipe CSS
This is the exact CSS your kit ships for Date Picker — token-driven, so it re-themes with every knob. Get it (and the rest) from the configurator's Use kit panel.
/* === Calendar ===
Single-cell selection + range pattern (shadcn / react-day-picker style).
Range cells fill with primary-soft and lose corner radius on connection
sides so the highlight reads as continuous. */
.calendar {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: var(--k-s-2) 0;
font-size: var(--k-type-eyebrow);
}
.calendar__head {
text-align: center;
font-size: var(--k-type-caption);
color: var(--k-fg-faint);
padding: var(--k-s-4) 0;
font-weight: var(--k-weight-medium);
}
/* Calendar cells follow Stature: compact = 28px (Linear/Notion slim picker),
* balanced = 32px (shadcn default), bold = 40px (iOS-style tap target).
* Aspect-ratio keeps cells square — min-height enforces stature minimum so
* compact-stature really feels compact even in a wide container. */
.calendar__cell {
aspect-ratio: 1;
min-height: var(--k-cal-cell, 32px);
width: 100%;
display: grid;
place-items: center;
border-radius: var(--k-radius-md);
color: var(--k-fg);
/* Numeric controls share one digit treatment: medium weight (= system UI
weight) at rest, tabular-nums so the date columns align. Selected/today
bump to 600 below. Button-reset props (background/border/font) so the cell
can be a real <button> — keyboard-operable, not a clickable <span>. */
font-weight: var(--k-ui-weight, 500);
font-variant-numeric: tabular-nums;
font-family: var(--k-font-body);
background: transparent;
border: 0;
cursor: pointer;
position: relative;
}
.calendar__cell:hover { background: var(--k-state-hover); }
.calendar__cell:active:not(.calendar__cell--out):not(.calendar__cell--disabled) { background: var(--k-state-press); }
.calendar__cell--on { background: var(--k-primary); color: var(--k-primary-fg); font-weight: var(--k-weight-semibold); }
.calendar__cell--out { color: var(--k-fg-faint); opacity: 0.4; cursor: default; }
.calendar__cell--out:hover { background: transparent; }
/* Today: thin ring without changing background — works alongside selection */
.calendar__cell--today { box-shadow: inset 0 0 0 1px var(--k-fg-faint); }
.calendar__cell--today.calendar__cell--on { box-shadow: none; }
/* Range fill — the SECONDARY container on dates between start and end (H4
usage pass: selected-but-not-the-action wears secondary, same rule as
.chip--on and .btn--toggle; the loud primary stays on the endpoints), with
edges flat-cornered to read as one connected band. */
.calendar__cell--range {
background: var(--k-secondary-soft);
color: var(--k-secondary-soft-fg);
border-radius: 0;
}
.calendar__cell--range-start {
background: var(--k-primary);
color: var(--k-primary-fg);
font-weight: var(--k-weight-semibold);
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.calendar__cell--range-end {
background: var(--k-primary);
color: var(--k-primary-fg);
font-weight: var(--k-weight-semibold);
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
/* Month-nav header (B4) — the title + prev/next cluster above the grid. Promotes
the picker's hand-rolled header into the kit so the FULL date-picker is one
exported pattern, not a per-app one-off. Pair the nav buttons with
.btn--ghost.btn--icon.btn--sm; the consumer owns month-shift + keyboard nav. */
.calendar__nav { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--k-s-6); }
.calendar__nav-title { font-size: var(--k-type-small); font-weight: var(--k-weight-semibold); color: var(--k-fg); }
.calendar__nav-btns { display: inline-flex; gap: var(--k-s-2); }
/* Disabled date (B4) — a day you can't pick (past / blackout / sold-out),
DISTINCT from --out (a faded prev/next-month day): muted + struck +
non-interactive. The consumer adds the native `disabled` + this class; the
recipe owns the look. Booking-grade pickers need this state. */
.calendar__cell--disabled { color: var(--k-fg-faint); opacity: 0.5; cursor: not-allowed; text-decoration: line-through; }
.calendar__cell--disabled:hover { background: transparent; }
/* --- Month as a SCHEDULER (.calendar--events) ----------------------------
The base .calendar is a centered date PICKER. An event-bearing month needs
taller, top-left cells that stack the day number above a run of event chips —
the Google/Notion month grid. Add .calendar--events to the grid; each cell
becomes a column (day number top-left, then .calendar__event chips, then a
.calendar__more overflow row). The picker states (--on/--today/--range) still
work, but events want their own quieter day-number treatment. */
.calendar--events .calendar__cell {
aspect-ratio: auto; min-height: var(--k-cal-cell-events, 5.5rem);
display: flex; flex-direction: column; align-items: stretch;
place-items: stretch; gap: var(--k-s-2);
padding: var(--k-s-4); border-radius: var(--k-radius-sm);
}
.calendar--events .calendar__cell:hover { background: var(--k-state-hover); }
.calendar--events .calendar__daynum { align-self: flex-start; font-variant-numeric: tabular-nums; color: var(--k-fg-muted); padding: 0 var(--k-s-2); }
.calendar--events .calendar__cell--out .calendar__daynum { color: var(--k-fg-faint); }
.calendar--events .calendar__cell--today .calendar__daynum {
display: grid; place-items: center; min-width: 1.5rem; height: 1.5rem;
border-radius: var(--k-radius-pill); background: var(--k-primary); color: var(--k-primary-fg);
font-weight: var(--k-weight-semibold); padding: 0;
}
/* event chip — a soft tonal pill with a leading colour edge + one-line title.
Timed events lead with a faint time; --allday is a flatter full-bleed bar.
Colour-code by calendar with --alt / --accent (mirrors the week-grid set). */
.calendar__event {
display: flex; align-items: center; gap: var(--k-s-4);
padding: var(--k-s-2) var(--k-s-4); border-radius: var(--k-radius-sm);
background: var(--k-primary-soft); color: var(--k-primary-soft-fg);
border-left: 2px solid var(--k-primary);
font-size: var(--k-type-eyebrow); line-height: 1.4;
cursor: pointer; text-align: left; border-top: 0; border-right: 0; border-bottom: 0;
}
.calendar__event:hover { filter: brightness(0.97); }
.calendar__event-time { opacity: 0.75; font-variant-numeric: tabular-nums; flex: none; }
.calendar__event-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.calendar__event--allday { border-left-width: 0; padding-left: var(--k-s-6); font-weight: var(--k-weight-medium); }
.calendar__event--alt { background: var(--k-secondary-soft); color: var(--k-secondary-soft-fg); border-left-color: var(--k-secondary); }
.calendar__event--accent { background: var(--k-accent-soft); color: var(--k-accent-soft-fg); border-left-color: var(--k-accent); }
/* "+N more" overflow — month cells cap their chip count and spill the rest into
this quiet link (opens the day). Keeps every cell on the row grid. */
.calendar__more {
align-self: flex-start; font-size: var(--k-type-eyebrow); color: var(--k-fg-muted);
cursor: pointer; padding: var(--k-s-2) var(--k-s-4); background: transparent; border: 0; text-align: left;
}
.calendar__more:hover { color: var(--k-fg); }
/* range hover-preview — while picking a range (start set, end not yet), the cells
between start and the hovered day get a LOWER-emphasis band so the forming
range is visible before commit. Consumer paints --range-preview on mouseover. */
.calendar__cell--range-preview { background: var(--k-secondary-soft); color: var(--k-secondary-soft-fg); border-radius: 0; opacity: 0.6; }
/* --- Date & time entry — the trichotomy (H4) ---
* The kit ships THREE date/time entry forms; pick by context, never invent a
* fourth:
* 1. DOCKED — the inline .calendar grid above, always visible (booking /
* scheduling surfaces where the date IS the content).
* 2. POPOVER — an .in--inline trigger opening a .popover with
* .calendar__nav + .calendar (the form-field default; on compact the
* same panel sits inside a .dialog — that's the "modal" arm).
* 3. INPUT — typed entry for keyboard-first / known values: a date .in,
* plus the .timefield below for times (M3's time-INPUT variant — we
* deliberately skip the clock-face dial as touch cosplay).
* Time field: hour + minute as two 2-digit cells around a colon, with an
* optional meridiem .segctrl. The cells ARE .in atoms, so Surface, Scale and
* radius cascade in for free; tabular-nums keeps the digits from wiggling. */
.timefield { display: inline-flex; align-items: center; gap: var(--k-s-4); }
.timefield .in {
width: calc(var(--k-in-h-default) * 1.4);
text-align: center;
font-variant-numeric: tabular-nums;
padding-inline: var(--k-s-4);
}
.timefield__sep { font-weight: var(--k-weight-semibold); color: var(--k-fg-muted); }
.timefield .segctrl { margin-left: var(--k-s-4); }