Skip to content

Datetime Picker

SCADatetimePicker is a self-contained calendar / time picker built entirely from SCA* primitives (no third-party date UI). It can be used as a dropdown (default, rendered as an input-style trigger) or inline, and supports six modes: date, time, datetime, dateRange, datetimeRange, timeRange.

The trigger comes in two flavours: the default read-only value display, or an opt-in manual-input mode (manual-input) that lets the user type the value into a masked, section-by-section editor while the calendar becomes an optional helper. See Manual input (masked typing).

  • v-model is a Date | null for single modes and a [Date | null, Date | null] tuple for range modes.
  • All formatting and weekday/month names are locale-aware (driven by the active vue-i18n locale, mapped to a date-fns locale — de for German, en-US otherwise).
  • The calendar is fully keyboard- and pointer-operable and exposes test-id hooks throughout for E2E.
  • The picker renders only the control — no label, helper text, or validation state. To add those, wrap it in SCAInput (see Labels, helper text & validation).

Architecture

The picker is a small tree of focused components. You normally only use SCADatetimePicker; the others are exported for advanced use.

ComponentResponsibility
SCADatetimePickerPublic entry point. Chooses dropdown vs. inline, renders the trigger (value display or the masked editor), formats the display value, handles clear, closes the dropdown when a selection completes, aggregates the masked editors' invalid flag.
SCACalendarPanelThe panel body: toolbar, month grid(s), and the time area. Owns all selection state, the range state machine, and value composition. Exported for direct/inline use.
CalendarMonthGridRenders one month (weekday header, week numbers, day cells) plus the year picker and the animated range bar.
CalendarDayCellA single day button — the circle, selected/today/preview/flash styling.
TimeUnitSelectA single numeric dropdown (hours or minutes or seconds). Used by time and datetime.
TimeCombinedSelectA combined hh:mm dropdown at minuteStep intervals. Used by the range modes (Start / End).
calendarUtils.tsLocale resolution, month-row building (ISO week numbers), localized weekday labels.
datetimePickerTypes.tsMode/value types and mode predicates (isRangeMode, hasDate, hasTime).

Modes

