ComponentsText & numbersField
On this page
Text & numbers

Field released

The wrapper around a form control — its label, its hint, its error, and the wiring between the three.

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

No APG pattern applies, and none is invented — the reasoning is under Accessibility ↓

field.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 { PctField, PctText } from '@pacit/components/field';
ExportedWhat it is
PctFieldcomponentpct-fieldThe chrome of a form field: label, hint, warning, error message, the required marker and the [pctPrefix] / [pctSuffix] slots inside the field.
PctFieldSizetypePctSizeThe size of a field: the library's own scale.
PctTextcomponentinput[pctText], textarea[pctText]Text field: a directive on a native <input>. The input is not wrapped in a component of ours, so type, browser autofill, mobile keyboard modes and the whole semantics are kept without an abstraction in between (req-api-platform).
PctAutosizedirectivetextarea[pctText][pctAutosize]A <textarea> as tall as what is written in it — the ARIA APG has no pattern for this, because it is not a behaviour but a height.
PctNumbercomponentinput[pctNumber]Number field: a component on a native <input type="text"> with the spinbutton role, a value of type number | null and locale-aware formatting.
PctAffixFittype'inset' | 'fill'How a decoration sits in its slot:
PctPrefixdirective[pctPrefix]A decoration before the control, inside the field border (a currency unit, an icon). Decorative content should be hidden from the screen reader (aria-hidden), or have an accessible name of its own if it is interactive.
PctSuffixdirective[pctSuffix]A decoration after the control, inside the field border (a clear button, say).
PctLabelAuxdirective[pctLabelAux]A label add-on — content in the label row, aligned to the end (an "i" icon with a hint about the field, a help link). It lies outside the field border, so it does not interfere with the control's touch area. If it is interactive (a button, a link) it needs an accessible name of its own.
PctMessageAuxdirective[pctMessageAux]A message-line add-on — content in the row below the field, aligned to the end (a character counter, say). It shares the row with the hint or the error: the chrome shows only one of those messages below the field, and this slot stands beside it whichever one is lit.
<pct-field label="E-mail" hint="A work address">
  <input pctText type="email" />
</pct-field>
Examples

Every state, running

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

Around the control

pctPrefix and pctSuffix sit inside the row, beside the control; pctLabelAux and pctMessageAux sit beside the label and the message — a counter, a link, a unit.

€/ month
Rules
@
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.

The chrome of a form field: label, hint, warning, error message, the required marker and the [pctPrefix] / [pctSuffix] slots inside the field.

Inputs

NameTypeWhat it does
labeldefault ''stringThe visible <label> of the row, wired to the control by for or by aria-labelledby — whichever the control's labelStrategy asks for.
hintdefault ''stringA line of help under the row; an error or a warning takes its place while one shows (one message at a time, req-api-message).
sizedefault config.defaultSizePctFieldSizeThe field size; taken from the global configuration by default (req-api-config). It concerns the field row — the height here equals that of a button of the same size, because both take it from the --pct-control-height-* token (req-api-size).
requireddefault falsebooleanRequiredness can be given outright when the control does not report it.

Outputs

None of its own. The native events — (click), (focus), the form's submit — are the platform's, and two-way models above carry the rest.

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-field
data-pct-sizesize()
data-pct-appearanceappearance()
data-pct-cursorcursor()
data-pct-invalidshowInvalid() ? "" : null
data-pct-warningshowWarning() ? "" : null
data-pct-disableddisabled() ? "" : null
Styling

Parts to select, tokens to override

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

Parts

In a test, PctFieldHarness, PctPrefixHarness, PctSuffixHarness, PctLabelAuxHarness, PctMessageAuxHarness from @pacit/components/testing read a part by the name below and a state by its attribute — the harness gate holds the names to the package.

