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.
A textarea as tall as its text, growing and shrinking as the reader types.
@pacit/components/fieldtextarea[pctText][pctAutosize]No APG pattern applies, and none is invented — the reasoning is under Accessibility ↓
import { Component } from '@angular/core';
import { PctAutosize, PctField, PctText } from '@pacit/components/field';
/** `pctAutosize` grows the box with the writing — no scrollbar inside a scrollbar. */
@Component({
selector: 'demo-textarea',
imports: [PctAutosize, PctField, PctText],
styles: ':host { display: block; max-inline-size: 24rem; }',
template: `
<pct-field label="Release notes" hint="It grows as you write">
<textarea pctText pctAutosize rows="2"></textarea>
</pct-field>
`,
})
export class TextareaDemo {}textarea.demo.ts — the same file runs here and is highlighted under Code
One import per component, and the shortest possible use — the package itself is installed once, on the start page.
import { PctAutosize, PctField, PctText } from '@pacit/components/field';| Exported | What it is |
|---|---|
PctFieldcomponentpct-field | The chrome of a form field: label, hint, warning, error message, the required marker and the [pctPrefix] / [pctSuffix] slots inside the field. |
PctFieldSizetypePctSize | The 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>Each example is the component itself, not a screenshot. Open the code under any of them.
rows is the height it starts at; maxRows is where it stops growing and starts scrolling instead — a comment box, not a document.
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.
| Name | Type | What it does |
|---|---|---|
rowsdefault 2 | number | The 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 0 | number | The 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. |
(click), (focus), the form's submit — are the platform's, and two-way models above carry the rest. The hooks a stylesheet or a test can hold. Stable by contract — the parts inventory gate fails the build when one disappears.
| Attribute | Bound to | Note |
|---|---|---|
data-pct-autosize | | |
rows | rows() | 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]property | rows() | |
data-pct-capped | maxRows() > 0 ? "" : null | |
[style.max-block-size]property | maxRows() > 0 ? maxRows() + "lh" : null | lh is the element's own line box — the arithmetic stays in the engine, in both roads. |
Two ways in, both stable. No class names to depend on.
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.
| Part | Selector | What 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 |
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.
| Token | Styles |
|---|---|
--pct-field-affix-fg{pct.text-muted}#334155 · #e2e8f0 | Text colour of an affix. |
--pct-field-bg{pct.surface}#ffffff · #0f172a | Background of the field. |
--pct-field-bg-disabled{pct.surface-disabled}#f1f5f9 · #1e293b | Background of the field while disabled. |
--pct-field-border{pct.border-strong}#64748b | Edge colour of the field. |
--pct-field-border-disabled{pct.border}#e2e8f0 · #334155 | Edge colour of the field while disabled. |
--pct-field-border-focus{pct.primary}#2563eb · #60a5fa | Edge colour of the field when focused. |
--pct-field-border-hover{pct.text-muted}#334155 · #e2e8f0 | Edge colour of the field on hover. |
--pct-field-border-invalid{pct.danger}#b91c1c · #f87171 | Edge colour of the field when invalid. |
--pct-field-border-warning{pct.warning}#b45309 · #fbbf24 | Edge colour of the field under a warning (0087). |
--pct-field-fg{pct.text}#0f172a · #ffffff | Text colour of the field. |
--pct-field-fg-disabled{pct.text-disabled}#64748b | Text colour of the field while disabled. |
--pct-field-fg-invalid{pct.danger}#b91c1c · #f87171 | Text colour of the field when invalid. |
--pct-field-fg-warning{pct.warning}#b45309 · #fbbf24 | Text colour of the warning line (0087). |
--pct-field-font-size{pct.font.size.md}14px | Font size of the field. |
--pct-field-font-size-lg{pct.font.size.lg}16px | Font size of the field at size lg. |
--pct-field-font-size-sm{pct.font.size.sm}13px | Font size of the field at size sm. |
--pct-field-gap{pct.space.3}8px | Gap between the field's label, control and hint. |
--pct-field-height{pct.control.height.md}36px | Height of the field. |
--pct-field-height-lg{pct.control.height.lg}44px | Height of the field at size lg. |
--pct-field-height-sm{pct.control.height.sm}28px | Height of the field at size sm. |
--pct-field-hint-fg{pct.text-muted}#334155 · #e2e8f0 | Text colour of the hint. |
--pct-field-label-fg{pct.text}#0f172a · #ffffff | Text colour of the label. |
--pct-field-label-gap{pct.space.2}4px | Gap inside the label. |
--pct-field-padding-x{pct.space.4}12px | Horizontal padding of the field. |
--pct-field-padding-x-lg{pct.space.5}16px | Horizontal padding of the field at size lg. |
--pct-field-padding-x-sm{pct.space.3}8px | Horizontal padding of the field at size sm. |
--pct-field-placeholder-fg{pct.text-muted}#334155 · #e2e8f0 | Text colour of the placeholder. |
--pct-field-radius{pct.radius.md}8px | Corner radius of the field. |
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;
} 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
None of its own — fully native (req-api-platform).
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.
apps/sandbox-e2e/src/a11y.spec.ts — the /textarea view is in SBX_ROUTES, so the audit and the RTL audit both take itforced-colors: activeinherited from PctText — the directive paints nothingprefers-reduced-motionnot applicable — the height is not animatedapps/sandbox-e2e/src/field-hitarea.spec.ts — guaranteed by the wrapper, and autosize only ever makes the target tallerapps/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 ownapps/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 logicalapps/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)libs/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 fromapps/sandbox-e2e/src/textarea.spec.ts — one set of assertions over two implementations, in three engines, within a pixel (lesson-111)apps/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)libs/components/parts.snapshot.md — the directive exposes no parts of its owncontrast.policy.jsonnot applicable — no colour of its ownPCT_TEXTStools/check-texts.mjs — one dev-mode console.warn (a ceiling under a floor) and no user-facing stringlibs/components/size.snapshot.md, tools/check-bundle.mjs (target check-bundle)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/components/textarea on the published site — prerendered, the demo's own source is the code tabRead at build time from the tracked snapshots, not typed here.
NgControl.valueChanges is null for the whole of a sibling directive's constructorresize: 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>.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.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).scrollHeight is an integer. Nothing in a layout depends on it, but a gate comparing the roads compares with a tolerance.[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.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.