Skip to content

Pickers ​

Date, time, and date-range pickers all share a common structure: an editable / read-only text trigger field with an inline popover anchored below. All values flow through as UTC DateTime instances.

Live Example ​

Open the full route: Pickers.

DateTimePicker ​

Editable text field + popover. Drives mode (date / time / dateTime) to choose what the popover shows. The text field enforces the format via DateTimeInputFormatter so users can either type or pick.

dart
DateTimePicker(
  mode: DateTimePickerMode.dateTime,
  value: selected,
  onChanged: (dt) => setState(() => selected = dt),  // dt is UTC
  firstDate: DateTime(2020),
  lastDate: DateTime(2030),
  minuteStep: 5,
  datePattern: 'MM/DD/YYYY',  // default
  timePattern: 'HH:mm',       // default
  showToday: true,            // default — show Today quick-jump in date modes
)

The field draws its own chrome from plain facts: label, helperText, errorText, and a prefixIcon / suffixIcon (the suffix sits ahead of the Clear and calendar buttons). An errorText replaces the helper line and turns the outline the error colour. The picker takes no Material InputDecoration.

dart
DateTimePicker(
  mode: DateTimePickerMode.date,
  value: due,
  label: 'Due',
  helperText: 'The day the work is owed',
  errorText: due == null ? 'Pick a day' : null,
  onChanged: (dt) => setState(() => due = dt),
)

DateTimePickerMode ​

ModeTrigger patternPopover
dateMM/DD/YYYYCalendar only
timeHH:mmTimeScrollPicker only
dateTimeMM/DD/YYYY HH:mmCalendar + time scroll side-by-side

CdxDateRangePicker ​

Range-specific variant. Read-only trigger displays MMM d, y - MMM d, y (formatted via FieldFormatter.instance.formatDate). The popover is a single-month calendar with shadcn-style range visuals: filled circles on start/end, light band between.

dart
CdxDateRangePicker(
  value: range,                 // DateTimeRange?
  firstDate: DateTime(2020),
  lastDate: DateTime(2030),
  onChanged: (r) => setState(() => range = r),
  hintText: 'Pick a date range',
  label: 'Period',              // also helperText: and errorText:
)

Two-tap selection model:

  • First tap → start, end cleared
  • Second tap → end (swapped if before start)
  • Third tap → resets start

DateTimePopover ​

The popover content used by DateTimePicker (date / time / dateTime modes). Useful when you want to embed the picker UI without the text trigger.

dart
DateTimePopover(
  value: current,
  showDate: true,
  showTime: true,
  onChanged: (dt) => store.setSchedule(dt),
  firstDate: DateTime(2020),
  lastDate: DateTime(2030),
  minuteStep: 15,
  showToday: true,
  onTodaySelected: () => analytics.track('today_selected'),
)

Calendar popovers ​

Two raw calendar popovers underlie the dateTime / range pickers:

WidgetSelectionUse directly
CdxSingleDatePopoverSingle date — single tap commits, optional Today action via showToday / onTodayChangedCustom date pickers
CdxDateRangePopoverTwo-tap range, with mid-range band visualCustom range pickers

Both share the same header (tappable month-year title opens a year grid) and weekday/day grid aesthetic.

Both take an optional firstDayOfWeek (a CdxWeekday) and an optional now (the wall clock that marks today and that the Today action reads). Without them the weeks start on the nearest CdxFormatScope's first day, else on the locale's (MaterialLocalizations.firstDayOfWeekIndex), and today is the scope zone's, else the device's.

Each has a natural width of its own, exposed as a constant so overlays and hosts share one source: CdxSingleDatePopover.width and CdxDateRangePopover.width (320 px), and DateTimePopover.dateWidth (320), dateTimeWidth (440) and timeWidth (140). A loose parent (Align, a Row child, a menu item) gets exactly that width in every month; a tight parent, such as the picker overlay capped to a narrow screen, overrides it and the content flexes (the date-time popover shares its width between calendar and clock).

Region — CdxFormatScope ​

The pickers show and read values in a region: a time zone, date and time patterns, number separators and a first day of the week, carried by FormatSettings. Without a scope they use the global FieldFormatter.instance.settings and follow its changes, as they always have. Put a CdxFormatScope above a screen to show it in a site's region, whatever the global settings say:

dart
CdxFormatScope(
  settings: FormatSettings.defaults.copyWith(
    timeZone: 'Asia/Kolkata',
    dateFormat: 'dd-MM-yyyy',
    timeFormat: 'HH:mm',
    firstDayOfWeek: 1, // Monday; 0 is Sunday
  ),
  child: const SiteScreen(),
)

Under that scope:

  • DateTimePicker reads a typed 10:00 as 10:00 in Asia/Kolkata (04:30 UTC) and shows stored instants on that zone's clock. An explicit datePattern or timePattern keeps its legacy device-time mask.
  • CdxDateRangePicker emits each endpoint as the instant its day begins in the zone, and writes it with the scope's date pattern.
  • The calendars start their weeks on the scope's first day and mark the zone's today.
  • CdxFieldValueTable, StatusBadge.count, ContentStateView and the default CdxStrings numbers use the scope's separators.

