Skip to content

Review 3935

Cindy Zhang edited this page Aug 27, 2026 · 2 revisions

Review 3935 — isSticky / stickyOffset on Stack family

PR

#3935 feat(core): add isSticky/stickyOffset props to Stack family by jiunshinn (bucket: contributor)

HEAD REVIEWED

f4408f06200d3e447011548f6d7f3f5e47647ce6

REFRESH: merged current main e6507a95fb13cae83f94e2d4b58009eaf584f1c6 in a normal merge commit. The only textual conflict was append-only drift in Stack.test.tsx; the resolution kept both main's padding tests and the PR's sticky tests. No author logic was changed.

VERSIONS

LOOP VERSION: 1.6.0 AUDIT RUBRIC: 1.13

LANE

LANE: full WHY: new permanent public API, intended layout behavior, a 44-day refresh conflict, and multiple real-container checks.

PROBLEM

WHY 1: product-detail, docs-sidebar, and filter-rail layouts need one column to remain visible while a taller neighbour scrolls. WHY 2: builders currently leave the design system and write three custom declarations for every instance. WHY 3: repeated custom layout CSS makes templates harder to theme and prevents the layout primitives from expressing a common relationship. USER-FACING PROBLEM: a builder composing a two-column product or navigation layout cannot express the sticky rail through Astryx layout APIs. PROBLEM SEVERITY: missing capability — the demonstrated product-detail template requires custom CSS.

VERDICT: clear

SOLUTION

A Stack can opt into sticky positioning and choose a token-based block-start offset. The responsibility lives on Stack and is inherited by HStack/VStack. The product-detail template then replaces its three-declaration workaround with those props.

SOLUTION (2 decisions · ~38 runtime lines of 255)

  1. isSticky enables sticky positioning and also forces align-self: flex-start; stickyOffset selects a spacing-token inset — serves the sticky rail problem.
  2. product-detail adopts the public props — proves one end-to-end consumer and removes its custom CSS.

BURDEN: medium — two permanent public props and one shared StyleX map; no state, Effects, listeners, timers, or observers. BURDEN MATCH: proportionate for the demonstrated rail class, but the bundled cross-axis alignment creates an ordinary layout footgun.

VERDICT: BLOCKS — the sticky switch also changes an independent sizing axis.

ARCHITECTURE

OWNER: Stack, as the public utility layout primitive selected in #3223. TIER 1: Stack family + spacing tokens; no new shared behavior system. TIER 2: none. SEAMS: Stack/HStack/VStack inheritance; grid and row/column flex parents; internal scroll containers; RTL; consumer xstyle precedence. BEHAVIOR UNIT: inline — two static StyleX declarations and an O(1) token lookup are small and directly testable.

seam driven result
previous product-detail workaround vs new props byte-identical pixels and identical 150×106 geometry after 180px scroll
scroll container sticks 33px from the pane edge for stickyOffset={8} (32px token + 1px border)
HStack in a column flex parent sticks, but width changes from 302px to 173.98px
xstyle={{alignSelf: 'stretch'}} equivalent sticks and restores 302px content width
nested RTL rail computed direction rtl; sticky positioning remains active
200% text zoom focus remains reachable; focusing the action scrolls to the pane's 498px maximum

Stack ownership and the isSticky / stickyOffset names are already reserved by the maintainer's layout standard. The open problem is implementation scope: isSticky currently owns both sticky positioning and cross-axis alignment.

VERDICT: BLOCKS — an orthogonal layout axis changes as a side effect of enabling sticky behavior.

IMPACT

Existing Stack/HStack/VStack callers are unchanged until they opt in. The migrated product-detail rail renders identically to its old workaround. A builder using the new API for an HStack header inside a normal column layout gets a content-width header instead of the parent's full width; the measured 302px box becomes 173.98px.

VERDICT: BLOCKS — an ordinary opt-in composition visibly collapses.

API

<VStack gap={0} isSticky stickyOffset={8}>
  <ProductInfo />
