Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

124. A pinned affix is revealed by its hole

Date: 2026-08-19

Status

Accepted. Repairs the meeting point of ADR-0119 (affix) and ADR-0120 (scrollIntoView), both of which were correct alone.

Context

Reported as “the buttons for affix stop working after a few clicks”. They were never working; the first press happened to be a scroll forwards, which any measurement would have got right.

The showcase’s jump buttons ask a section to scroll into view. ADR-0120’s rule is that the thing wanting to be seen measures itself, so the section’s header implemented Located, was handed its own rectangle and the viewport’s, and passed both to ScrollController.reveal.

That header is inside an affix. Which means that the moment its section starts scrolling away, the header is pinned to the viewport’s edge — it is sitting at the top of the visible area, by design, permanently. A reveal measured against it therefore concludes the section is already in view and scrolls nowhere, no matter how far away the section actually is.

Two rules, each right, composing into something wrong:

  • affix: your content stops at the viewport’s edge.
  • scrollIntoView: the thing that wants to be seen measures itself.

Follow both and a sticky header can never ask to be scrolled to, because by its own account it has already arrived.

Decision

An affix hands out its hole, and the hole is what a reveal measures.

Affix.revealedBy(listener) gives a caller the outer node’s rectangle and the rectangle that clips it, once a frame. The outer node is the hole §1 already requires — the same-sized gap left behind so nothing jumps — and it travels with the document precisely because it never moves itself. It is the only rectangle in the widget that still means “where this section is”.

The listener is a door, not a policy. Affix does no scrolling and holds no controller: it forwards two rectangles, and the caller decides whether the section wants showing and what to do about it. That keeps the widget generic and puts the decision where the reason lives, which is the same split Tab uses for the identical job.

Null by default, so exactly one affix per build is measured. A list of forty sections has forty affixes and at most one of them is being revealed.

Why not fix it inside scrollIntoView

Because there is nothing there to fix. The controller is handed two rectangles and does arithmetic on them; both were accurate. The mistake was upstream, in choosing which node to measure, and that choice can only be made by something that knows the widget is an affix — which the controller deliberately does not.

Why not have affix reveal itself

It would need a ScrollController, and then a rule for what “wants to be revealed” means, and then a way for a caller to say when. All of that is the caller’s already: the showcase holds one flag and clears it when the reveal lands. A widget that owned the policy would own a worse version of it.

Consequences

ADR-0120’s rule stands with a caveat worth stating plainly: the thing that wants to be seen measures itself, unless something is deliberately holding it somewhere. affix is the only widget in the catalog that does that today. Any future one — a docked panel, a frozen table column — will have the same problem and now has the same answer.

The showcase’s SectionHeader went back to being a plain node, which is the small proof that the door is in the right place: the widget that had to know about geometry no longer does.

None of the eleven AffixTest cases could have caught this, and neither could the four in ScrollingScreenTest as first written: the failing sequence is scroll away, then ask to come back, and every test asked to go somewhere new. The test that finds it presses two buttons alternately, four times — which is what the person who reported it did.