ComponentsDate Picker

Date Picker

component

A 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); }