ComponentsText & numbersDate
On this page
Text & numbers

Date released

A date typed into a field or picked from a calendar the field opens.

  • Import@pacit/components/date
  • Selectorpct-date

Implements the W3C ARIA APG Date Picker Dialog pattern ↗

Type it, or pick it from the calendar

date.demo.ts — the same file runs here and is highlighted under Code

Start here

Usage

One import per component, and the shortest possible use — the package itself is installed once, on the start page.

Import this component
import { PctDate } from '@pacit/components/date';
ExportedWhat it is
PctCalendarCellinterfaceOne cell of the grid, with everything the template and the reader need already decided.
PctDayPredicatetype(day: PctDay) => booleanA day the consumer refuses — weekends, holidays, a taken slot.
PctCalendarcomponentpct-calendarCalendar — one month of days as a role="grid", walked by a roving tabindex.
PctDatePanelAligntype'start' | 'end'Which edge of the field the panel lines its own up with.
PctDatecomponentpct-dateDate field — a text control the library formats and parses per locale, with a calendar in a panel beside it.
PctDayBoundtypeunreleasedPctDay | undefined | LogicFn<TValue, PctDay | undefined, TPathKind>A bound pctDayBounds takes: a day, or a function of the field's context that returns one — the two shapes Angular's own min() takes. One that is not a day, undefined among them, is no bound, as it is on the field's input.
PctDayBoundErrortypeunreleased| { readonly kind: 'minDay'What pctDayBounds reports: minDay for a day before min, maxDay for one after max. Each carries the bound it broke, as the bound was written.
PctDayBoundsConfiginterfaceunreleasedThe bounds of pctDayBounds, and the sentence its errors carry — one for both, or one written from the error, which holds the bound.
pctDayBoundsfunctionunreleasedBounds a date field in a signal form: what Angular's minDate() and maxDate() do for a Date, for a PctDay — which they cannot take (their path is a Date's, and their validator reads the value with getTime()), and which the compiler refuses as an attribute beside [formField] (NG8022). The bounds reach the field's min / max, so the calendar's walk stops at them, and the form grades the value: a day outside is minDay or maxDay.
PctDaytypestringA calendar day, written the way the platform already writes one: YYYY-MM-DD.
PctDayPartsinterfaceThe three fields of a day, as numbers. month is 1–12, the way a person says it.
pctDaysInMonthfunctionHow many days February has — the Gregorian rule, written once.
isPctDayfunctionWhether a string is a day this library will work with: the right shape and a day the calendar has. 2026-02-30 has the shape and is not a day, and the difference matters — a value read from a server, a query string or a localStorage entry is a string somebody else wrote.
pctDayfunctionBuilds a day from its three fields, normalising an overflow the way a person expects — month 13 is January of the next year, day 0 is the last day of the previous month. That is what makes pctAddMonths and pctAddDays one line each instead of three branches.
pctDayAsUtcfunctionThe day as the one Date anything outside this file is allowed to see: midnight UTC, which is what Intl.DateTimeFormat with timeZone: 'UTC' reads back as the same three fields it was built from. Handing out a local-time Date is the defect this whole module exists to refuse.
pctDayPartsfunctionThe three fields of a day. The caller has already established that it is one.
pctTodayfunctionToday, read from the LOCAL clock — the one read of local time in this file, and the only one that belongs: "what day is it" is a question about where the user is standing, and every other question here is arithmetic on a day that already has an answer.
pctAddDaysfunctionn days on (or back).
pctAddMonthsfunctionn months on (or back), clamped to the length of the month it lands in: 31 January plus one month is 28 February and not 3 March. The clamp is what a calendar's "next month" button means — a walk that changed the day of the month would move the cursor twice.
pctCompareDaysfunction-1, 0 or 1 — read from the three fields, though the shape invites a string compare and carried one for months.
pctWeekdayfunctionThe ISO weekday: Monday is 1, Sunday is 7 — the numbering Intl.Locale.prototype.getWeekInfo() answers in, so the two never need converting between them.
pctClampDayfunctionHeld inside the bounds, either of which may be absent — and undefined is what absent is, because that is how the FormUiControl contract's min / max spell an absent bound.
pctMonthGridfunctionThe grid of one month: whole weeks, starting on firstDayOfWeek, with the days of the neighbouring months that share those weeks. Six weeks always, and that is a decision rather than an oversight — a month spans four to six weeks depending on where it starts, and a panel that changed height between March and August would move the page under the pointer mid-walk.
PctDayFieldtype'day' | 'month' | 'year'Which of the three fields a position in the format holds.
PctDayLettersinterfaceThe letters a format hint is written with — one per field, from PCT_TEXTS.
pctFirstDayOfWeekfunctionWhich day the week starts on, 1 (Monday) … 7 (Sunday).
PctDayFormatinterfaceEverything about writing and reading a day in one language, built once per locale.
pctDayFormatfunctionThe day, weekday and month names of a locale, built once and shared.
PctWeekdayNameinterfaceThe two names a column header needs: the one the eye reads and the one a reader says.
pctWeekdayNamesfunctionThe seven column headings, rotated so that the first is firstDayOfWeek.
pctMonthCaptionfunctionThe month and year over the grid ("December 2026"), in the value's own calendar.
<pct-date label="Starts on" [(value)]="startsOn" />
Examples