Scopes nest; the nearest wins. CdxFormatScope.maybeOf(context) reads it, and the CdxRegionClock extension on FormatSettings gives its wallClock, dateOf, startOfDay and firstWeekday. FormatSettings is exported by vyuh_cdx_ui, so an app need not depend on cdx_field_formatting to build one.

CdxMonthGrid ​

The atomic month calendar: a row of weekday names over one row per week. It draws no header and keeps no month of its own, so a date popover or a page-sized calendar view builds on it.

dart
CdxMonthGrid(
  month: _month,
  firstDayOfWeek: CdxWeekday.monday,
  isSelected: (day) => DateUtils.isSameDay(day, _picked),
  isEnabled: (day) => !day.isBefore(_earliest),
  onDayPressed: (day) => setState(() => _picked = day),
  onMonthChanged: (month) => setState(() => _month = month),
  dayBuilder: (context, day) => Column(
    children: [CdxMonthDayMark(day: day), ..._eventsOn(day.date)],
  ),
)
ParameterNotes
firstDayOfWeekElse the scope's, else the locale's
todayElse today in the scope's zone, else the device's
isSelected / isEnabledPredicates over calendar dates (local midnight)
dayBuilderDraws a CdxMonthDay (date, in-month, today, selected, enabled, hovered, focused); CdxMonthDayMark by default
rowHeightElse the rows share a height the parent fixes, or are a control tall
fixedWeeks / showOutsideDays / showWeekdaysLayout switches

The grid owns gestures, keyboard and semantics; the builder owns the look, including its hover and focus layers, so they take its shape. The grid is one focus stop: arrows move a day or a week, Home/End go to the ends of the week, Page Up/Down move a month, Enter or Space presses. A move off the month calls onMonthChanged; without it the move stops at the edge. Each day reads as its full date, with "today" and its selected state. CdxMonthLayout holds the pure rule of which date sits in which cell.

TimeScrollPicker ​

iOS-style two-column scrollable time picker. Each column auto-centers the selection.

dart
TimeScrollPicker(
  hour: time.hour,
  minute: time.minute,
  minuteStep: 15,                              // snaps minutes to nearest valid step
  onHourChanged: (h) => store.setHour(h),
  onMinuteChanged: (m) => store.setMinute(m),
)

Fixed at 120 px wide, expands to fill its parent's vertical constraint via Expanded.

TimeStepper ​

Compact ◀ 22 ▶ row with arrow buttons and a tappable middle that enters direct-edit mode. Mouse scroll wheel increments / decrements. Values wrap past min / max.

dart
TimeStepper(
  value: minutes,
  min: 0,
  max: 59,
  step: 1,
  onChanged: (v) => store.setMinutes(v),
)

DateTimeInputFormatter ​

TextInputFormatter enforcing a date/time pattern. Auto-inserts separators (/, -, ., :, ), validates field ranges inline (e.g. month can only start with 0 or 1), rejects impossible dates (Feb 30), and exposes tryParse / format helpers.

TokenMeaning
DDDay (01–31)
MMMonth (01–12)
YYYYYear (4 digits)
HHHour (00–23)
mmMinute (00–59)
dart
final formatter = DateTimeInputFormatter(pattern: 'YYYY-MM-DD HH:mm');

TextField(
  inputFormatters: [formatter],
  keyboardType: TextInputType.number,
);

final dt = formatter.tryParse('2026-04-18 14:30');  // local DateTime or null
final str = formatter.format(DateTime.now());        // '2026-04-18 14:30'

UTC contract ​

Every picker emits UTC values via onChanged, and accepts UTC values as value. Internal display logic converts to local for the calendar / text trigger so users see their local date/time, but the round-trip is always UTC. Match this in your store: store UTC, display local through the picker.

CdxColorSwatches ​

Colour swatches inline in a form or an inspector: round discs in a row that wraps, one per CdxColorSwatch, the value among them ringed. Each is a button named by its label (on hover and to a screen reader) that says whether it is chosen. A hovered swatch wears the chosen ring, drawn inside its box, so nothing moves.

dart
CdxColorSwatches(
  swatches: [
    CdxColorSwatch(scheme.primary, 'Primary'),
    CdxColorSwatch(scheme.error, 'Error'),
  ],
  value: color,
  onSelect: (picked) => setState(() => color = picked),
)

allowNoColor adds a first swatch (noColorLabel, "No color") that picks null; trailing puts a control after the last swatch, on the same line. Wrap it in a CdxDisabled to mute it.

CdxColorPalette ​

The panel a CdxColorField opens: a title, the preset swatches (as CdxColorSwatches), up to ten customColors (the value first when it is not a preset), and an add button that turns the panel into the full CdxColorPicker. It has no surface of its own; set it on a CdxMenuSurface, as the field does in its popover. onClose adds a close button to the heading, and allowAlpha lets the custom picker edit alpha.

dart
CdxMenuSurface(
  child: CdxColorPalette(
    title: 'Fill',
    value: fill,
    swatches: cdxDefaultColorPalette,
    onSelect: (color) => setFill(color),
    onClose: close,
  ),
)
  • Overlays — buildOverlayFollower positioning
  • Inputs — text-input formatting patterns
  • Field Formatting — FieldFormatter for displaying formatted dates/times outside the picker