PartSelectorWhat it is
field-control[data-pct-part="field-control"]the wrapped control itself
field-error[data-pct-part="field-error"]the message when the control is invalid
field-footer[data-pct-part="field-footer"]the row under the control: the hint, the warning or the error
field-header[data-pct-part="field-header"]the row above the control: the label and what stands beside it
field-hint[data-pct-part="field-hint"]the hint, read as the description of the control
field-label[data-pct-part="field-label"]the label, tied to the control by id
field-label-aux[data-pct-part="field-label-aux"]the slot beside the label — a counter, a link
field-message-aux[data-pct-part="field-message-aux"]the slot beside the hint, the warning or the error
field-prefix[data-pct-part="field-prefix"]what sits before the control inside the row
field-row[data-pct-part="field-row"]the control's row, with its prefix and suffix
field-suffix[data-pct-part="field-suffix"]what sits after the control inside the row
field-warning[data-pct-part="field-warning"]the message when the value is allowed and suspect — after the error, before the hint, in the warning tone with a glyph and a hidden word (0087)
field-label-aux-item[data-pct-part="field-label-aux-item"]one projected item inside the label slot
field-message-aux-item[data-pct-part="field-message-aux-item"]one projected item inside the message slot
field-prefix-item[data-pct-part="field-prefix-item"]one projected item inside the prefix
field-suffix-item[data-pct-part="field-suffix-item"]one projected item inside the suffix

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-field-affix-fg{pct.text-muted}#334155 · #e2e8f0Text colour of an affix.
--pct-field-bg{pct.surface}#ffffff · #0f172aBackground of the field.
--pct-field-bg-disabled{pct.surface-disabled}#f1f5f9 · #1e293bBackground of the field while disabled.
--pct-field-border{pct.border-strong}#64748bEdge colour of the field.
--pct-field-border-disabled{pct.border}#e2e8f0 · #334155Edge colour of the field while disabled.
--pct-field-border-focus{pct.primary}#2563eb · #60a5faEdge colour of the field when focused.
--pct-field-border-hover{pct.text-muted}#334155 · #e2e8f0Edge colour of the field on hover.
--pct-field-border-invalid{pct.danger}#b91c1c · #f87171Edge colour of the field when invalid.
--pct-field-border-warning{pct.warning}#b45309 · #fbbf24Edge colour of the field under a warning (0087).
--pct-field-fg{pct.text}#0f172a · #ffffffText colour of the field.
--pct-field-fg-disabled{pct.text-disabled}#64748bText colour of the field while disabled.
--pct-field-fg-invalid{pct.danger}#b91c1c · #f87171Text colour of the field when invalid.
--pct-field-fg-warning{pct.warning}#b45309 · #fbbf24Text colour of the warning line (0087).
--pct-field-font-size{pct.font.size.md}14pxFont size of the field.
--pct-field-font-size-lg{pct.font.size.lg}16pxFont size of the field at size lg.
--pct-field-font-size-sm{pct.font.size.sm}13pxFont size of the field at size sm.
--pct-field-gap{pct.space.3}8pxGap between the field's label, control and hint.
--pct-field-height{pct.control.height.md}36pxHeight of the field.
--pct-field-height-lg{pct.control.height.lg}44pxHeight of the field at size lg.
--pct-field-height-sm{pct.control.height.sm}28pxHeight of the field at size sm.
--pct-field-hint-fg{pct.text-muted}#334155 · #e2e8f0Text colour of the hint.
--pct-field-label-fg{pct.text}#0f172a · #ffffffText colour of the label.
--pct-field-label-gap{pct.space.2}4pxGap inside the label.
--pct-field-padding-x{pct.space.4}12pxHorizontal padding of the field.
--pct-field-padding-x-lg{pct.space.5}16pxHorizontal padding of the field at size lg.
--pct-field-padding-x-sm{pct.space.3}8pxHorizontal padding of the field at size sm.
--pct-field-placeholder-fg{pct.text-muted}#334155 · #e2e8f0Text colour of the placeholder.
--pct-field-radius{pct.radius.md}8pxCorner radius of the field.

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-field-border-focus: #0f766e;
  --pct-field-bg: #f0fdfa;
  --pct-field-fg: #134e4a;
}
Accessibility

Measured, not assumed