Every state, running

Each example is the component itself, not a screenshot. Open the code under any of them.

Within bounds

min and max are days, and the calendar greys what falls outside; typing a day outside them marks the field invalid rather than silently moving it.

Within the next two weeks

Chosen: not yet

API

What it reads, and what it writes back

Read from the source at build time — the types, the defaults and the lines of documentation are the library's own.

unreleased marks 5 of them: on main, and not in @pacit/components@0.2.0 — the next release names the version.

Date field — a text control the library formats and parses per locale, with a calendar in a panel beside it.

Inputs

NameTypeWhat it does
valuetwo-waydefault nullPctDay | nullThe chosen day, or null when the field is empty.
disableddefault falsebooleanBlocks the control and greys it — the native disabled, so it leaves the tab order as well. With [formField] the directive writes it, as it writes every input of the FormUiControl contract.
readonlydefault falsebooleanKeeps the value: the input takes the native readonly and the calendar button is disabled with it, while the field stays focusable.
invaliddefault falsebooleanThe form's verdict; shown only once touched, so an empty form does not open red.
toucheddefault falsebooleanWhether the user has left the field once; with invalid it gates the error face.
requireddefault falsebooleanMarks the label with the required sign; with [formField] it follows the schema's required().
errorsdefault []readonly ValidationError.WithOptionalFieldTree[]The form's validation errors; the first one's message takes the hint's place once the field is touched.
warningsunreleaseddefault undefinedreadonly ValidationError.WithOptionalFieldTree[] | undefinedA verdict without a veto (0087): shown after the error and before the hint once the field is touched, in the warning tone, with aria-invalid untouched. Left unbound, the control reads what pctWarn() wrote for its own [formField]; bound — [] included — the list given stands.
namedefault ''stringThe native name — what a form submission calls the value.
mindefault undefinedunknownThe earliest and latest day. They belong to the FormUiControl contract, which spells an absent bound undefined, and a signal form gives them through pctDayBounds — the compiler refuses the attribute beside [formField].
maxdefault undefinedunknownThe latest day — as min, a bound on the calendar's walk and not a rewrite of what was typed.
dateDisableddefault nullPctDayPredicate | nullDays inside the bounds that still cannot be picked — weekends, holidays, taken slots.
labeldefault ''stringThe visible label, rendered by the control itself when it stands outside a pct-field; inside one, the field's label is the name.
hintdefault ''stringA line of help under the control, outside a pct-field; the first error message takes its place while the field is invalid and touched.
ariaLabeldefault ''stringThe accessible name of a field with no visible label — an INPUT rather than an aria-label on the tag, because the textbox sits inside this template and an ARIA name on the roleless host is ignored.
ariaLabelledbydefault ''stringAs ariaLabel, for a name that already stands somewhere on the page.
localedefault ''stringOverrides the application's LOCALE_ID for this field, as on [pctNumber].
firstDayOfWeekdefault 0numberWhich day the week starts on, 1 (Monday) … 7 (Sunday). Absent, the locale decides.
showFormatdefault truebooleanWhether the format hint stands in the field while it is empty (dd.mm.yyyy).
sizedefault config.defaultSizePctSizeHeight 28 / 36 / 44 px — the axis every field shares; from providePctConfig by default (req-api-config).
panelAligndefault 'start'PctDatePanelAlignWhich edge of the field the panel lines up with.

Outputs

NamePayloadWhen it fires
touchvoidEmitted on blur — lets the form mark the field as touched.

What it writes on the element

The hooks a stylesheet or a test can hold. Stable by contract — the parts inventory gate fails the build when one disappears.

AttributeBound toNote
classpct-date
data-pct-sizesize()
data-pct-openopen() ? "" : null
data-pct-invalidshowInvalid() ? "" : null
data-pct-warningshowWarning() ? "" : null
data-pct-disableddisabled() ? "" : null
data-pct-malformedmalformed() ? "" : nullWhat the control knows and the form cannot: there is text in the field and it is not a date. See malformed below.
data-pct-in-fieldinField ? "" : null
Styling

Parts to select, tokens to override

Two ways in, both stable. No class names to depend on.

Parts

In a test, PctDateHarness from @pacit/components/testing reads a part by the name below and a state by its attribute — the harness gate holds the names to the package.

PartSelectorWhat it is
caption[data-pct-part="caption"]the month and the year over the grid
day[data-pct-part="day"]one day cell
grid[data-pct-part="grid"]the grid of days
nav[data-pct-part="nav"]the previous-month and next-month buttons
week[data-pct-part="week"]one row of the grid
weekday[data-pct-part="weekday"]a heading over a column of days
control[data-pct-part="control"]the typed input
error[data-pct-part="error"]the message when the value is invalid
hint[data-pct-part="hint"]the hint under the input
label[data-pct-part="label"]the label of the typed input
panel[data-pct-part="panel"]the calendar, floating or inline
toggle[data-pct-part="toggle"]the button that opens the calendar
warning[data-pct-part="warning"]the message when the value is allowed and suspect — after the error, before the hint (0087)

Tokens

Every value the component reads, with the light and the dark default. Override a token at any scope and it cascades like any custom property.

