Appearance
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-modelis aDate | nullfor 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-i18nlocale, mapped to adate-fnslocale —defor German,en-USotherwise). - The calendar is fully keyboard- and pointer-operable and exposes
test-idhooks 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.
| Component | Responsibility |
|---|---|
SCADatetimePicker | Public 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. |
SCACalendarPanel | The 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. |
CalendarMonthGrid | Renders one month (weekday header, week numbers, day cells) plus the year picker and the animated range bar. |
CalendarDayCell | A single day button — the circle, selected/today/preview/flash styling. |
TimeUnitSelect | A single numeric dropdown (hours or minutes or seconds). Used by time and datetime. |
TimeCombinedSelect | A combined hh:mm dropdown at minuteStep intervals. Used by the range modes (Start / End). |
calendarUtils.ts | Locale resolution, month-row building (ISO week numbers), localized weekday labels. |
datetimePickerTypes.ts | Mode/value types and mode predicates (isRangeMode, hasDate, hasTime). |
Modes
| Mode | v-model shape | Calendar | Time UI |
|---|---|---|---|
date | Date | null | ✅ | — |
time | Date | null (today's date + time) | — | Separate Hour + Minutes dropdowns, stacked |
datetime | Date | null | ✅ | Separate 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
| Prop | Type | Default | Description |
|---|---|---|---|
mode | 'date' | 'time' | 'datetime' | 'dateRange' | 'datetimeRange' | 'timeRange' | 'date' | Picker mode (see table above). |
modelValue | Date | null | [Date|null, Date|null] | null | Selected value(s). |
allDay | boolean | false | All-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. |
months | number | 1 | Number 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. |
columns | number | 1 | Column count for the multi-month grid. Clamped to months. Two-way via v-model:columns. |
config | CalendarDisplayConfig | see below | Static calendar configuration (one object). Read once at setup — not reactive; provide the full object up front. |
inline | boolean | false | Render the panel inline instead of inside a dropdown. |
readonly | boolean | false | Show the panel but ignore all selection/toggle interaction. |
dateFormat | string | 'dd.MM.yyyy' | date-fns format for the date portion of the display value. |
timeFormat | string | 'HH:mm' | date-fns format for the time portion of the display value. |
datePrecision | string | — | Precision token (yy, yy_mm, yy_mm_dd) that drops finer date sections from dateFormat before display/editing. Applied via @scayla/core's applyDatePrecision. |
timePrecision | string | — | Precision token (hh, hh_mm, hh_mm_ss, hh_mm_ss_ms) that drops finer time sections from timeFormat. Applied via applyTimePrecision. |
manualInput | boolean | false | Render 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. |
clearable | boolean | true | Show a clear (×) control in the trigger when a value is set. |
placeholder | string | — | Trigger placeholder when empty. |
disabled | boolean | false | Disable the trigger. |
htmlFor | string | — | id applied to the trigger <input> (for external <label for> or an SCAInput wrapper). |
alert | boolean | false | Force the alert (error) underline on the trigger. In manual-input mode it is OR-ed with the editors' own invalid state. |
isOpen | boolean | false | Whether 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). |
invalid | boolean | false | (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). |
config — CalendarDisplayConfig
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.
| Field | Type | Default | Description |
|---|---|---|---|
maxMonths | number | 12 | Upper bound for the "Months" setting in the gear popover. If > 1, the gear (multi-month settings) is shown. |
showWeekNumbers | boolean | true | Show the Wk (week-number) column. |
weekStartsOn | 0–6 | 1 (Mon) | First day of the week (0 = Sun). Also drives ISO week numbering. |
min / max | Date | null | null | Selectable date bounds (inclusive, compared at day granularity). Also bound the year picker range. |
disabledDate | (date: Date) => boolean | — | Predicate to disable individual dates. |
minuteStep | number | 15 | Minute 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
SCAInputto add a label / helper text / validation state — see Labels, helper text & validation.
Emits
| Event | Payload | When |
|---|---|---|
update:modelValue | DatetimePickerValue | Any value change. |
update:allDay | boolean | The all-day toggle flips. |
update:months | number | The "Months" setting changes in the gear popover. |
update:columns | number | The "Columns" setting changes in the gear popover (also when auto-clamped to a lower months). |
update:isOpen | boolean | The calendar dropdown opens or closes. |
update:invalid | boolean | (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 at00:00:00.datetime→ picked day combined with the chosen hours/minutes (seconds0).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 asv-model:months/v-model:columnsso 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 – lastfor 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 everyminuteStepminutes across the day. A current value that doesn't land on the step grid (e.g. the23:59produced 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-precisionreduce which sections exist before the editor sees the pattern, so ayy_mmfield 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:invalidistruewhile any editor holds an incomplete or impossible entry (e.g.31.02.). An invalid value is never emitted throughv-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-i18nlocale is mapped to adate-fnslocale viaresolveDateFnsLocale(de*→ German, everything else →en-US). This drives month names, weekday labels, and the "now" line.Week numbers are ISO-style (
firstWeekContainsDate: 4) and honorweekStartsOn.UI strings live under the
datetimePicker.*i18n namespace inlibs/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-id | Element |
|---|---|
sca-calendar-panel | Panel root |
sca-calendar-prev / sca-calendar-next | Previous / next month |
sca-calendar-settings | Gear (multi-month settings) trigger |
sca-calendar-months-select | Months select in the settings popover |
sca-calendar-columns-{1..4} | Columns toggle buttons |
sca-calendar-now | The live "now" label |
sca-calendar-month-{i} | Month grid at index i |
sca-calendar-month-{i}-period | Month 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-time | Time area container |
sca-time-hours / sca-time-minutes | Single hour / minute selects (time, datetime) |
sca-time-start / sca-time-end | Combined start / end selects (range modes) |
sca-time-allday | All-day toggle |