Skip to content

fix(flex): give justifyContent free space to distribute again - #51

Open
chiefcll wants to merge 1 commit into
mainfrom
fix/flex-justify-content-width
Open

fix(flex): give justifyContent free space to distribute again#51
chiefcll wants to merge 1 commit into
mainfrom
fix/flex-justify-content-width

Conversation

@chiefcll

@chiefcll chiefcll commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

What

A flex row with no explicit width seeds w = 0, and only the flexStart branch of the layout resizes the container to fit its children. Every other justify mode computes positions against containerSize, so with a shrink-to-fit width they have no free space to work with.

This restores the pre-regression default: when justifyContent needs free space, the container falls back to filling its parent. An explicit flexBoundary: 'contain' still wins.

Follow-up to #39, prompted by @konclave's comment there — they were right that #38's direction was the correct one.

Why

Two separate problems.

1. This is a regression, not intended behavior. git log -S flexFitsWidth points at e7310f8 ("for flex rows the grow start with w 0 to avoid flash"), which changed the default from props.w = node.flexGrow ? 0 : parentWidth - props.x to seeding 0 for any non-fixed flex row. Before that commit <view display="flex" justifyContent="center"> filled its parent and centering worked.

The flash that commit was avoiding doesn't apply here: a container that fills its parent for center/spaceBetween/etc. never gets resized by layout, so there's no width jump to flash. Only the flexStart path — the one that does shrink — keeps the w = 0 seed.

2. The failure mode is worse than "falls back to flexStart." With containerSize = 0, center computes (0 − totalItemSize) / 2, so children get negative offsets against a zero-width container. spaceBetween/spaceAround/spaceEvenly produce negative spacing and overlap.

3. The #39 warning is ~80% dead code. justifyContent is a camelCase union ('flexStart' | 'flexEnd' | 'center' | 'spaceBetween' | 'spaceAround' | 'spaceEvenly'), but the warning tests kebab-case strings:

['center', 'flex-end', 'space-between', 'space-around', 'space-evenly']

Only 'center' matches, so it never fired for the four cases most likely to confuse someone. The as string cast is why the compiler didn't catch it.

Approach

if (
  flexFitsWidth &&
  node.justifyContent !== undefined &&
  node.justifyContent !== 'flexStart'
) {
  if (node.flexBoundary === undefined) {
    flexFitsWidth = false;      // fill the parent — there's space to distribute
  } else if (isDev) {
    console.warn(...);          // explicit contain + justify is a contradiction
  }
}
  • Tests !== 'flexStart' instead of listing the modes that need space. Correct by construction, no string list to keep in sync, and no casing to get wrong. The as string cast is gone, so TypeScript checks these comparisons now.
  • Keeps the feat(flex): warn in dev when justifyContent requires an explicit width #39 warning but narrows it to the one case the fix can't resolve: an explicit flexBoundary: 'contain' alongside a justify mode that needs space. The developer asked for two incompatible things; honor the boundary and say so.

What this changes for existing apps

This is a behavior change, not just a bug fix. Measured before/after on a 1000px-wide parent with two 100px children:

justifyContent="center" on a row positioned at x={500}

container width first child x content spans (absolute)
before 0 -100 400 → 600
after 500 150 650 → 850

Before, the content ended up visually centered on the container's own x. If anyone reached for x={centerPoint} + justifyContent="center" and it looked right, it was right by accident — and it moves now. The fix centers within x → parent right edge, which is what the old pre-e7310f8 behavior did.

justifyContent="flexEnd" on a row at x={400}

container width last child x content ends at
before 0 -100 400
after 600 500 1000

Same shape of break: content used to right-align ending at the container's x, now it right-aligns to the parent's right edge.

Containers with a color or border. Width goes from 0 to the parent width, so a background that was silently invisible now paints across the full parent. Worth grepping for display="flex" + justifyContent + color with no width.

Anything reading .width on these containers — custom onLayout handlers, scroll math, measurement code — now reads parent width instead of 0.

spaceBetween/spaceAround/spaceEvenly were producing negative spacing and overlapping children, so I'd be surprised if anyone is depending on those.

Workaround for all of the above: set an explicit width, or add flexBoundary="contain" to keep shrink-to-fit (you'll get a dev warning explaining the tradeoff).

Known limitation

The width is still chosen once at node creation, so:

  • A justifyContent that becomes non-flexStart reactively after render won't re-pick the width.
  • Nested inside a shrink-to-fit parent, parent.w is 0 at creation time, so parentWidth - x is 0 and nothing improves. Verified: outer w=0, inner w=0, child x=-100, unchanged by this PR.

Fixing either means moving container sizing into the layout functions' non-flexStart branches, which is a bigger change than I wanted to fold in here.

Tests

New tests/flex-justify-width.test.tsx — render-level, because the existing tests/flex.spec.ts calls the layout functions directly with explicit container widths and can't reach this code path at all.

  1. center and spaceBetween with no width → container fills parent, children land at the right offsets
  2. flexStart and no justifyContent → still shrinks to fit (fix is scoped)
  3. explicit flexBoundary="contain" + center → stays contained, dev warning fires

I confirmed test 1 is discriminating by stashing the fix and re-running: fails with expected +0 to equal 600. Test 3 passes against both old and new code — the old kebab-case list happened to contain 'center', the one value that matched — so it's guarding the flexBoundary refinement, which a plain #38-style fix would break.

npm test 171 passing, npm run tsc clean, npm run lint 0 errors.

🤖 Generated with Claude Code

A flex row with no explicit width seeds w = 0 and only the flexStart
branch of the layout resizes the container to fit its children. Every
other justify mode computes against containerSize, so with a
shrink-to-fit width they position children out of zero free space —
producing negative offsets against a zero-width container rather than
simply falling back to flexStart.

This regressed in e7310f8, which switched non-fixed flex rows from
filling the parent to seeding 0 to avoid a resize flash. Restore the
previous default when justifyContent needs free space: that case never
triggers the shrink-to-fit resize, so it can't flash.

An explicit flexBoundary 'contain' still wins — that combination is a
genuine contradiction, so honor the boundary and warn in dev. Replaces
the #39 warning, which tested kebab-case strings against a camelCase
union and so only ever fired for 'center'. Testing !== 'flexStart'
avoids the mismatch entirely and drops the `as string` cast that hid it
from the compiler.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chiefcll

chiefcll commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

@pecoram - you may like the last sentence in What section.

LMK thoughts before I merge this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant