Cloud|

@k2b/ui

Date pickers

Date, date-time, and date-range selection with explicit timezone handling.

DatePicker@k2b/uiDateTimePicker@k2b/uiDateRangePicker@k2b/ui

Date, date-time, and range pickers share one controlled, timezone-aware calendar interaction with clear and preset support. Every calendar marks today in its time zone; the empty Due picker opens on it. The Deadline picker keeps a date and adds a time only on request.

TSX

Copy
<DatePicker label="Release date" value={date()} onValueChange={setDate} clearable />
<DatePicker label="Due" placeholder="Pick a due date" value={due()} onValueChange={setDue} dateConfig={dateConfig} clearable />
<DateTimePicker label="Starts at" value={dateTime()} onValueChange={setDateTime} dateConfig={dateConfig} />
<DateTimePicker label="Deadline" optionalTime defaultTime="17:00" value={deadline()} onValueChange={setDeadline} dateConfig={dateConfig} clearable />
<DateRangePicker label="Window" value={range()} onValueChange={setRange} presets={datePresets} />
<DateRangePicker label="One-day range" value={singleDayRange()} onValueChange={setSingleDayRange} />
<DatePicker label="Friday/Saturday weekend (he-IL)" value={fridayWeekendDate()} onValueChange={setFridayWeekendDate} dateConfig={{ locale: "he-IL" }} />
<DateRangePicker
label="Meeting window"
value={dateTimeRange()}
onValueChange={setDateTimeRange}
withTime
dateConfig={dateConfig}
datePresets={datePresets}
durationPresets={durationPresets}
/>

DatePicker, DateTimePicker, and DateRangePicker provide one controlled calendar interaction for date-only values, instants, and ranges. The parent owns the value and any preset choices.

Use a date picker

Use DatePicker for a calendar date such as a birthday or due date.

Use DateTimePicker for one date and time. Set optionalTime when the date alone is the normal case and a time is the exception, such as a task's due date. Use DateRangePicker for a start and end, with withTime when both endpoints include time.

Import

tsx
import {
  DatePicker,
  type DatePickerProps,
  type DatePreset,
  DateRangePicker,
  type DateRangePickerProps,
  type DateRangeValue,
  DateTimePicker,
  type DateTimePickerProps,
  type DurationPreset,
} from "@k2b/ui";
import type { DateContext } from "@k2b/stdlib";

Value formats

DatePicker reads and emits a date key such as 2026-07-27.

DateTimePicker emits a local date-time string when no time zone is configured. With dateConfig.timeZone, it displays wall-clock time in that zone and emits an instant.

With optionalTime, DateTimePicker emits a date key such as 2026-10-24 until a time is added, and the date-time form above with a time.

DateRangePicker uses { start, end }. Both values are date keys unless withTime is enabled, in which case they follow the DateTimePicker rules.

Pass the value directly or through a Solid accessor. Applying a selection or clearing reports the complete next value through both onValueChange and onValueCommit. Clearing emits null for a single picker or { start: null, end: null } for a range.

Show a date in a property row

Set appearance="plain" when the date is a property shown in a DescriptionList row, such as a task's due date. The trigger then reads as text, without box, calendar icon, or chevron, and the whole row opens the panel; see plain selects for the shared row behavior. A clearable plain picker clears from a Clear date button in the panel footer instead of beside the value. The empty value shows placeholder in muted text.

DatePicker and DateTimePicker accept renderValue(value) for the trigger content of a set value, for example the date followed by a relative hint. The panel and the emitted value are unchanged.

tsx
<DateTimePicker
  aria-label="Due"
  appearance="plain"
  placeholder="No due date"
  clearable
  value={due}
  onValueChange={setDue}
  renderValue={(value) => (
    <>
      {formatDateTime(value)} <span class="text-dimmed">· {formatTimeSpan(value)}</span>
    </>
  )}
/>;

Add a time only when needed

With optionalTime, the panel ends with Add time (Uhrzeit hinzufügen) beside Apply. Without a time, choosing a day applies it and closes the panel, as in DatePicker. Add time turns the button into the time field with a Remove time button and moves the focus into the field; a day then only marks the date until Apply. Remove time brings the button back with the focus on it, and Apply saves the date alone. Both states have the same height in the same place, so the panel never moves. The time starts at defaultTime (09:00 unless set), and a set value opens in its own state. The trigger shows the calendar icon for a date and the calendar-with-clock icon for a date with a time.

Presets in this mode can mix both forms, for example a date key for Tomorrow.

tsx
const [due, setDue] = createSignal<string | null>("2026-10-24");

<DateTimePicker
  label="Due"
  optionalTime
  defaultTime="17:00"
  value={due}
  onValueChange={setDue}
  dateConfig={{ timeZone: "Europe/Berlin" }}
  clearable
/>;

Presets and ranges

One click picks a complete one-day range. The next click on another day extends it; the earlier day becomes the start. If you are used to selecting both endpoints, those two clicks still pick the same range, but a one-day range now needs only one click before Apply. Clicking that same day again keeps it. After an extension, the next click starts a new one-day range; reopening the panel also starts a fresh selection. Hover or focus previews an extension until you pick the second day.

