improvement(spacing): let Stack flip direction from CSS via stackBelow (CUI-38) - #1182
Open
JeanMarcMilletScality wants to merge 1 commit into
Open
Conversation
Contributor
Hello jeanmarcmilletscality,My role is to assist you with the merge of this Available options
Available commands
Status report is not available. |
Contributor
Waiting for approvalThe following approvals are needed before I can proceed with the merge:
Peer approvals must include at least 1 approval from the following list: |
JeanMarcMilletScality
force-pushed
the
improvement/CUI-38-self-orienting-separator
branch
from
August 12, 2026 16:41
2dfb90d to
f5e87ef
Compare
JeanMarcMilletScality
marked this pull request as draft
August 13, 2026 07:21
JeanMarcMilletScality
force-pushed
the
improvement/CUI-38-self-orienting-separator
branch
from
August 26, 2026 13:17
f5e87ef to
6010197
Compare
Contributor
Waiting for approvalThe following approvals are needed before I can proceed with the merge:
Peer approvals must include at least 1 approval from the following list: |
…w (CUI-38)
Stack picked its separator element from the direction prop at render time,
injecting HSeparator (a vertical rule) or VSeparator (a 24px horizontal dash).
A CSS flex-direction flip — how container-query-driven responsive layouts work —
left the separators pointing the wrong way, blocking any consuming
application that needs a row to collapse into a column as its container
narrows.
An earlier revision made the row separator self-orienting, so that a CSS flip
turned it into a full-width rule. Design rejected that: a column Stack must keep
its short dash. The two treatments are different shapes rather than one shape on
two axes, and CSS cannot query a parent's flex-direction, so the component has
to emit the query itself.
Both treatments now live in one element as two css blocks, and Stack gains
stackBelow: below that container width a horizontal Stack flips its direction,
its align-items and its separators together. Consumers set one number instead of
hand-writing a container query and fighting styled-system for specificity.
This follows the convention already used by Button (iconOnly={number}) and Form
(STACK_BELOW): a numeric breakpoint the component turns into an
@container responsive query. Without an ancestor establishing that container the
query never matches and the Stack stays horizontal, the same graceful no-op as
iconOnly.
Layout moved off Box's styled-system props onto a styled(Box) wrapper so the
query does not have to out-specify them.
Verified by rendering old and new Stack side by side and diffing the computed
declarations per element: in both default directions the container and separator
are identical, except that the column separator now also states align-self: auto
and margin: 0 — the initial values it must restore when the query swaps
treatments. The separators' text nodes are dropped; both treatments set
explicit dimensions, so they render the same without them.
stackBelow is opt-in, so no existing call site changes. No public API removal:
Separator was never exported.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JeanMarcMilletScality
force-pushed
the
improvement/CUI-38-self-orienting-separator
branch
from
August 28, 2026 13:23
6010197 to
131de48
Compare
Contributor
Waiting for approvalThe following approvals are needed before I can proceed with the merge:
Peer approvals must include at least 1 approval from the following list: |
JeanMarcMilletScality
marked this pull request as ready for review
August 28, 2026 13:24
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TL;DR —
StackgainsstackBelow={px}: below that container width a horizontal Stack becomes vertical, and its separators switch to the vertical treatment along with it. Nothing renders differently today — the prop is opt-in.Context / Why
Stackpicks its separator element from thedirectionprop, at render time. A CSSflex-directionflip — which is how container-query-driven responsive layouts work — leaves the separators pointing the wrong way. A consuming application hits this the moment it needs a row of columns to collapse into a stack as its container narrows — the row cannot flip, so the content clips silently instead. CUI-38🧩 Approach
This PR replaces an earlier revision that made the row separator self-orienting. That version used
flex-basisfor the main axis andalign-selffor the cross axis, so a CSS flip turned the rule into a full-width one. Design rejected it: a columnStackmust keep its short dash. The two treatments are different shapes, not one shape on two axes — and CSS cannot query a parent'sflex-direction— so the component has to emit the query itself.Both treatments now live in one element as two
cssblocks, andStackgainsstackBelow:One element is what makes this possible at all: CSS can restyle an element, but it cannot swap which element React rendered.
This follows a convention the library already has — a numeric breakpoint the component turns into an
@container responsivequery:ButtoniconOnly={number}FormSTACK_BELOWStackstackBelow={number}(new)Without an ancestor establishing that container (
<Box container>), the query never matches and theStackstays horizontal — the same graceful no-op asiconOnly.Layout moved off
Box's styled-system props onto astyled(Box)wrapper so the container query doesn't have to out-specify them. This is what removes the&&specificity hack a consumer previously needed.Verified no-op in both default directions
Rendered the old and new
Stackside by side withServerStyleSheet, then diffed the computed declarations per element (not the raw stylesheet, which differs only in class bookkeeping and rule splitting):horizontalverticalalign-self: autoandmargin: 0addedThose two additions are the CSS initial values for a flex item, so the computed style is unchanged. They exist only so the shared
dashSeparatorblock resets whatruleSeparatorsets when the container query swaps treatments.The
text nodes are dropped from both separators. Each treatment sets explicit dimensions, so neither depends on content for its size.Why the vertical dash had to stay — beyond design's ruling, a full-width rule in a page
Formduplicates the header's ownborder-bottom: 1px solid theme.border(Form.component.tsx:102): same weight, same colour, near-same width, so a section separator becomes indistinguishable from the form title's rule.📷 Screenshots
🔧 Usage
No existing
Stackusage changes. What changes is how a consumer opts into a responsive direction — before, they hand-wrote the query and foughtBoxfor specificity:Before — what the earlier revision of this PR required:
After — from the story added in this PR:
Two notes for consumers:
responsivecontainer —<Box container>. Without one the query never matches and the Stack stays horizontal.stackBelowis ignored whendirection="vertical". Flipping a vertical Stack to a row in CSS is still unsupported — pre-existing behaviour, unchanged here.🔍 Review focus
src/lib/spacing.tsx › StackBox— layout (display,flex-direction,align-items) moved offBox's styled-system props onto astyled(Box)wrapper. This is the change most able to affect existing call sites: everyStackin every repo now gets its layout from a different class. The computed-declaration diff above is the evidence it's a no-op;gapstill comes fromBox, so the two classes now split what one used to emit.src/lib/spacing.tsx › Separator—ruleSeparator/dashSeparatormust stay mutually resetting.dashSeparatorrestoresalign-selfandmarginprecisely becauseruleSeparatorsets them; dropping either reset silently breaks the flipped state, and container queries don't evaluate in jsdom so no unit test will catch it.src/lib/spacing.tsx › Stack—stackBelowis additive andSeparatorwas never exported, so no public API is removed.🧪 How to test
npm run storybook→ Components/Styling/Spacing Utils › stackBelow — direction follows the container width.development/1.0.page-form,tab-form,form-with-accordion) — section separators should be unchanged short dashes, still clearly subordinate to the form title's rule.🚧 Follow-up
@scality/core-uiversion bump once this is released; the integration details live in the consuming application's own ticket.Stackstill can't be flipped to a row in CSS. No consumer needs it today. Not yet ticketed.Wrapis deliberately untouched — it does not wrap despite its name, deferred pending a usage audit. Not yet ticketed.🔗 References
IconWrapperdeforming into an ellipse), merged; released in 0.228.0.What changed
src/lib/spacing.tsx—HSeparator/VSeparator/Separator({ type })collapse into oneSeparatorstyled component carrying both treatments asruleSeparatoranddashSeparatorcssblocks. A newStackBox = styled(Box)holds the layout so the container query doesn't fight styled-system for specificity.StackgainsstackBelow?: numberand passes it, withdirection, to both as transient props (verified not to reach the DOM).stories/spacing.stories.tsx— replaces the earlierContainerQueryDirectionFlipstory withStackBelowStory, which uses the prop instead of a hand-written query. Its container setscontainer-name: responsiveexplicitly, documenting the requirement.Deliberately not in this PR:
Wrap(out of scope per CUI-38), any restyle of existing vertical separators, and anyFormchange.tsc --noEmitclean,npm run buildclean,npm run lintclean, suite green.