350. A gutter strip is outside the clip, and its numbers are inside it
Date: 2026-09-17
Status
Accepted. Closes docs/gaps.md G43, a defect in
ADR-0331. Relies on
ADR-0272’s
ContainingBlock. text-input’s matching arithmetic is fixed by
ADR-0355.
Context
An application reported two things about text-area gutter=#true.
The strip did not reach the padding. With padding: 12px 16px, the filled
column started 12px down and 16px in, and a band of the field’s own background
showed above it and to its left. The insets were right: -padding on three
edges, which ContainingBlock shifts back to the border box. The clip cut the
strip. RenderTree.clipFor clips a box’s children to the box less its padding,
which is the content box, and a field is overflow: hidden so that scrolled text
stops at the content box. The strip was a child like the text, so the same clip
cut it by exactly the padding.
Changing the clip for every box is not the fix. Borders here are drawn inside the padding rather than laid out, so “inside the border” is not an edge the render tree knows, and every scroll view, field and golden is built on the clip as it is.
The numbers were said to drift after a wrap. The report was that with
--gb-gutter-gap other than 8 and a non-zero left padding, numbers sat one
visual line too high after the first soft wrap. That could not be reproduced
against this checkout. It was rendered with each combination the report named,
and the numbers and the text wrap at one width in every case. What the
reproduction did find is a real width error beside it:
TextAreaState.contentWidth() subtracted 2 * leftPadding. That is right for
every stylesheet the toolkit ships and wrong for padding: 12px 16px 12px 4px,
where the text wrapped 12px wider than its room and ran under the right padding.
visibleRows() did the same with 2 * topPadding.
Decision
The field draws two layers. A content layer is pinned to the content box and clips, and the strip is its sibling under a field that does not clip. The wrap subtracts each padding edge once.
- The content layer is an absolute box with a zero inset on all four edges.
ContainingBlockshifts each edge by the field’s padding, which lands the box on the content box. It has no padding of its own, so every part inside it (the washes, the text, the caret, the composition’s rules and the numbers) is placed in exactly the coordinates it was before. It isoverflow: hidden, which is the clip the field used to have. - The strip sits beside that layer and is inset from the border’s inner
edge: each inset is
borderWidth - padding. The field no longer clips it, and it would otherwise paint over the field’s 1px edge. Its two leading corners take the field’s radius less the border width, so a rounded field keeps its curve. Those corners override any radius a stylesheet givestext-area-gutter, which has no shipped rule for one. AreaPaddingcarries all four edges fromrenderto the state, replacing the two-edge record. The wrap subtractsleft + rightand the visible height subtractstop + bottom.
Consequences
text-area-gutter { background: … }fills the column from border to border, and the stopgapbackground: transparentin the reporting application can go.- A field with asymmetric padding wraps inside its room.
TextAreaGutterStripTestchecks the right padding for ink across five padding and gap combinations, and checks that scrolled text still stops at the top padding. gallery-formsandgallery-forms-lightchanged where the numbered area’s strip now fills the padding, and nowhere else.- The box tree under a
text-areais one level deeper. Tests that walk it look in the last child, asTextAreaGutterTestdoes now. text-inputstill computes its room aswidth - 2 * leftPadding. Its padding is symmetric in every shipped stylesheet, and the same asymmetry would scroll the text 12px late rather than wrap it wrong. It is filed inTODO.mdrather than widened into this change.- If the drift in the report came from a published snapshot older than this checkout, this ADR does not close it. The reporter’s own rendering test is the check.