ComponentsText & numbersTextarea
On this page
Text & numbers

Textarea released

A textarea as tall as its text, growing and shrinking as the reader types.

  • Import@pacit/components/field
  • Selectortextarea[pctText][pctAutosize]

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

textarea.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 { PctAutosize, 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="Release notes">
  <textarea pctText pctAutosize rows="2"></textarea>
</pct-field>
Examples

Every state, running

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

It grows, up to a point

rows is the height it starts at; maxRows is where it stops growing and starts scrolling instead — a comment box, not a document.

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.

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.

Inputs

NameTypeWhat it does
rowsdefault 2numberThe floor, in lines — the platform's own rows, kept as the platform spells it (req-api-platform) and read here only because field-sizing: content discards it: measured, an empty rows="2" box under the property is 34 px, one line, against the 58 a plain one is.
maxRowsdefault 0numberThe ceiling, in lines; 0 is no ceiling. This one has no native spelling, so it is an input. Past it the box scrolls rather than clipping — measured in three engines, on both roads.

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
data-pct-autosize
rowsrows()Written back to the DOM because the input above has taken it: a bound [rows] is consumed by a directive input and never reaches the element, and the measured road takes its floor from the element's own rows sizing. Both roads read one number.
[style.--_pct-text-rows]propertyrows()
data-pct-cappedmaxRows() > 0 ? "" : null
[style.max-block-size]propertymaxRows() > 0 ? maxRows() + "lh" : nulllh is the element's own line box — the arithmetic stays in the engine, in both roads.
Styling

Parts to select, tokens to override

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

Parts

In a test, PctTextHarness, PctAutosizeHarness 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 APG has no pattern for a height. The control is a native <textarea> and stays one, so placeholder, maxlength, spellcheck, wrap and the mobile keyboard mode are the platform's and untouched

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.

n/aARIA APG pattern in the JSDocnot applicable — a height is not a pattern; the class comment names the decision instead
n/aKeyboard mapnot applicable — native
Measuredaxe auditapps/sandbox-e2e/src/a11y.spec.ts — the /textarea view is in SBX_ROUTES, so the audit and the RTL audit both take it
GapVisual screenshotnone — gap. Deliberate for now: the subject is a height that changes with the value, and a baseline pins one value
Measuredforced-colors: activeinherited from PctText — the directive paints nothing
n/aprefers-reduced-motionnot applicable — the height is not animated
MeasuredTouch targetapps/sandbox-e2e/src/field-hitarea.spec.ts — guaranteed by the wrapper, and autosize only ever makes the target taller
MeasuredSize axisapps/sandbox-e2e/src/size.spec.ts — size belongs to the wrapper; the floor is rows line boxes, so it follows the field's font size on its own
GapDensity axisnone — gap
MeasuredRTLapps/sandbox-e2e/src/rtl.spec.ts — the /textarea view is in SBX_ROUTES. The feature is a block size, and min-block-size / max-block-size are logical
MeasuredSSR + hydrationapps/sandbox-e2e/src/hydration.spec.ts — and the height at the first paint is a measured difference between the roads: right with no script where the property exists, the floor until hydration where it does not (0041)
MeasuredFormslibs/components/field/src/autosize.spec.ts — signal forms through PctText's model, and [formControl] through the form's own valueChanges, which is the case lesson-114 was written from
MeasuredTwo roads agreeapps/sandbox-e2e/src/textarea.spec.ts — one set of assertions over two implementations, in three engines, within a pixel (lesson-111)
MeasuredThe fallback is forcedapps/sandbox-e2e/src/textarea.spec.ts — every case on both roads in every engine, the measured one by an init script that takes field-sizing away from the script and the sheet; a case per road says which road the box is on, and every case ends with no error event in the page (0084)
MeasuredParts in the inventorylibs/components/parts.snapshot.md — the directive exposes no parts of its own
n/aTokens + contrast.policy.jsonnot applicable — no colour of its own
MeasuredStrings through PCT_TEXTStools/check-texts.mjs — one dev-mode console.warn (a ceiling under a floor) and no user-facing string
MeasuredSize budgetlibs/components/size.snapshot.md, tools/check-bundle.mjs (target check-bundle)
MeasuredScreen-reader logRead 2026-09-16 from docs/acr/at/ — three readers, whole sandbox. All three announce the label, the value and the hint; only NVDA says it is multi-line: A plain textarea, for comparison, edit, multi line, This one keeps its two lines and scrolls., blank. VoiceOver names the role text entry area, Orca says entry, and neither distinguishes it from a single-line field. Nothing in the logs separates the autosizing box from the plain one, which is correct — the height is not a promise to a reader
Measureddocs page/components/textarea 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.

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

Decisions behind it

  • 0041A height the platform computes, and a measurement only where it does not
  • 0034Multiplicity is a type, so it is a tag
  • 0003Wrapper and control
  • 0004Height stated outright, not derived from padding
  • lesson-114NgControl.valueChanges is null for the whole of a sibling directive's constructor
  • lesson-111Two engines lay out on two grids, so a measured value is compared with a tolerance and never rounded

Known limitations

  • No manual resize. resize: none under autosize: a drag handle is a second author of the height and this feature is the first. A consumer who wants the handle wants a plain <textarea pctText>.
  • Without field-sizing, a server-rendered value is the floor until hydration. The measured road cannot run before there is a layout, and this is stated rather than hidden — where field-sizing exists the first paint is already right.
  • On firefox 112–119, maxRows has no effect. Those versions have no lh unit, so the ceiling and the sheet's floor are dropped; the box still follows its text by measurement, and rows falls back to the platform's own sizing (0084).
  • The two roads round differently, by up to a pixel: scrollHeight is an integer. Nothing in a layout depends on it, but a gate comparing the roads compares with a tolerance.
  • A bound [rows] is read by the directive, which writes it back to the element so both roads see one number. A consumer setting rows through the DOM instead of the binding is outside the contract.
  • No character counter. pctMessageAux already holds one and belongs to the chrome (field.md), so a counter here would be a second place to put the same thing.