Modev-model shapeCalendarTime UI
dateDate | null
timeDate | null (today's date + time)Separate Hour + Minutes dropdowns, stacked
datetimeDate | nullSeparate h + min dropdowns, side by side
dateRange[Date|null, Date|null]
datetimeRange[Date|null, Date|null]Combined hh:mm per Start/End + All-day toggle
timeRange[Date|null, Date|null] (today's date + times)Combined hh:mm per Start/End + All-day toggle

Props

PropTypeDefaultDescription
mode'date' | 'time' | 'datetime' | 'dateRange' | 'datetimeRange' | 'timeRange''date'Picker mode (see table above).
modelValueDate | null | [Date|null, Date|null]nullSelected value(s).
allDaybooleanfalseAll-day mode for the range modes. When on, the time selects are disabled and the emitted range spans start-of-day … end-of-day. Two-way via v-model:all-day.
monthsnumber1Number of months rendered. Two-way via v-model:months — the user can change it in the gear popover, so bind it if you persist the setting.
columnsnumber1Column count for the multi-month grid. Clamped to months. Two-way via v-model:columns.
configCalendarDisplayConfigsee belowStatic calendar configuration (one object). Read once at setup — not reactive; provide the full object up front.
inlinebooleanfalseRender the panel inline instead of inside a dropdown.
readonlybooleanfalseShow the panel but ignore all selection/toggle interaction.
dateFormatstring'dd.MM.yyyy'date-fns format for the date portion of the display value.
timeFormatstring'HH:mm'date-fns format for the time portion of the display value.
datePrecisionstringPrecision token (yy, yy_mm, yy_mm_dd) that drops finer date sections from dateFormat before display/editing. Applied via @scayla/core's applyDatePrecision.
timePrecisionstringPrecision token (hh, hh_mm, hh_mm_ss, hh_mm_ss_ms) that drops finer time sections from timeFormat. Applied via applyTimePrecision.
manualInputbooleanfalseRender the trigger as a masked section editor the user can type into, instead of the read-only value display. The calendar then opens only via v-model:is-open.
clearablebooleantrueShow a clear (×) control in the trigger when a value is set.
placeholderstringTrigger placeholder when empty.
disabledbooleanfalseDisable the trigger.
htmlForstringid applied to the trigger <input> (for external <label for> or an SCAInput wrapper).
alertbooleanfalseForce the alert (error) underline on the trigger. In manual-input mode it is OR-ed with the editors' own invalid state.
isOpenbooleanfalseWhether the calendar dropdown is open. Two-way via v-model:is-open. In manual-input mode this is the only way to open the calendar (it no longer opens on focus).
invalidbooleanfalse(manual-input) true while any masked editor holds an incomplete or impossible entry. Read-only two-way via v-model:invalid — aggregated from the child editor(s).

configCalendarDisplayConfig

All fields are optional; defaults are merged in. The object is read once when the component is created — later mutations are ignored by design (static configuration, not state). The type is exported from @scayla/components.

FieldTypeDefaultDescription
maxMonthsnumber12Upper bound for the "Months" setting in the gear popover. If > 1, the gear (multi-month settings) is shown.
showWeekNumbersbooleantrueShow the Wk (week-number) column.
weekStartsOn0–61 (Mon)First day of the week (0 = Sun). Also drives ISO week numbering.
min / maxDate | nullnullSelectable date bounds (inclusive, compared at day granularity). Also bound the year picker range.
disabledDate(date: Date) => booleanPredicate to disable individual dates.
minuteStepnumber15Minute granularity for the combined hh:mm dropdowns (range modes).

Labels, helper text, and validation are not picker props. The picker renders only the control; wrap it in SCAInput to add a label / helper text / validation state — see Labels, helper text & validation.

Emits

EventPayloadWhen
update:modelValueDatetimePickerValueAny value change.
update:allDaybooleanThe all-day toggle flips.
update:monthsnumberThe "Months" setting changes in the gear popover.
update:columnsnumberThe "Columns" setting changes in the gear popover (also when auto-clamped to a lower months).
update:isOpenbooleanThe calendar dropdown opens or closes.
update:invalidboolean(manual-input) The aggregated typed-entry validity changes.

Auto-close: in dropdown mode the panel emits an internal selectionComplete that closes the popup once a selection is "done" — for date (a day is picked) and dateRange (both endpoints picked). Modes with a time step stay open so the user can adjust the time.

Value semantics

  • date → the picked day at 00:00:00.
  • datetime → picked day combined with the chosen hours/minutes (seconds 0).
  • time → today's date combined with the chosen time.
  • dateRange[start, end], both at start-of-day, always ordered (earlier first).
  • datetimeRange[start+time, end+time]; when all-day, [startOfDay(start), endOfDay(end)].
  • timeRange → today's date combined with start/end times; when all-day, [startOfDay(today), endOfDay(today)].

The trigger display value uses dateFormat/timeFormat (combined with a space for datetime modes, joined with for ranges).

Toolbar & navigation (calendar modes)

Calendar modes render a toolbar above the grid:

  • ‹ / › — step one month back / forward.
  • Gear (only when config.maxMonths > 1) — a settings popover with a Months select and a Columns 1–4 toggle (columns are capped at the current month count). Both settings are exposed as v-model:months / v-model:columns so they can be persisted (e.g. as a user setting).
  • Today — jump back to the current month and play a highlight flash pulse on today's cell (repeat clicks restart the animation).
  • Range label — the visible month (or first – last for multi-month). It automatically switches from the full month name (September 2026) to the abbreviated form (Sep 2026) when the full label would overflow the toolbar width.
  • "Now" label — a live, locale-formatted current date/time line (EEE dd. MMM. yyyy, HH:mm).

Year picker

Click the month/year label inside a month header to open a year dropdown. It lists years from min (or ~100 years back) to max (or ~100 years forward), highlights the current year, and scrolls it into the middle of the list on open. Selecting a year keeps the same month.

Time input layouts

  • time — two separate dropdowns, Hour (0–23) and Minutes (0–59, step 1), stacked vertically.
  • datetime — two separate dropdowns, h and min, laid out side by side with compact labels.
  • Range modes (datetimeRange, timeRange) — one combined hh:mm dropdown for Start and one for End, listing every minuteStep minutes across the day. A current value that doesn't land on the step grid (e.g. the 23:59 produced by all-day) stays selectable. An All-day toggle disables both time selects and switches the emitted value to day-spanning bounds.

Range selection & hover preview

Range modes use a two-click state machine: the first click sets the start (and clears any previous end); the second click sets the end, auto-ordering the two dates. While picking the end, hovering a day previews the resulting span with an animated range bar that grows forward or backward from the anchor, and the hovered endpoint gets a preview highlight.

Manual input (masked typing)

Set manual-input to turn the trigger into a masked, section-by-section editor the user can type into — day, month, year, hour, … are each an independently-typed section (MUI-style). This is the mode records use, so keyboard-first users can type a date instead of clicking through the calendar.

  • Calendar becomes optional. It no longer opens on focus; open it only through v-model:is-open (e.g. from a calendar/clock icon you render nearby).
  • Precision-driven sections. date-precision / time-precision reduce which sections exist before the editor sees the pattern, so a yy_mm field shows only month + year.
  • Ranges look like one field. Range modes render two editors with a between them inside a single input chrome.
  • Validity is aggregated. v-model:invalid is true while any editor holds an incomplete or impossible entry (e.g. 31.02.). An invalid value is never emitted through v-model, so bad data can't leak out.
  • Textual months. If the format uses MMMM / MMM, the month section shows a localized name and can be typed by name (jul → July) or digit (3 → March).

Manual input — single

Manual input — textual month

Manual input — datetime with precision

Manual input — range

Examples

Date (dropdown)

Date (inline)

Date + time

Time only

Date range

Date + time range (with all-day toggle)

Time range

Multi-month (inline)

Open the gear icon in the toolbar to change the number of months and columns (up to config.maxMonths). Bind v-model:months / v-model:columns to observe or persist the settings.

Min / max & disabled dates

Weekends are disabled here, and selection is bounded to the current month.

Custom display format

Without week numbers, Sunday-first

Readonly

The panel renders but ignores clicks and toggles.

Disabled

The calendar is not clickable.

Localization

  • The active vue-i18n locale is mapped to a date-fns locale via resolveDateFnsLocale (de* → German, everything else → en-US). This drives month names, weekday labels, and the "now" line.

  • Week numbers are ISO-style (firstWeekContainsDate: 4) and honor weekStartsOn.

  • UI strings live under the datetimePicker.* i18n namespace in libs/components/src/i18n/locales/{en,de}.json:

    today, settings, months, columns, start, end, h, hour, m, minutes, seconds, allDay, weekShort, previousMonth, nextMonth.

Test IDs (E2E)

The panel and its controls expose stable test-ids:

test-idElement
sca-calendar-panelPanel root
sca-calendar-prev / sca-calendar-nextPrevious / next month
sca-calendar-settingsGear (multi-month settings) trigger
sca-calendar-months-selectMonths select in the settings popover
sca-calendar-columns-{1..4}Columns toggle buttons
sca-calendar-nowThe live "now" label
sca-calendar-month-{i}Month grid at index i
sca-calendar-month-{i}-periodMonth header (year-picker trigger)
sca-calendar-month-{i}-year-{year}A year option in the year picker
sca-calendar-month-{i}-day-{yyyy}-{m}-{d}A specific day cell
sca-calendar-timeTime area container
sca-time-hours / sca-time-minutesSingle hour / minute selects (time, datetime)
sca-time-start / sca-time-endCombined start / end selects (range modes)
sca-time-alldayAll-day toggle