TokenStyles
--pct-date-bg{pct.surface}#ffffff · #0f172aBackground of the date field.
--pct-date-bg-disabled{pct.surface-disabled}#f1f5f9 · #1e293bBackground of the date field while disabled.
--pct-date-border{pct.border-strong}#64748bEdge colour of the date field.
--pct-date-border-disabled{pct.border}#e2e8f0 · #334155Edge colour of the date field while disabled.
--pct-date-border-focus{pct.primary}#2563eb · #60a5faEdge colour of the date field when focused.
--pct-date-border-hover{pct.text-muted}#334155 · #e2e8f0Edge colour of the date field on hover.
--pct-date-border-invalid{pct.danger}#b91c1c · #f87171Edge colour of the date field when invalid.
--pct-date-border-warning{pct.warning}#b45309 · #fbbf24Edge colour of the date field under a warning (0087).
--pct-date-caption-font-size{pct.font.size.md}14pxFont size of the month caption.
--pct-date-caption-font-weight{pct.font.weight.medium}500Font weight of the month caption.
--pct-date-day-bg-hover{pct.surface-100}#f1f5f9 · #1e293bBackground of a day cell on hover.
--pct-date-day-bg-selected{pct.primary}#2563eb · #60a5faBackground of a day cell when selected.
--pct-date-day-border-today{pct.primary}#2563eb · #60a5faEdge colour of a day cell for today.
--pct-date-day-fg-disabled{pct.text-disabled}#64748bText colour of a day cell while disabled.
--pct-date-day-fg-muted{pct.text-muted}#334155 · #e2e8f0Text colour of a day outside the month shown.
--pct-date-day-fg-selected{pct.on-primary}#ffffff · #0f172aText colour of a day cell when selected.
--pct-date-day-font-size{pct.font.size.md}14pxFont size of a day cell.
--pct-date-day-font-size-lg{pct.font.size.lg}16pxFont size of a day cell at size lg.
--pct-date-day-font-size-sm{pct.font.size.sm}13pxFont size of a day cell at size sm.
--pct-date-day-font-weight-selected{pct.font.weight.medium}500Font weight of a day cell when selected.
--pct-date-day-radius{pct.radius.md}8pxCorner radius of a day cell.
--pct-date-day-size32pxSize of a day cell.
--pct-date-day-size-lg36pxSize of a day cell at size lg.
--pct-date-day-size-sm28pxSize of a day cell at size sm.
--pct-date-fg{pct.text}#0f172a · #ffffffText colour of the date field.
--pct-date-fg-disabled{pct.text-disabled}#64748bText colour of the date field while disabled.
--pct-date-fg-invalid{pct.danger}#b91c1c · #f87171Text colour of the date field when invalid.
--pct-date-fg-warning{pct.warning}#b45309 · #fbbf24Text colour of the warning line (0087).
--pct-date-font-size{pct.font.size.md}14pxFont size of the date field.
--pct-date-font-size-lg{pct.font.size.lg}16pxFont size of the date field at size lg.
--pct-date-font-size-sm{pct.font.size.sm}13pxFont size of the date field at size sm.
--pct-date-gap{pct.space.3}8pxGap between the label, the input and the hint.
--pct-date-hint-fg{pct.text-muted}#334155 · #e2e8f0Text colour of the hint.
--pct-date-icon-size16pxSize of the icon.
--pct-date-label-fg{pct.text}#0f172a · #ffffffText colour of the label.
--pct-date-nav-bg-hover{pct.surface-100}#f1f5f9 · #1e293bBackground of the month navigation on hover.
--pct-date-nav-radius{pct.radius.md}8pxCorner radius of the month navigation.
--pct-date-nav-size28pxSize of the month navigation.
--pct-date-padding-x{pct.space.4}12pxHorizontal padding of the date field.
--pct-date-padding-x-lg{pct.space.4}12pxHorizontal padding of the date field at size lg.
--pct-date-padding-x-sm{pct.space.3}8pxHorizontal padding of the date field at size sm.
--pct-date-padding-y{pct.space.3}8pxVertical padding of the date field.
--pct-date-padding-y-lg{pct.space.4}12pxVertical padding of the date field at size lg.
--pct-date-padding-y-sm{pct.space.2}4pxVertical padding of the date field at size sm.
--pct-date-panel-bg{pct.surface}#ffffff · #0f172aBackground of the panel.
--pct-date-panel-border{pct.border-strong}#64748bEdge colour of the panel.
--pct-date-panel-padding-x{pct.space.3}8pxHorizontal padding of the panel.
--pct-date-panel-padding-y{pct.space.3}8pxVertical padding of the panel.
--pct-date-panel-radius{pct.radius.md}8pxCorner radius of the panel.
--pct-date-panel-shadow{pct.shadow-panel}0 8px 24px rgba(15, 23, 42, 0.14) · 0 8px 24px rgba(0, 0, 0, 0.55)Shadow of the panel.
--pct-date-placeholder-fg{pct.text-muted}#334155 · #e2e8f0Text colour of the placeholder.
--pct-date-radius{pct.radius.md}8pxCorner radius of the date field.
--pct-date-target-min{pct.target.min}24pxTouch-target floor of the date field.
--pct-date-toggle-bg-hover{pct.surface-100}#f1f5f9 · #1e293bBackground of the calendar toggle on hover.
--pct-date-toggle-fg{pct.text-muted}#334155 · #e2e8f0Text colour of the calendar toggle.
--pct-date-toggle-radius{pct.radius.md}8pxCorner radius of the calendar toggle.
--pct-date-toggle-size20pxSize of the calendar toggle.
--pct-date-weekday-fg{pct.text-muted}#334155 · #e2e8f0Text colour of a weekday heading.
--pct-date-weekday-font-size{pct.font.size.sm}13pxFont size of a weekday heading.
--pct-date-weekday-font-weight{pct.font.weight.medium}500Font weight of a weekday heading.

Theming

A brand override in a few declarations. Scope it to a theme attribute, a page or one container — the component never notices.

[data-theme='brand'] {
  --pct-date-border-focus: #0f766e;
  --pct-date-day-bg-selected: #0f766e;
  --pct-date-day-fg-selected: #ffffff;
}
Type it, or pick it from the calendar
Accessibility

Measured, not assumed

Pattern: Date Picker Dialog — a plain textbox with a button beside it that opens a role="dialog" holding a role="grid". The textbox is deliberately not a combobox: a combobox promises a popup that helps complete what is being typed, and a calendar narrows nothing — it is a second road to the same value (0035 read backwards)