Pattern: none — the wrapper has no role of its own: it supplies the label and the descriptions, and the control inside carries the role

Keyboard

None of its own — the wrapper is not focusable. A click on the row's background is forwarded to the control: focus() on mousedown, activate() on click.

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.

GapARIA APG pattern in the JSDocnone — gap
n/aKeyboard mapnot applicable
Measuredaxe auditapps/sandbox-e2e/src/a11y.spec.ts (the /field view, plus the validation error state)
MeasuredVisual screenshotapps/sandbox-e2e/src/visual.spec.ts
Measuredforced-colors: activeapps/sandbox-e2e/src/forced-colors.spec.ts; the mapping for the whole library is written down in field.scss, and tools/check-styles.mjs (point 7) makes the bare variant say in the mode itself that it has neither surface nor ring — until now the base rules decided that, and only because they are more specific (lesson-70)
Measuredprefers-reduced-motionapps/sandbox-e2e/src/preferences.spec.ts
MeasuredTouch target ≥ 24×24 pxapps/sandbox-e2e/src/field-hitarea.spec.ts — a cursor map over a grid of points, not a measurement of one element
MeasuredSize axisapps/sandbox-e2e/src/size.spec.ts — the field row equals a button of the same size
GapDensity axisnone — gap
MeasuredRTLapps/sandbox-e2e/src/rtl.spec.ts — the affix measured on the other side of the control as GEOMETRY in both directions, which that file names its own reference control; plus the field-affixes-rtl and field-aux-slots-rtl baselines in apps/sandbox-e2e/src/visual.spec.ts, and tools/check-styles.mjs (point 5, req-token-logical) over the stylesheet
MeasuredSSR + hydrationapps/sandbox-e2e/src/hydration.spec.ts
MeasuredFormslibs/components/field/src/field-controls.spec.ts — the wrapper is tested with every control
MeasuredParts in the inventorylibs/components/parts.snapshot.md, tools/check-parts.mjs (target check-parts). The card has under-counted twice: 11 parts against a requirement listing 7, then 15 against a card listing 11 — four slot parts sit in the host blocks of directives. Both times noticed only on counting
MeasuredTokens + contrast.policy.jsonlibs/tokens/src/contrast.policy.json
MeasuredStrings through PCT_TEXTStools/check-texts.mjs — every string comes from an input
MeasuredSize budgetlibs/components/size.snapshot.md — the ./field row
MeasuredScreen-reader logRead 2026-09-16 from docs/acr/at/ — three readers, whole sandbox. The whole routed set arrives in one announcement: Orca gives E-mail · entry · john@example.com · required. · invalid entry. · A work address. — label, role, value, required, invalid, hint — and NVDA gives A short description, edit, has auto complete, Briefly — write at least 10 characters, A few words about yourself, blank, VoiceOver A short description Briefly — write at least 10 characters edit text. The label auxiliary is its own stop and announces as a button in all three, which is what it is. The pass dispatched on 2026-10-08 after the warning landed (run 37733848017) read the amount and the phone with label, role and hint in NVDA and VoiceOver, the amount with its value, and neither reader heard the status line at the stop after it appeared, nor the phone's at the next; that silence is an open finding in the plan, read again by the weekly pass
Measureddocs page/components/field 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.

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

Decisions behind it

  • 0003Wrapper and control
  • 0004Height stated outright, not derived from padding
  • 0006The anchor and inheritance in an overlay
  • lesson-70A media query adds no specificity, and the browser it is written for hides that
  • lesson-68A warning about a second one needs to be told when the first one left

Known limitations

  • Two controls in one chrome are reported, not repaired. The last to register wins, as before — the chrome cannot know which of the two the label was written for — but it is no longer quiet about it: a dev-mode message names both control ids (lesson-68). Registration is a pair (attach / detach), so a control merely replaced inside an @if is not mistaken for a second one, and a control that leaves takes its state out of the chrome with it.
  • An inset button has to be one step smaller than the field. At size sm there is no step below, so the button fills the height there and pushes the row out by the thickness of the frame. That is a corollary of 0004, not a fitting defect.