The date-only duration in the footer uses the date locale, for example 1 day / 3 days in English and 1 Tag / 3 Tage in German.

presets contains caller-defined labels and complete values. The components do not provide a fixed preset list.

datePresets changes the selected date without closing the picker and applies to every DateRangePicker. durationPresets changes the end from the current start by a number of minutes and appears only with withTime.

Use dateConfig to define the application time zone and first day of the week. Without a supplied week start, the calendar starts on Monday.

An explicit dateConfig.locale wins; without one the pickers inherit the render locale from LocaleProvider or the browser's <html lang> (see the Locale and formatting page). The timezone never comes from the locale context.

Weekend columns

Weekend columns have a shaded background, from the weekday labels through the last week. They follow the locale's week data: Saturday and Sunday in de-DE, Friday and Saturday in he-IL. Without available week data, Saturday and Sunday are the fallback. The grid keeps the first day of the week from dateConfig; the shading is background only and causes no layout shift.

Today

Every calendar marks today: the day number in the accent color with a small dot below it. A selected today keeps the filled selection, and its number and dot take the fill's contrasting color. Today on a day outside the visible month keeps the muted number of the other outside days, and only its dot takes the accent color. The mark is drawn inside the day cell, so it never changes the grid's size.

Today is the current date in dateConfig.timeZone, the zone the values use, not in the server's or the device's zone. Without a time zone it is the runtime's local date. An open page moves the mark when the next day starts in that zone, also where a daylight saving change skips or repeats midnight. It also checks the date again when a hidden tab becomes visible and when dateConfig changes. The pickers have no built-in Today preset; add one through presets when a form needs it.

API reference

See shared field props for FieldProps, ValueFieldProps<T> and MaybeAccessor<T>.

ts
type DatePreset<T> = {
  label: string; value: T;
};

type DurationPreset = {
  label: string; minutes: number;
};

type DateRangeValue = {
  start: string | null; end: string | null;
};

type DatePickerBaseProps<T> = Omit<ValueFieldProps<T>, "value"> & {
  placeholder?: string; value: MaybeAccessor<T>; presets?: readonly DatePreset<T>[]; dateConfig?: DateContext;
  clearable?: boolean; appearance?: ChoiceAppearance;
};

type DatePickerProps = DatePickerBaseProps<string | null> & {
  renderValue?: (value: string) => JSX.Element;
};

type DateTimePickerProps = DatePickerBaseProps<string | null> & {
  renderValue?: (value: string) => JSX.Element; optionalTime?: boolean; defaultTime?: string;
};

type DateRangePickerProps = DatePickerBaseProps<DateRangeValue> & {
  withTime?: boolean; datePresets?: readonly DatePreset<string | null>[];
  durationPresets?: readonly DurationPreset[];
};

DateContext is defined under date and locale options. DatePicker emits YYYY-MM-DD; without a timezone DateTimePicker emits YYYY-MM-DDTHH:mm, with a timezone it emits an ISO instant in UTC. With optionalTime, DateTimePicker emits YYYY-MM-DD for a date without time and the datetime contract with one. DateRangePicker follows the date contract by default and the datetime contract with withTime=true. Empty range endpoints are null, not an absent range object. Supply preset values in the same format as the controlled value.

ChoiceAppearance is "field" | "plain" and defaults to "field". renderValue only replaces the trigger content of a set value. presets applies complete values; datePresets picks individual dates in a timed range, and durationPresets.minutes adjusts its duration. Clearing is enabled unless clearable={false}. Week-start and locale follow dateConfig; withTime defaults to false.

Accessibility

Provide a visible label or a specific placeholder. The trigger exposes dialog and expanded state, and direct JSX descriptions and errors are connected to it. A button's label replaces its content, so the trigger also names its shown value, or the placeholder, as its description: a plain picker with aria-label="Due" reads as Due followed by the date.

With optionalTime, Add time is a button with its visible name, and the icon-only Remove time button carries that name as its label.

Calendar navigation has named previous and next controls. Selected days and active duration presets expose pressed state. Time fields receive start and end labels in a range.

Today's day carries aria-current="date", and its name starts with Today (Heute in German) in the date's locale, for example "Today, Friday, October 9, 2026". Without a value the calendar's tab stop starts on today.

Runtime

The trigger and current value render in server HTML. Opening the calendar, navigating dates, selecting presets, and applying time or range values require browser code.

Example

tsx
const dateConfig: DateContext = {
  timeZone: "Europe/Berlin",
  weekStartsOn: 1,
};

const [startsAt, setStartsAt] = createSignal<string | null>(null);

<DateTimePicker
  label="Starts at"
  placeholder="Pick date and time"
  value={startsAt()}
  onValueChange={setStartsAt}
  dateConfig={dateConfig}
  clearable
/>;

Connect a coding agent

The Cloud skill provides compact working instructions. MCP supplies exact current documentation when details matter.

CLI command
bunx skills add https://docs.example.com

This command installs the working instructions published by this website in the selected coding agent.

For exact, current details, also connect the MCP server through one of the agent tabs.

Installation uses the open-source Vercel Skills CLI.

The skill and MCP complement each other: the skill describes workflows, while MCP supplies current documentation.