Keyboard

keyeffecttest
any texttyped into the field, parsed on commitapps/sandbox-e2e/src/date.spec.ts
Tabfield → calendar button → outnative
Enter on the buttonopens the panel, focus lands on the cursor's cellapps/sandbox-e2e/src/date.spec.ts
Escape in the panelcloses it, focus back to the fieldapps/sandbox-e2e/src/date.spec.ts
Tab out of the panelcloses it, focus spliced back onto the fieldapps/sandbox-e2e/src/date.spec.ts

The grid's own map is on PctCalendar. The field itself adds no key of its own: what a user types is text, and the platform's own editing keys are the whole of it.

What is measured, and what is not yet

Every line is a test in three engines or a gate on every commit. The gaps are listed the same way, each with the requirement that will close it.

MeasuredARIA APG pattern in the JSDoclibs/components/date/src/date.ts — the pattern is named and linked in the class comment
MeasuredKeyboard mapapps/sandbox-e2e/src/date.spec.ts — the panel's map in three engines; the field's editing keys are the platform's
Measuredaxe auditapps/sandbox-e2e/src/a11y.spec.ts — the /date view and /all, in both directions, and separately the open calendar with the field it belongs to, whole-page: the toggle's aria-expanded/aria-controls pointing into the overlay container and the roving cell that takes focus, a stage the derived list asked for (lesson-196) and the one panel the audit had never opened
MeasuredVisual screenshotapps/sandbox-e2e/src/visual.spec.ts — date-field, date-calendar and date-calendar-rtl
Measuredforced-colors: activeapps/sandbox-e2e/src/forced-colors.spec.ts — the chosen day takes the mode's own Highlight pair, today keeps a shape — chromium and firefox force box-shadow to none there and webkit leaves it, so the block writes both halves (lesson-119)
Measuredprefers-reduced-motiontools/check-styles.mjs point 9 — the day cell's transition takes --pct-motion-transition-duration, which the token build switches off
MeasuredTouch target ≥ 24×24 pxlibs/components/date/src/date.scss — the calendar button is max(--pct-date-toggle-size, --pct-date-target-min), a floor on the DRAWING and not a zone laid over it
MeasuredSize axisapps/sandbox/src/app/views/size/size-view.html — the /size view puts the field on the shared sm/md/lg axis beside a text field and a button
GapDensity axisnone — gap (req-token-density has not one token in the sources; the day cell is the number a density axis would move)
MeasuredRTLapps/sandbox-e2e/src/date.spec.ts — the grid mirrors as geometry AND the arrows mirror with it; plus apps/sandbox-e2e/src/rtl.spec.ts over the /date view
MeasuredSSR + hydrationapps/sandbox-e2e/src/hydration.spec.ts — the /date view is in SBX_ROUTES
MeasuredFormslibs/components/date/src/date.spec.ts › the forms that bind it — [formField], [formControl] and [(ngModel)], each from a non-empty value (lesson-26): written in the field's language (27/08/2026 in es-ES, 08/27/2026 in en-US), typed back into the model, a setValue and disable() reaching the input, and no warning; a form's reset — reset() keeping the value, reset(value) showing it, the form pristine: libs/components/date/src/date.spec.ts › the forms that bind it
MeasuredBounds from a signal formlibs/components/date/src/date.spec.ts › bounds from a signal form (pctDayBounds) — the bounds reach the field and the calendar walks inside them, a day outside is minDay / maxDay and a bound is inside, the message, a bound read off another field, two rules walked as their intersection in either order, days past the year 9999, and Angular's minDate() refused by the compiler; apps/sandbox-e2e/src/date.spec.ts › bounds from a signal form — the bounds reaching the calendar and a day typed outside reported, in three engines
MeasuredMessage announcedtools/check-aria.mjs (point 7, target check-aria) — the error part carries role="alert"
MeasuredDay arithmetic as lawslibs/components/date/src/day.property.spec.ts — the step invertible and composing, the weekday anchored on the platform's own epoch, a month's length read from Date's normalisation, an order agreeing with the millisecond difference, the grid six whole weeks containing its month, and the domain refused loudly at both ends. It found two defects no worked case had reached (lesson-185)
MeasuredParts in the inventorylibs/components/parts.snapshot.md, tools/check-parts.mjs (target check-parts)
MeasuredTokens + contrast.policy.jsonlibs/tokens/src/contrast.policy.json — 23 pairs for the field and the calendar together
MeasuredStrings through PCT_TEXTStools/check-texts.mjs — six keys, and the three letters are texts because they are words: dd.mm.yyyy reads as language, and a y in a Polish field means nothing
MeasuredSize budgetlibs/components/size.snapshot.md, tools/check-bundle.mjs (target check-bundle)
MeasuredNot a date, saidlibs/components/date/src/date.spec.ts › junk in the field — the sentence in the message line ahead of the form's, in the default wording and in the application's; apps/sandbox-e2e/src/date.spec.ts › keeps junk in the field and reports it — the same in three engines, in French, with aria-describedby naming it (0070)
MeasuredScreen-reader logRead 2026-09-28 from docs/acr/at/ — three readers, whole sandbox, and the act that opens the grid. The INPUT is answered as before: Start date · entry · 27/08/2026 · required. · Type it, or pick it from the calendar. (Orca), Day, edit, …, selected 27 slash 08 slash 2026 (NVDA), 27/08/2026 … Day … edit text (VoiceOver), and the toggle says it opens a dialog in all three. The grid is now read, and the cell's name is not: opening lands focus on the chosen day, NVDA says Choisir une date, dialog and stops, Orca gives Choisir une date · dialog · août 2026. · table with 7 rows 7 columns · jeudi column header 27 · row 6 column 4 — the column heading and the cell's text, not the whole date the cell carries as its accessible name — and VoiceOver was silent in the window and said 27 on the row after it. The calendar card reads the same rows
Measureddocs page/components/date on the published site — prerendered, the demo's own source is the code tab
Evidence

