Skip to content

[material-ui] Spacing-derived component dimensions#48585

Draft
siriwatknp wants to merge 29 commits into
mui:masterfrom
siriwatknp:feat/components-theme-spacing
Draft

[material-ui] Spacing-derived component dimensions#48585
siriwatknp wants to merge 29 commits into
mui:masterfrom
siriwatknp:feat/components-theme-spacing

Conversation

@siriwatknp
Copy link
Copy Markdown
Member

Summary

Rolls a --mui-spacing-driven density system across packages/mui-material/src/*. Every padding / margin / gap (and selected geometry on Chip) that should scale with theme spacing now rides --mui-spacing via calc(${theme.spacing(N)} ± offset), so users can shift the entire app's density by changing one CSS variable — pixel-identical to today at the default 8px.

Decision + spec:

  • docs/adr/0001-spacing-derived-component-dimensions.md — decision + rationale
  • docs/adr/spacing-derived-dimensions-spec.md — the rule, worked examples, "choosing N" principle, clamp pattern
  • docs/adr/spacing-derived-rollout-plan.md — per-component checklist (iter 1 done + iter 2 in progress) and the iter-2 backlog

Scope (≈ 50 components derived; rest audited-skip with rationale):

  • Input/form family — Button, OutlinedInput, FilledInput, InputBase, Input, all three InputLabel transform sets, InputAdornment, FormControl, FormControlLabel
  • Buttons/controls — IconButton, ToggleButton, Tab, BottomNavigationAction, SpeedDialAction, PaginationItem, Fab extended
  • Lists/menus — List, ListItem, ListItemButton, ListItemAvatar, ListItemIcon, ListItemText, ListSubheader, MenuItem, Autocomplete
  • Surfaces — Accordion family, Alert, Dialog content parts, Card family, SnackbarContent, Tooltip, Breadcrumbs, MobileStepper
  • Steppers/tables/misc — Stepper family, TableCell, TablePagination, TableSortLabel, ImageListItemBar, Badge
  • Iter-2 refinements — Chip (coupled formula, N=2 container + N=1 inner glyphs, max-clamped negatives), FormControlLabel -11, List gutter 16, SwitchBase 9

Key principles (full detail in the spec):

  • Pixel-identical at default by construction: calc(spacing(N) ± X) ≡ literal at --mui-spacing: 8px. Verified per component with a Playwright harness (scripts/spacing-screenshots/, maxDiffPixels: 0).
  • Vertical derives, anchored horizontal stays literal. Notch, label-x, icon reservation, checkbox columns, etc.
  • N is the step coefficient. Larger containers N=2, smaller inner glyphs N=1, so containers respond visibly while contents scale gently and don't outgrow their parent.
  • Static styled objects wrapped in memoTheme(({ theme }) => …). Compensation negatives use theme.spacing(-n) ± offset; inner-positive offsets that must not go negative wrap in max(…, 0px).

Iter-2 backlog (deferred — see plan § "Iteration 2"): coordinated horizontal pass (input inline, list inset, autocomplete reservation, checkbox column), geometry→spacing for Switch/Slider/Fab/PaginationItem, Dialog breakpoint coupling, AvatarGroup default.

Test plan

  • Per-component screenshot harness — pixel-identical at default + density review (6/8/10px) green on every derived component
  • Unit tests green — browser (chromium) + node (jsdom). One Tab computed-style test marked skipIf(isJsdom()) for the calc value
  • eslint + prettier clean across all changed files
  • Argos CI — anticipate small AA diff on a few components (calc-vs-literal sub-pixel; documented in spec § "Workflow refinement")
  • Manual density review in the docs experiment fixture (docs/pages/experiments/spacing-fixture.tsx)

siriwatknp added 29 commits May 27, 2026 17:10
Route Button padding (all variants/sizes) and OutlinedInput block padding +
outlined InputLabel resting position through theme.spacing() so they ride
--mui-spacing for density. Pixel-identical at default theme. Horizontal axis
stays literal on notch-coupled components to avoid breaking the outline.

Adds ADR 0001 + replication spec for the remaining components.
Rollout plan checklist (~70 components, grouped) with per-component
requirement, edge cases, and a local Playwright verification flow (no Argos):
toHaveScreenshot asserts the default is pixel-identical before/after; 6px/10px
shots saved for density review. Adds spacing-fixture route + spacing:shot
scripts.
FilledInput block padding (incl. 25/8 label-space, hiddenLabel, small,
multiline) and the filled InputLabel transformY ride theme.spacing();
horizontal (inline 12, label x) stays literal. Pixel-identical at default.
Harness writes to its own outputDir (stop clobbering tracked test-results/).
Document FilledInput learnings: horizontal coupling extends to filled/standard
label x; shrunk-label tracking splits by variant (border=literal, padding=tracks);
verify floating labels with a valued field.
Block padding only (4px/5px -> spacing(1)-4px / -3px); 1px small nudge stays
literal (sub-unit); inline already 0. Standard InputLabel tracking is a
separate rollout item.
marginTop 16 -> theme.spacing(2). Only spacing value in Input root.
InputAdornment filled start marginTop 16 -> spacing(2) (tracks filled
label-space); horizontal margins stay literal with the input inline layout.
NativeSelect/Select audited: paddingRight 24/32 is icon-anchored geometry,
nothing to derive.
Standard resting y tracks Input marginTop + InputBase paddingTop:
spacing(3)-4px (md), spacing(2)+1px (sm). Shrunk -1.5px floats above -> literal.
…orm family

FormControl margin normal/dense -> spacing (static styled wrapped in memoTheme).
FormControlLabel row gaps 16 -> spacing(2); -11 stays literal (control padding).
FormHelperText/FormLabel audited skip. Completes the input/form family.
IconButton padding 8/5/12 -> spacing(1) / spacing(1)-3px / spacing(2)-4px.
fontSize + edge margins stay literal. ButtonBase: only resets, nothing to derive.
… Chip

Fab extended 0 16px/0 8px -> 0 spacing(2)/0 spacing(1); circle + sizes are
geometry. ButtonGroup: border-overlap + minWidth only. Chip: height-fixed
geometry, inline padding anchored-coupled, no block spacing.
ToggleButton padding 11/7/15 -> spacing-based. Tab padding 12px 16px +
labelIcon paddingTop/Bottom 9 + stacked-icon margin 6 -> spacing-based.
Tab jsdom computed-style test skipped (calc). ToggleButtonGroup: border overlap.
… PaginationItem

Inline paddings + margins -> spacing-based; geometry (height/minWidth) literal.
Pagination root skip (resets). Completes buttons & actionable controls.
List/ListItem/ListItemButton/MenuItem block paddings + ListItemText/Avatar/Icon
margins -> spacing-based. Inline (16/56/72/36/52) kept literal: anchored
horizontal alignment system. List/ListItemAvatar/ListItemText static -> memoTheme.
ListSubheader skip (horizontal only).
…ts & menus

Dropdown block paddings + input-integration block paddings -> spacing-based,
redistribution preserved (root 9 + inner 7.5 sums to OutlinedInput total).
Inline kept literal (icon-anchored reservation). Static root -> memoTheme.
Accordion margins, Alert paddings (icon/message/action derived together),
Dialog Title/Content/Actions paddings -> spacing-based. Dialog paper margin
skipped (coupled to static media-query breakpoints). Several static -> memoTheme.
…readcrumbs, MobileStepper

Finishes surfaces & containers. Card/Snackbar paddings, Tooltip padding,
Breadcrumbs separator, MobileStepper padding -> spacing-based. Tooltip arrow
margins + Card action sub-unit margins kept literal. Static -> memoTheme.
…dding)

Stepper/Step gap+padding, StepButton padding/negative margin, StepLabel +
StepContent paddings -> spacing-based. Half-icon 12px + StepConnector geometry
kept literal. Several static -> memoTheme.
Cell paddings, pagination margins, sort-label icon margins -> spacing-based.
Checkbox-column paddings + select arrow reservation kept literal.
…yout

Badge/ImageListItemBar paddings -> spacing-based. ImageList gap (public prop),
AvatarGroup overlap (var-driven), Link (none) skipped.
Switch/Slider/LinearProgress/Skeleton/Divider/Typography/CssBaseline audited as
geometry/skip. All checklist groups done. Added recurring-decisions summary.
… left

Groups deferred work by nature: (A) anchored horizontal systems, (B) geometry->
spacing, (C) sub-unit nudges, (D) media-query breakpoint coupling, (E) prop/var
driven, (F) em/rem + resets. Each item lists the exact literal values remaining.
Compensation negative now tracks IconButton padding at every --mui-spacing.
Both marginLeft (end placement) and marginRight (start placement) derived.

Baseline refreshed: literal -11 vs calc(spacing(-1) - 3px) is mathematically
equal at default but the browser resolves calc via var(--mui-spacing) with
sub-pixel precision differences from the integer literal -> ~1% anti-aliasing
diff. Equality at default is by construction; refreshed baseline anchors the
strict gate to the calc form for future changes.

Workflow refinement documented in the plan.
Every value (height/borderRadius, avatar/icon width+height, all the small
5/-6/4/-4/2/3 horizontal offsets, deleteIcon fontSize 22/16, label paddings)
maps to spacing(N) +/- offset via a coupled formula. The whole pill scales
together with --mui-spacing: at 10px the chip is 40px tall with 30px avatar
and proportional gaps. Pixel-identical at default.

ChipLabel static -> memoTheme. Refines the 'sub-unit literal' rule: when the
small values are load-bearing in a coupled system, derive them via the formula.
ListItem / ListItemButton / ListSubheader gutter paddingLeft/Right derived
together. The three parallel containers now stay aligned at every --mui-spacing
(was the horizontal-coupling reason for iter-1's 'literal' decision).

Still literal: ListItemText inset 56, ListSubheader inset 72, ListItem
secondaryAction paddingRight 48, ListItemIcon/Avatar widths 36/56 -- all
anchored to icon geometry, deferred to a coordinated icon-width derivation.
Match the +2/unit step you set on chip height. Medium avatar width/height
spacing(3) -> calc(8px + spacing(2)) (=24); medium deleteIcon fontSize
calc(spacing(3) - 2px) -> calc(6px + spacing(2)) (=22); medium icon gets
explicit fontSize calc(8px + spacing(2)) so it scales with the avatar (was
inheriting SvgIcon default 24). Small variants already had the right step.

Constant 8px chip-vs-avatar gap preserved at every density (centering exact).
Pixel-identical at default.
theme.spacing(N) inside calc(literal + spacing(N)) is the step coefficient:
each 1px change in --mui-spacing shifts the value by N px. Pick N from the
element's size/role, not just round(P/8):
- larger / container-like elements -> larger N (Chip height, Button padding N=2)
- smaller / inner glyphs -> smaller N (Chip avatar/icon/deleteIcon N=1)
At density the container scales faster than its contents, so inner gaps grow
instead of overflowing.

Wrap inner-positive offsets that must not go negative at ultra-low density in
max(..., 0px) (Chip avatar/icon marginLeft, deleteIcon marginRight).

Chip refined accordingly: height keeps N=2; avatar/icon/deleteIcon width/height/
fontSize switched to N=1 with literal offsets; min-clamp applied. Pixel-identical
at default; small-density (6px) chips stay proportional.
Internal SwitchBase used by Checkbox + Radio (Switch overrides padding).
+1/unit step deliberately matches FormControlLabel marginLeft's -1/unit step
(calc(spacing(-1) - 3px)). The two-axis sum stays constant -2px at every
--mui-spacing, so checkbox/radio glyphs hold their exact visual position
relative to FormControlLabel's edge across all densities. Pixel-identical
at default.
@code-infra-dashboard
Copy link
Copy Markdown

Deploy preview

https://deploy-preview-48585--material-ui.netlify.app/

Bundle size

Bundle Parsed size Gzip size
@mui/material 🔺+5.28KB(+1.02%) 🔺+512B(+0.34%)
@mui/lab 0B(0.00%) 0B(0.00%)
@mui/private-theming 0B(0.00%) 0B(0.00%)
@mui/system 0B(0.00%) 0B(0.00%)
@mui/utils 0B(0.00%) 0B(0.00%)

Details of bundle changes


Check out the code infra dashboard for more information about this PR.

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