</VStack>
change public? class doc'd? verdict
+ Stack.isSticky?: boolean = false (inherited by HStack/VStack) yes — core Stack/Layout barrels product-detail, Outline side rail, AppShell side rail HStack/VStack en+zh; unified Stack entry arrived later on main BLOCK — changes alignment as well as position
+ Stack.stickyOffset?: SpacingStep = 0 (inherited by HStack/VStack) yes — core Stack/Layout barrels same sticky rail class HStack/VStack en+zh; current-main literal-union guard red ok after mechanical refresh cleanup

OSSIFICATION: the class and names are established by #2613 and #3223, and utility-layout knobs are consistent with API Conventions. The meaning of isSticky would also permanently promise a cross-axis alignment reset, though, so that side effect must be resolved before release.

Current-main drift, not contributor debt: the unified Stack docs and literal-union guard landed after this PR opened. The refreshed branch now needs the Stack entry plus four inline SpacingStep unions before it can pass current CI.

VERDICT: BLOCKS — the public switch is not orthogonal in a builder's hands.

THEMING

No theme target, component variable, or token is added. stickyOffset consumes the existing spacing scale. The consumer xstyle remains last in the StyleX merge; the alignment override control restored the full 302px width while preserving stickiness.

VERDICT: clear

BREAKING

BEHAVIOR: existing callers unchanged; opt-in isSticky adds position, block-start inset, and cross-axis alignment. API: additive only. VISUAL: product-detail migration is byte-identical to the previous workaround; constrained column composition shrinks 302px → 173.98px. THEME: no target/token/override changes; spacing tokens continue to resolve.

VERDICT: BLOCKS — the opt-in visual contract collapses an ordinary full-width header.

PERFORMANCE & RESOURCES

EFFECTS: zero added, changed, kept, or moved.

RENDER: one static conditional StyleX merge; no state or extra render pass. LISTENERS/OBSERVERS: none. LAYOUT: no JS reads/writes or forced reflow; browser-native sticky layout only. BUNDLE: no dependency; one 11-entry spacing-token style map.

VERDICT: clear

VISUAL EVIDENCE

VISUAL CHECK: manual frames required WHY: the PR adds opt-in layout/CSS behavior, changes the product-detail template, and has no landed story covering sticky endpoints.

Demonstrated migration is exact

Previous custom CSS New props
Legacy workaround New props

The PNGs are byte-identical (f940f24fe2eb7a67292fa00b022aef8754f4e792c4e82350095df71b7e36c7af). Both receipts: exact head f4408f0; neutral/light; LTR; 900×600@1; 180px scroll; position: sticky; 32px inset; align-self: flex-start; focused action retained; 150×106 subject; fonts loaded; zero Storybook/page errors.

Constrained neighbour, RTL, zoom, and override

Constrained column + RTL 200% text zoom Alignment override control
Constrained and RTL matrix 200 percent text zoom Alignment override

The constrained receipt measures the normal stretched baseline at 302px and the sticky HStack at 173.98px, while a nested RTL rail remains sticky. The zoom receipt verifies 32px computed text, focus retention, and a reachable action at maximum scroll. The override receipt verifies align-self: stretch, sticky top 17px in the padded pane, and restored 302px width. All use exact head f4408f0, neutral/light, 900×600@1, settled fonts, and zero capture errors. The exact temporary story delta is archived as measurement-arm.diff.

VERDICT: BLOCKS — the constrained neighbour proves an unintended width change on the new API.

REMEDY SEARCH

REMEDY SEARCH: not triggered — author owns HOW; acceptance criteria are sufficient

The settled outcome is: preserve the Stack's existing cross-axis sizing in a column parent, while the demonstrated grid/rail case still sticks at its token offset. The implementation is not prescribed.

A11Y & I18N

No role, ARIA, string, event, or focus-order code changes. Real Chromium kept the button inside the sticky rail focused through scrolling in LTR, nested RTL, 200% text zoom, and the alignment override control. At 200% zoom the lower action becomes fully visible at the pane's scroll end, and focusing it scrolls there. Remote pr-a11y and pr-rtl both passed on the exact head; no a11y baseline entry was added.