What the repository holds for this component

Read at build time from the tracked snapshots, not typed here.

80.15%mutation score218 of 272 mutants killed
20 × 3e2e cases × enginesplus the shared audits: axe, forced colors, preferences, size, hydration
26colour pairs measuredlight and dark, at every token build
3visual baselinesChromium, the sandbox's own pictures
10elements in the preview4 listeners · 1 render to settle · 4037 µs in jsdom, held exactly but for the clock

Decisions behind it

  • 0043A day is not an instant, and the field that takes one is text the application formats
  • 0093A bound of the field's own type reaches it through the platform's limit key, and the module ships the rule
  • 0035A filter is a question, not a value
  • 0031A panel's tab order belongs to its trigger
  • 0025A panel says whether it takes focus, and the rest of the focus layer waits for a consumer
  • 0009The number field on type="text"
  • 0005Signal forms without ControlValueAccessor
  • 0003Wrapper and control
  • lesson-196A list that decides what a gate looks at is a denominator nobody measures
  • lesson-119Forced-colors mode is a request, and each engine honours a different part of it
  • lesson-26FormField supplies an NgControl itself
  • lesson-185A day's string is not fixed-width, and two functions had assumed it was
  • lesson-20On a native element DefaultValueAccessor leads
  • lesson-117The forms interop writes null into a model typed number, and nothing says so

Known limitations

  • One day, not a range. A booking's two ends are [PctDay, PctDay], which by 0034 is a question about the tag rather than about an input. Two PctDates with min / max bound to each other is what a consumer composes today — in a signal form, pctDayBounds with a bound read off the other field.
  • No time. A day has none, by construction. A datetime control is a different value type and therefore a different control.
  • The Gregorian calendar only. Intl resolves fa-IR to the persian calendar and th-TH to the buddhist one, and the value is a Gregorian day — so the formatter is pinned and a user in Bangkok reads 01.12.2026 rather than 01.12.2569. Supporting the others means the value stops being one string, which Temporal will answer better than we can.
  • The sentence comes first. A control's own error stands ahead of the form's, always — there is no knob to let "required" win over "not a date" (0070).
  • An empty value written from outside does not take junk back. Junk leaves the value null; a form that then writes null again — ctrl.reset(), setValue(null) — writes what the model already holds, the model does not move, and the field keeps the text and the report. A date written from outside does take it back.
  • A two-digit year is a guess. 01.12.26 lands in [today − 80, today + 19], the convention every other date field a user has met uses. It is the only guess about intent this control makes, and the number is written down in locale.ts where it can be read.