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.
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.
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
| Mode | Trigger pattern | Popover |
|---|---|---|
date | MM/DD/YYYY | Calendar only |
time | HH:mm | TimeScrollPicker only |
dateTime | MM/DD/YYYY HH:mm | Calendar + 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.
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.
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:
| Widget | Selection | Use directly |
|---|---|---|
CdxSingleDatePopover | Single date — single tap commits, optional Today action via showToday / onTodayChanged | Custom date pickers |
CdxDateRangePopover | Two-tap range, with mid-range band visual | Custom 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:
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:
DateTimePickerreads a typed 10:00 as 10:00 in Asia/Kolkata (04:30 UTC) and shows stored instants on that zone's clock. An explicitdatePatternortimePatternkeeps its legacy device-time mask.CdxDateRangePickeremits 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,ContentStateViewand the defaultCdxStringsnumbers 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.
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)],
),
)| Parameter | Notes |
|---|---|
firstDayOfWeek | Else the scope's, else the locale's |
today | Else today in the scope's zone, else the device's |
isSelected / isEnabled | Predicates over calendar dates (local midnight) |
dayBuilder | Draws a CdxMonthDay (date, in-month, today, selected, enabled, hovered, focused); CdxMonthDayMark by default |
rowHeight | Else the rows share a height the parent fixes, or are a control tall |
fixedWeeks / showOutsideDays / showWeekdays | Layout 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.
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.
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.
| Token | Meaning |
|---|---|
DD | Day (01–31) |
MM | Month (01–12) |
YYYY | Year (4 digits) |
HH | Hour (00–23) |
mm | Minute (00–59) |
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.
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.
CdxMenuSurface(
child: CdxColorPalette(
title: 'Fill',
value: fill,
swatches: cdxDefaultColorPalette,
onSelect: (color) => setFill(color),
onClose: close,
),
)Cross-links
- Overlays —
buildOverlayFollowerpositioning - Inputs — text-input formatting patterns
- Field Formatting —
FieldFormatterfor displaying formatted dates/times outside the picker