VERDICT: clear

JUDGEMENT

slot verdict
PROBLEM clear
SOLUTION BLOCKS — sticky also changes cross-axis sizing
ARCHITECTURE BLOCKS — orthogonal layout axis is bundled
IMPACT BLOCKS — ordinary column composition collapses
API BLOCKS — public switch promises the side effect
THEMING clear
BREAKING BLOCKS — opt-in visual contract
PERFORMANCE clear
VISUAL BLOCKS — 302px → 173.98px
A11Y & I18N clear

GOAL: partly met — the demonstrated product rail is byte-identical to the workaround, but the same public API visibly narrows an ordinary column-parent header. REMOTE CI: lint, build, Storybook, sandbox, docsite, smoke, a11y, RTL, theme layers, and stable visual regression passed. Full test was 12,788 pass / 1 fail / 5 skipped; the sole failure is the four current-main SpacingStep doc literals reproduced locally. Vercel failed separately; review-required and visual-acceptance remain pending. DISPOSITION: bundled alignment side effect → blocks now; refreshed-doc/literal-union drift → maintainer-owned cleanup on this PR after the API shape settles, not contributor debt. ADVICE: bounded outcome criteria — preserve existing cross-axis sizing while keeping the demonstrated rail sticky at its token offset. AUTHOR CAN PROCEED: yes — ownership, public names, constraints, and acceptance checks are settled; implementation HOW is open. WORST OUTCOME: “a builder using the new API for an HStack header inside a normal column layout gets a content-width header instead of the parent's full width” → request changes.

JUDGEMENT NEEDED: none — #3223 already assigns the capability and names to Stack; this is an implementation-contract defect.

request changes

  1. isSticky unconditionally adds align-self: flex-start → a builder's full-width HStack header collapses from 302px to 173.98px in a column parent · sticky.stylex.ts:31 CONFIRMATION: changing only the alignment through the documented xstyle seam restores 302px while the element remains sticky.

REVIEW

Thanks for closing the sticky-layout gap. I merged current main and kept both appended Stack test blocks.

isSticky also changes cross-axis sizing. In a normal column layout, a full-width HStack collapses around its button; restoring its width needs the custom xstyle this API is meant to remove. Could we preserve the Stack's existing cross-axis sizing while keeping the rail case sticky?

Constrained Stack matrix

If you'd rather talk it through with someone, we're in Discord.

[Reviewed by Robohands]

INLINE

EVIDENCE I DID NOT SPEND

  • The product-detail old/new screenshots are byte-identical, including geometry and focus state.
  • Current main added the unified Stack docs and literal-union guard after this PR opened; the four local doc failures are refresh drift.
  • Public AppShell/SideNav sticky owners add their own stacking/background treatment; the generic rail test does not need either because its sibling does not overlap it.

TIME

TIME total 28m setup/rules 5m loop 1.6.0, audit 1.13, trust guard, public guidance branch refresh 3m trial merge, one append-only conflict, normal push build/server 2m fast-install donor + guarded Storybook; warm main reused: yes browser/a11y 7m 5 receipt frames + 3 diagnostic re-measures focused tests 2m 381 focused tests on head; current-main control code/history 3m landed precedent, issues, drift chronology critique/wiki 6m 2 critic passes, durable frames and page CI wait 16m concurrent remote exact-head checks; no review slot held waste 3m (subset) favicon recapture, pane-padding probe correction, failed contact-sheet tooling

WHAT I COULD NOT VERIFY

  • Safari/WebKit rendering was unavailable in this run; the change uses standard logical sticky CSS and Chromium evidence is complete.

What changed before posting

Posted exactly as drafted at the unchanged reviewed head: CHANGES_REQUESTED review. No body edits.

Current exit: open, MERGEABLE, CHANGES_REQUESTED at f4408f06200d3e447011548f6d7f3f5e47647ce6; merge remains held by the blocking cross-axis sizing finding, red full test, failed Vercel deployment, and pending visual-acceptance status. No merge or auto-merge action.

Clone this wiki locally