repro(Container): demonstration of responsive form layout limits - #1172
Draft
DreaminDani wants to merge 1 commit into
Draft
repro(Container): demonstration of responsive form layout limits#1172DreaminDani wants to merge 1 commit into
DreaminDani wants to merge 1 commit into
Conversation
Adds a Storybook-only reproduction of a two-column form layout (the Billing information onboarding step) built four ways with Container and GridContainer, so the failure modes can be compared at a fixed card width while the viewport changes independently. Also adds three isolated repros for the primitive-level defects the comparison runs into: - Container sets width:100% plus padding with no box-sizing:border-box, and the library ships no global reset, so a padded Container overflows its parent by the padding amount. - .container_responsive sets max-width:none below 768px, so a Container's own maxWidth is discarded below the breakpoint. - isResponsive is keyed to the viewport rather than the container, and defaults to true, so every Container flips to column at 768px including footer rows using justifyContent="space-between". No library code changes. Stories only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Contributor
Storybook Preview Deployed✅ Preview URL: https://click-gqkymjrwu-clickhouse.vercel.app Built from commit: |
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.
Why?
Responsive forms are pretty much impossible to do correctly with our current primitives. For example, the payment info form in onboarding needs a form shaped like this: full-width address lines, two rows of side-by-side fields, a toggle row, and a footer with Back on the left and the primary action on the right.
In Figma that is a stack of auto-layouts. In a browser it also has to survive the card getting narrower, at which point the side-by-side fields need to stack.
I tried to build it with
Containerand withGridContainerand could not get a correct result from either without switching off theisResponsiveprop and overriding the flex/grid properties directly.This PR is the reproduction only. It changes no library code, so the Storybook preview shows how
mainbehaves today.How?
One new stories file:
src/components/Container/ResponsiveFormLayout.stories.tsx, under Layout > Responsive Form Layout.CompareAllbuilds the same form four ways and gives each one acontainerWidthcontrol, so card width and window width can be varied independently. That separation is the thing to look at.Container, library defaultsContainer,isResponsive={false}withwrap/grow/minWidthGridContainer,gridTemplateColumns="1fr 1fr"withisResponsiveGridContainer,isResponsive={false}withrepeat(auto-fit, minmax(200px, 1fr))City and Zip field widths, measured in Chromium:
Three more stories isolate the primitive-level defects behind those numbers.
isResponsiveis keyed to the viewport and never to the container. At a 360px card on a 1400px window, A and C leave the fields side by side at 172px and clip the primary button. Inside a dialog or a side panel the card width is the only thing that matters. Because the prop also defaults totrue, at 768px everyContainerflips toflex-direction: column, so approach A stacks Back on top of the primary button and drops the toggle label below the switch, with 720px of room available.Containerpadding overflows its parent..containersetswidth: 100%andpaddingwith nobox-sizing: border-box, and the library ships no global reset (global.cssis a singlebodycolor rule).<Container fillWidth padding="lg">inside a 360px parent measures 408px. The overflow equals the padding, and it is invisible in any app that ships its own reset, which is probably why it has survived.isResponsivediscardsmaxWidth..container_responsivesetsmax-width: nonebelow 768px. AContainerasking formaxWidth="480px"inside a 900px parent measures 482px at a 1000px viewport and 902px at 700px.Underneath all three sits an API problem rather than a CSS one:
GridContainerchildren cannot describe their own placement, and neither component accepts any instruction about what to do at a given breakpoint. That is what the recommendations below are about.Recommendations, for a separate PR
The first two are API shape, the third is the mechanism both of them would need.
1. GridContainer gives children no way to place themselves
A child cannot say how many columns it spans. The full-width rows in this repro need
style={{ gridColumn: 'span 2' }}, which leaves the design system entirely, and those inline spans are what create the implicitautotrack that garbles theisResponsivecollapse into512px 192px. The one prop that would fix it does not exist.MUI's Grid puts the span on the child, and accepts breakpoint objects there:
That would express this form directly:
size={{ xs: 12, md: 6 }}on City and Zip,size={12}on the address lines, no inline styles and no1 / -1strings.2. Neither component accepts per-breakpoint instruction
isResponsiveis one boolean covering one hardcoded breakpoint. There is no way to say "two columns here, one column below that", so the escape hatch is a raw CSS string ingridTemplateColumns, which is why approach D ends up with three columns at a 720px card.MUI takes a responsive object on every layout prop, not just child spans:
Stackis the closest analogue to ourContainer, anddirectiontaking a breakpoint object is the thingorientationcannot do today. A footer row would setorientation="horizontal"flat and never stack, instead of needingisResponsive={false}to opt out of a default it never wanted.3. Key the reflow to the container, not the viewport
Both of the above still leave the breakpoints keyed to the window. Two references for fixing that:
shadcn's
Fieldhasorientation="vertical" | "horizontal" | "responsive", andresponsiveresolves against a container query rather than the viewport. The parent opts in by naming a containment context:<FieldGroup className="@container/field-group ...">. Underneath it is Tailwind v4, where@containermarks the container and@md:flex-row,@max-md:flex-col,@sm:@max-md:flex-coland@min-[475px]:flex-rowset the conditions. shadcn is copied source plus utility classes rather than a component API, so the transferable part is the mechanism and theorientation="responsive"naming, not the implementation.MUI v6 added
theme.containerQueries, mirroring the existingtheme.breakpointsmethods so the same breakpoint keys work either way:We already have the token half of this:
--breakpoint-sizes-smthrough-2xlexist and onlymdis used.Concretely for us:
container-type: inline-sizeon both components, re-key the existing768pxrules to@container, and let callers name a breakpoint token instead of accepting the hardcoded one..browserslistrcis not a blocker; the oldest target is Safari/iOS 16.0, and container queries shipped in Safari 16.0 and Chrome 105.The two mechanisms compose rather than compete, which is what MUI does: responsive objects for the props, container queries for what those breakpoints resolve against. If we take both,
orientation={{ base: 'vertical', md: 'horizontal' }}andsize={{ base: 12, md: 6 }}would read against container width.Smaller fixes in the same area
box-sizing: border-boxon both base classes. Apaddingprop onGridContainer, which has none. And a decision onisResponsiveitself: defaulting totrueis wrong for any row that should never stack, so the default and the name are both open questions.On the two approaches in this repro
Approach B is correct at every width I tested, and it gets there by turning
isResponsiveoff on all tenContainers and reimplementing reflow with flex wrap. A plaindivwould do the same. Approach D reflows on container width but mis-groups: at a 720px card,auto-fitproduces three columns and orphans State / Province, because a track list cannot express "these two fields belong together." A child-sidesizeprop can.References
Tickets?
None yet. Filing follow-ups once there is agreement on the direction.
Contribution checklist?
buildcommand runs locally (not run; stories-only change,tsc --noEmitand eslint are clean)Security checklist?
dangerouslySetInnerHTMLPreview?
Storybook preview on this PR, under Layout > Responsive Form Layout. Start with
CompareAll, then resize the window across 768px while leavingcontainerWidthalone.Note for reviewers: these stories render deliberately broken layouts, so they will show up as new Chromatic snapshots needing approval.