ComponentsText & numbersText
On this page
Text & numbers

Text released

A text field on the platform's own <input> element, wired to the label and the error around it.

  • Import@pacit/components/field
  • Selectorinput[pctText]

Semantics come from the native <input> — no role and no keyboard handling of ours

text.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 { PctText, PctField } 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="Workspace name">
  <input pctText />
</pct-field>
Examples

Every state, running

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

Read-only and disabled are two states

Read-only keeps the value in the tab order and lets it be copied; disabled takes it out. Both are the input's native attributes, so the platform announces them itself.

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 1 of them: on main, and not in @pacit/components@0.2.0 — the next release names the version.

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).

Inputs

NameTypeWhat it does
valuetwo-waydefault ''stringThe value — a required field of the FormValueControl contract.
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 from being edited while the field stays focusable and readable — the native readonly.
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 falsebooleanThe native required; a pct-field around the control reads it to mark the label.
errorsdefault []readonly ValidationError.WithOptionalFieldTree[]The form's validation errors; a pct-field around the control shows the first one's message in place of the hint 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.

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-text
idpropertycontrolId
disabledpropertydisabled()
readOnlypropertyreadonly()
namename() || null
requiredrequired() || null
aria-invalidshowInvalid() ? "true" : null
aria-describedbydescribedBy()
Styling

Parts to select, tokens to override

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

Parts

In a test, PctTextHarness 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
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: a native <input> — type, the browser's autofill and the mobile keyboard modes are all preserved

Keyboard

None of its own — fully native (req-api-platform).

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 — native
Measuredaxe auditapps/sandbox-e2e/src/a11y.spec.ts (the /text view)
MeasuredVisual screenshotapps/sandbox-e2e/src/visual.spec.ts
Measuredforced-colors: activeapps/sandbox-e2e/src/forced-colors.spec.ts
Measuredprefers-reduced-motionapps/sandbox-e2e/src/preferences.spec.ts
MeasuredTouch targetapps/sandbox-e2e/src/field-hitarea.spec.ts (guaranteed by the wrapper)
MeasuredSize axisapps/sandbox-e2e/src/size.spec.ts — size belongs to the wrapper
GapDensity axisnone — gap
MeasuredRTLtools/check-styles.mjs (point 5, req-token-logical) over the stylesheet, plus the text-types-rtl baseline in apps/sandbox-e2e/src/visual.spec.ts
MeasuredSSR + hydrationapps/sandbox-e2e/src/hydration.spec.ts
MeasuredFormslibs/components/field/src/field-controls.spec.ts, apps/sandbox-e2e/src/forms.spec.ts — signal forms, [formControl] and [(ngModel)], each starting from a non-empty value; a form's reset — reset() keeping the value, reset(value) showing it, the form pristine: libs/components/field/src/field.spec.ts › signal forms
MeasuredParts in the inventorylibs/components/parts.snapshot.md, tools/check-parts.mjs (target check-parts) — the component exposes no parts of its own; the field entrypoint's inventory covers it in the same row
MeasuredTokens + contrast.policy.jsonlibs/tokens/src/contrast.policy.json
MeasuredStrings through PCT_TEXTStools/check-texts.mjs — no strings of its own
MeasuredSize budgetlibs/components/size.snapshot.md — the ./field row, the entrypoint the text shares
MeasuredScreen-reader logRead 2026-09-16 from docs/acr/at/ — three readers, whole sandbox, and the input type reaches the announcement: VoiceOver names it outright (E-mail john@example.com email field, Search… search text field, Password secure text field), NVDA gives the behaviour instead (E-mail, edit, has auto complete, john at example dot com, Password, edit, protected), Orca sits between (Password · password text., Search · entry · Search…). A password value is spoken by none of the three, which is the one thing here that had to be true
Measureddocs page/components/text 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.

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

Decisions behind it

  • 0003Wrapper and control
  • 0005Signal forms without ControlValueAccessor
  • lesson-20On a native element DefaultValueAccessor leads
  • lesson-26FormField supplies an NgControl itself

Known limitations

  • Coexistence with DefaultValueAccessor rests on a heuristic. The control detects an NgControl without a FormField and then gives up ownership of the value. The condition has already been too wide once and rendered an empty field against a non-empty model (lesson-20, lesson-26) — it is the most fragile part of this control.
  • No textarea — autosize is a separate component, not built yet.