[WIP][prototype] density system#48749
Draft
siriwatknp wants to merge 187 commits into
Draft
Conversation
Expose Button padding as overridable CSS vars resolved inline from a (variant,size) lookup; add enhanceDensity to wire tokens to a --mui-density-* scale. Literal-px fallbacks keep the default pixel-identical. Design in CONTEXT.md + docs/adr/0001; demo at /experiments/density-tokens.
…t of density experiment
… + --Button-pad seam - Root consumes var(--Button-pad, var(--_pad)); --_pad universal default on root - (variant,size) literals + built-in-size routing live in variants (deduped CSS) - Inline bridge only for custom sizes (keeps custom sizes tunable, zero inline for built-ins) - Two-var rationale + accepted trade-offs documented in ADR-0001 + CONTEXT - enhanceDensity maps sized tokens (--Button-<size>-pad) to density scale
…seam - OutlinedInput: block-only density (--OutlinedInput-<size>-padBlock); root routes, input inherits; drop redundant size/multiline variants - InputLabel: generic --InputLabel-y seam; OutlinedInput bridges sibling label via :has(~ &) - Docs: ADR-0001 OutlinedInput + label :has bridge, CONTEXT, density-adapter-rollout guide, experiment demo
- density-fixture.tsx: per-component matrix scoped by ?c=&level (default pixel-identical) - scripts/density-screenshots: config + spec + README (maxDiffPixels 0) - density:shot / density:shot:update scripts; gitignore harness outputs
- Tokenize the 14px inline gutter as --OutlinedInput-padInline (size-invariant base token) - Uniform consume shape var(--seam, var(--_internal)) across both axes: block sized (routed), inline base; --_padInline internal default - Docs: base-token shape in ADR/CONTEXT; rollout gotchas — split-only-if-forced, uniform consume shape, inline gutter != adornment gap
Revert the lift of block padding to the root + inheritance; tokenize each literal where master has it (input owns inline/non-multiline, root owns multiline/adornment gutters) for the smallest diff. Promote padInline from a base token to a sized axis: default 14px both sizes, but expose --OutlinedInput-<size>-padInline so a design system can tune inline density per size. Both axes now routed per size in place; label :has derives --InputLabel-y straight from the public sized token. Docs: base token reserved for axes where per-size override is meaningless; a size-invariant default alone no longer justifies it.
Apply the density adapter (docs/adr/0001) to the @mui/material components used by the dashboard template: Chip, IconButton, MenuItem, ListItem, ListItemButton, ListItemIcon, ListItemText, ListSubheader, Toolbar, Tab, Tabs, TablePagination, CardContent, Select, Breadcrumbs, InputAdornment, Badge. Each exposes its real spacing axes as public sized tokens over literal-px internal defaults; the default render stays pixel-identical to master (density screenshot harness, maxDiffPixels:0). Checkbox/FormControl skipped - no density axis. enhanceDensity wires every component's sized tokens (incl. OutlinedInput) to the density scale. The verification fixture gains a matrix + dense/loose scope per component. Boolean `dense` components (MenuItem, ListItem, ListItemButton, ListItemText) expose the default state via the plain seam --Component-<key> and only the dense override as --Component-dense-<key>. Toolbar keeps theme.mixins.toolbar for its regular height (only dense + gutters tokenized).
Boolean compactness toggles (dense) use a state token: default state is the plain seam --Component-<key> (base-token-shaped, no base routing), only the on state is qualified --Component-dense-<key>. No --Component-normal/regular/default- qualifier - a boolean has no name for off. Added to CONTEXT language, ADR 0001 resolution, and the rollout recipe + naming.
SwitchBase (shared agnostic base) consumes one seam: padding var(--SwitchBase-pad, var(--_pad)), --_pad 9px. Checkbox/Radio (styled(SwitchBase)) route per-size public tokens --Checkbox/Radio-<size>-pad into the seam; default 9px both sizes (pixel-identical). Switch routes its thumb (SwitchBase) padding via --Switch-<size>-pad (9/4); box geometry stays literal (size-coupled). enhanceDensity + fixture wired.
SwitchBase owns the agnostic seam consumed once; Checkbox/Radio/Switch route per-component sized tokens into it. Covers the two reader topologies (consumer is the base vs wraps it as a descendant), delivery via custom-property inheritance (no descendant selector), and the --_<key>-shadowing caveat. Added to CONTEXT relationships, ADR 0001 specifics, rollout Recipe C + Done list.
Tokenize Switch's four real dims per size (--Switch-<size>-width/height/thumbSize/ touchSize). Derive SwitchBase pad = (touchSize-thumbSize)/2, button top = (height-touchSize)/2, checked travel = width-touchSize, thumb size = thumbSize, so the thumb stays centered on the track (absolute + transform). Replaces the pad-only token that drifted the thumb. Switch dropped from enhanceDensity (geometry isn't spacing-scale-derived). Default pixel-identical.
…lues Switch tokenizes width/height/thumbSize/touchSize per size and derives pad/top/ travel via calc (thumb stays centered); not the pad-only approach. Corrects the shared-base sections in ADR 0001 + rollout Recipe C.
The root padding (12/7, track inset) is its own axis -> tokenize as --Switch-<size>-pad over --_pad, consumed padding: var(--Switch-pad, var(--_pad)). Distinct from the derived thumb SwitchBase pad. Fixture scope + docs updated.
borderRadius = (height - 2*pad)/2 (full-pill track thickness) instead of literal 14/2, so the track stays rounded when the dims are tuned. Pixel-identical (medium 7px; small clamps to a pill).
Add an xxl density step (4x spacing unit). Wire MuiSwitch: map per-size width/height/touchSize/thumbSize/pad to scale steps (xxl for the wider track); pad/top/travel/radius re-derive so the geometry stays valid. Docs updated.
Switch dims were mapped to single scale steps, shrinking it. Compose from steps so defaults land on today's px (medium 58/38/20/38/12, small 40/24/16/24/7) and still scale with density: width calc(xxl*2-6), height/touch calc(xxl+xs), thumb calc(lg+xxs), etc. touchSize == height keeps the thumb centered.
…Tabs minHeight - enhanceDensity: derive OutlinedInput --InputLabel-y from density step (sibling label can't read the input's padBlock token); per-size via variants - MenuItem: consume --ListItemIcon-minWidth (was hardcoded 36) so density reaches the icon - Tabs: add --Tabs-minHeight base seam (parent can't read child --Tab-minHeight) + wire MuiTabs
- New /experiments/density-showcase: preset switcher (compact/normal/comfort), live scale readout + per-component token accordion, masonry gallery - Extract shared demos to densityDemos.tsx; fixture imports it - Fixture: --Tabs-minHeight scope, center row Stacks
calc(var(--Chip-height) - inset) per size so they track density; insets reproduce today's medium/small sizes (pixel-identical default)
Signed-off-by: Siriwat K <siriwatkunaporn@gmail.com>
Prefix density tokens, tracking the css-var feature: --mui-Button-pad with cssVariables, bare --Button-pad without. One cached getButtonVars(theme) resolver shared by component + consumer so emitted/targeted names can't drift; as-const key map is the typed handle. Button + OutlinedInput + InputLabel converted; enhanceDensity blocks guarded to match. No theme.vars, no getCssVar, no forced prefix; custom-size inline routing dropped. Default render unchanged (Argos zero-diff). Demo: /experiments/density-var-prefix.
Rename the agnostic seam off the component name and prefix: --Button-pad ->
literal --comp-pad, --OutlinedInput-pad{Block,Inline} -> --comp-pad{Block,Inline},
--InputLabel-y -> --comp-labelY. The seam is the bare root's consumption point, not
a design knob, so it stays literal/unprefixed and out of buttonVars. buttonVars now
holds only the Material UI layer's public sized tokens (which track the prefix).
Document the per-layer naming in ADR-0003.
…e granularity Custom (theme-added) sizes are excluded from density (no inline routing) — the seam is only set for built-in sizes, so the generic-seam inheritance case is a documented non-goal, and we avoid prefixing turning the free inline custom-size string into a theme-dependent read. Record that as a concrete cost of prefixing.
…al-mode Export enhance baked density to raw px + dropped the mechanism. Rewrite to match enhance*Density: - port applyDensity (theme.density + CSS-vars channel: vars.density, wrapped generateThemeVars/generateStyleSheets); no CssBaseline - enhance mutates+returns like the preset (no createTheme re-run -> no dropped cssVars mode); overrides keep var(--mui-density-*) refs - static themes resolve refs -> px at runtime; cssVars keep refs - generated file imports only `type Theme` - exportPayload: drop export-time ref resolution; scale stays bare-key px
- Spacing tab (var-backed theme.spacing, per-preset default compact=6); Stack/Grid/Container demos with subtle dashed tiles; Padding toggle greens Stack/Grid gaps; export bakes spacing (applySpacing, dual-mode) - all canvas demo row Stacks -> spacing 10 - tooltip height-measure targets .MuiTooltip-tooltip, not trigger buttons - how-to dialog: @mui/lab Timeline -> MUI List - enhanceCompactDensity: MenuItem height em + typography lineHeight tune; regenerated emit table / labels / fields
…ug (padding+margin) - select minHeight:auto across all 3 presets; hidden from sidebar (preset-driven, not tunable) - InputBase surfaces under both TextField and Select; Select input slot hidden via per-family denylist - Padding toggle renamed to Bounding box; adds a margin overlay alongside padding - margin overlay is JS-driven (reads live getComputedStyle, not a hardcoded density var) so it tracks sidebar overrides and shows every non-zero side, not just one - wired: InputAdornment, AccordionSummary, Tooltip offset, Checkbox/Radio label-pull margin
…how-to-use copy - add icon button next to Export to copy density.ts to clipboard - MenuItem demo: trailing keyboard-shortcut-style text (sx color text.secondary) - update How-to-use walkthrough: measure overlay, Spacing tab, copy button
11 tasks
…ug overlays (message/action/icon), show family in selector
…tering margins; show family, ChipMatrix row Stack + small avatar/icon demos, margin markers
…eholder/helper roundtrip; drop token-count validation, trust input fragments
…ons), 3 composition demos (full, content-only last-child, action-area grid), child-gap + last-child labels, margin markers
…ine (was padding shorthand); group-level ToggleButton height measure; per-size demo icons; Avatar debug-text; Badge bubble measure; positioned-ring-slots exception (bounding-box kept Badge bubble anchored)
…mal = master dims); size-scoped var knobs; clamped left re-anchor; FormControlLabel in family; gutter-synced label pull via linked writes; SwitchMatrix demo
…e + endAdornment exclusion; tag + separator margin markers; option label polish
… + linked alternativeLabel connector-right; compact 9.5px vertical/alternativeLabel tuning (connector insets/top, StepContent per-layout variants, StepLabel label gap d.md); 2-row demos incl vertical alternativeLabel; ButtonGroup group measure + per-size demos; Button/SvgIcon cross-family sidebar
…el (master zeroes vertical); tokenize vertical-alternativeLabel iconContainer paddingLeft (all presets); collapse both gap rows into 'row label spacing' virtual knob
…hidden/hiddenIn/note self-declared, hiddenFieldIds derived; codegen preserves values + remaps ids); surface Progress family — LinearProgress height raw px 3/4/5, linear demos + measure slot, pinned last in selector (minor single-knob rule)
…ultProps, first-class defaultProp target field in codegen, buildOverrides/export routing with app defaults winning) + MuiDataGrid emissions: rowHeight 40/52/64 + columnHeaderHeight 44/56/68 raw px (grid density prop stays unset; selector = relative override), cell/header/edit insets + title gap + toolbar minHeight/padding/gap + footer minHeight + count gutters + actions gap as steps; playground DataGrid family (core/toolbar/forced-edit demos, debug registries, defaultProps knobs); finalize densityFields registry merge (densityExtraFields removed) + densityRender test
…l slot: upstream key collision with popup shell; menuList under menu: internal slot + body portal; columnsManagement paddings + FormControlLabel row-label gap), filter-form gap/field widths, quick-filter expanded width via variants matcher (ownerState-only state, collapsed keeps --trigger-width), overlay gap, drag/rowspan insets (nested to match upstream specificity), pro header-filter paddings/input margins, grouping indent multiplier var + toggle gutters, premium chrome sizing (sidebar/pivot/AI widths + 44/52/60 header trio + field rows + drop zones; charts mirrors pivot); inline-axis longhand refactor (paddingInline/marginInline where block side is 0); playground: DataGridPremium demos (inline filter/columns panels via basePopper.material.disablePortal, header filters, pre-expanded grouping, no-columns overlay, pivot sidebar) + debug registries + knob notes
…— pulls the row off the sidebar edge so the absolutely-pinned hover drag handle gets space (charts mirrors pivot); knob notes + export baseline
…ops (rowHeight + columnHeaderHeight follow same bases; the grid's fixed x0.7/x1.3 stops can't express the 1.5x comfort ratio — exact bases, density prop unset); tests + export baseline + demo blurbs updated
… ListItemButton, SnackbarContent, BottomNavigation) + close their seam gaps — Toolbar regular responsive minHeight via styleOverrides (mixins untouched; landscape media emitted flat, up(xs) is a no-op wrapper the readback can't resolve); Dialog paper --_dialogMargin var driving margin + all 100%-minus-margin viewport calcs (media guards stay at master boundaries, fullScreen state re-asserted) + DialogActions button gap; PaginationItem paddingInline/marginInline steps + --_height var deriving height and pill radius (h/2); SnackbarContent message paddingBlock + action paddingLeft; BottomNavigationAction width clamps + icon-only centering paddingTop (no-label zero re-asserted); demos: real inline Dialog (fullWidth), ellipsis pagination, icon-only bottom nav, extended Fab control; registries + knob meta (derived rows hidden)
… + frozen re-assert — DialogContent padding into dividers:false (master dividers 16/24 untouched, knob edits can't leak into the state), Dialog paper margin/viewport calcs all scoped fullScreen:false (drops the 4 re-assert rows), BottomNavigationAction paddingTop on the net icon-only-with-label condition (drops the no-label zero re-assert); knob meta rebuilt for the new ids
…t into view — extract slotHighlightSelector from slotHighlightSx, scroll effect on toggle-on/switch (canvas scrolls independently, a big component's slot can sit off-screen exactly while its knobs are edited)
…st one demo (16 recovered) — filter panel goes Premium with a 2-item model (community forbade multi-filtering: gated BOTH logicOperatorInput and panelFooter), core demo preselects 2 rows via rowSelectionModel (initialState.rowSelection is dead config), pivot Columns zone left empty for the placeholder, new tree-data demo (toggle + non-paginated Premium footer = rowCount), new row-spanning multiSelect demo (aria-rowspan=2 exercises the rowspan emission), toolbar gains label prop + prefilled quickFilterValues (control does not mount while collapsed; primitive auto-expands on non-empty value), empty columns panel via getTogglableColumns [] (emptyText), AI panel via preferencePanel initialState + explicit slot (ships null by default), charts panel via GridChartsRendererProxy with a null renderer (proxy ships in the grid package — x-charts-premium only paints; Fields tab flipped by one-shot effect, tab state is internal), preference panels pinned open via slotProps.panel.onClose no-op (GridPanel routes clickaway+Escape through it); SLOT_HIGHLIGHT entries for 2 slots whose utility classes upstream never emits (emotion label class only), padding/margin/measure registries for the newly visible slots incl. pre-existing pivotPanelField margin miss
…wspan multiSelect paddingTop now have live demos
… itemChildrenIndentation via defaultProps string 'var(--mui-density-md)' on Rich+SimpleTreeView (the var is INLINE STYLE on the tree root, styleOverrides can't reach it; a string prop passes verbatim so step semantics survive; knob-typed keys resolve the same way), row height rides upstream's own --TreeView-itemHeight unset-fallback hook (28/32/40), content longhand paddings + gap steps with the paddingLeft depth calc re-emitted on a step base (a shorthand would clobber it); Pickers: one --_daySize private var on the DayCalendar root (NOT DateCalendar — an ancestor copy would be shadowed by the descendant's and kill the knob) re-points PickerDay's --PickerDay-size and drives weekday/week-number boxes + both 6-week container minHeight calcs ((size+4)*6), DateCalendar root box raw per-preset trios (286x278/336x320/406x376 — day-var can't reach an ancestor), header spacing steps with min+maxHeight pinned together (Safari-jump comment upstream) and the toolbar padding scoped to pickerOrientation=portrait (landscape master 16 untouched), year/month buttons + multi-section clock root/item width trios, clock item padding steps with the 2px 4px margin frozen (JS scroll math subtracts the first item's 4px); demos: pre-expanded 3-level RichTreeView + checkbox variant, DateCalendar with week numbers, loading calendar (loadingContainer shares the calc), year+month grids, both digital clocks (DigitalClock width-boxed — items stretch unconstrained), StaticDateTimePicker for the toolbar, fixed dayjs date + weekOfYear plugin (displayWeekNumber calls value.week(), adapter doesn't install it — was an SSR 500); registries: rings/markers/measure entries, SLOT_HIGHLIGHT for the two defaultProps rows; derived var-consumer rows hidden (day var row is the knob, Chip/Dialog precedent). Verified: material+docs types, 57 tests (export snapshot regen reviewed = exactly the new emissions), codegen md5-stable x2 incl. post-prettier, eslint, prettier, browser presence audit both families (compact values live: item 28, day 30, containers 204, buttons 64x32, clock items 32) + locator pulse/scroll end-to-end
…aultProp now emits d.md instead of a hardcoded 'var(--mui-density-md)' string (dual-mode: prefix-aware var ref under cssVariables, raw px on static themes — the hardcoded form broke both; zero table diff in vars mode), DateCalendar maxHeight pinned to the height trio (the PickerViewRoot base clamps maxHeight at 336 — comfort's 406 was silently clipped, scrollHeight now equals clientHeight), missed Year/Month grid spacing knobs: rowGap (12->md / 16->lg) + block padding (6->xs / 8->sm) + 3-per-row columnGap (24->xl) with Year's paddingBlock + both columnGaps scoped to the default 3-per-row variant (the 4-per-row variant redefines padding '0 2px' / columnGap 0 — an unconditional emission would clobber it, negated-variant rule) + Year buttonFiller mirroring the button box (last-row spacer, was left at master 72x36); verified: types, 57 tests (snapshot regen = exactly the additions), codegen md5-stable x2, eslint, prettier, comfort probe 376x406 unclipped + year/month buttons 84x44
…l usage rank — X-inline positions (DataGrid 21 / TreeView 27 / DatePicker 31) replaced by a bottom MUI X group (DataGrid, DatePicker, TreeView, matching the yml's group listing); the 7 non-Weave-ranked families (Toolbar..BottomNavigation) made explicit after the ranked core instead of implicit Infinity-rank, Progress stays pinned after them
…ow — the block padding is the seam (master su(1) 0), an inline knob on the list wrapper had no master value to remap
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.
Playground: https://deploy-preview-48749--material-ui.netlify.app/experiments/density-playground/
Part of #48746 (density RFC). Prototype / experiment — draft, not for merge. The combined, end-to-end preview of the proposed density feature — 10 component families verified and surfaced in the playground so far, more staged in the presets.
What this demonstrates
A holistic, opt-in density layer for
@mui/materialbuilt entirely on emitted themestyleOverrides— no new density prop, no component-source changes, zero cost when unused.enhanceCompactDensity/enhanceNormalDensity/enhanceComfortDensity(packages/mui-material/src/styles/). Each ships a--mui-density-*step scale on the theme's own CSS-variables channel (materialized byThemeProviderat the theme'srootSelector— noCssBaselinerequirement) and emits every covered component's density values asstyleOverridesagainst the selectors the component already ships (size/densevariants, slots). MirrorsenhanceHighContrast;createThemestays untouched.--_*vars emitted inside the overrides — coupled sub-elements derive from one value viacalc, so alignment holds at any density; decided per component.rootby default, on the portal slot where the styled element renders through a portal (Autocompletelistbox, Tooltiptooltip).rowHeightprop), so the presets attach themedefaultProps:rowHeight28/40/60 withcolumnHeaderHeighton the same bases (app defaults win); the grid's owndensityprop stays unset — its fixed ×0.7/×1.3 stops can't express a 1.5× comfort ratio, and the toolbar density selector stays a relative multiplier on top. Insets, toolbar/footer, filter/columns panels, column menu, quick filter (expanded-state via avariantsmatcher), overlays, and Pro/Premium chrome (header filters, grouping indent, pivot/AI/charts panels, sidebar) ride emittedstyleOverrides; grid chrome built from Material components reflows via the Material emissions for free.defaultProps.itemChildrenIndentationcarrying a density-var string (the component sets the indentation var as an inline style on the tree root, sostyleOverridescan't reach it;'var(--mui-density-md)'passes through verbatim — a second flavor of the JS-gated exception where the prop value stays CSS), row height through the component's own--TreeView-itemHeighthook (unset upstream → the no-preset state stays pixel-identical), content paddings/gap via steps with the depth-indent calc re-emitted on a step base. Pickers: one private--_daySizevar on the day-calendar root re-points--PickerDay-sizeand drives the weekday/week-number boxes plus both 6-week containerminHeightcalcs (master values are JS constants baked in at build time); calendar root box, year/month buttons, and clock-section widths as raw per-preset px; header/toolbar/clock-item spacing via steps (toolbar scoped to portrait; clock-item margins frozen where scroll-positioning JS reuses them). Both families' Material-built internals (checkboxes,MenuItemclock items, text fields, action bars) reflow via the Material emissions for free.size, ImageListgap) now expressible via thedefaultPropschannel./experiments/density-playground— flipunset/compact/normal/comfort; per-family demo matrices; every emitted value inspectable and overridable live (density-step keys or raw CSS); scale-step, typography, and spacing knobs; visual-debug overlays (bounding box, text box, outline, measure); export or copy a self-containeddensity.tsto try the feature in any sandbox without upgrading@mui/material.pnpm density:codegensnapshots the three presets into a generated emission table;density.test.tsdiffs table ↔ preset readback so the playground and the presets can't drift.scripts/density-screenshots/(pnpm density:shot) asserts the unconfigured default render is pixel-identical to amasterbaseline atmaxDiffPixels: 0, plus compact/comfort reflow captures.Verification
master(maxDiffPixels: 0); no preset → no--mui-density-*vars emitted.compact/normal/comfortreflow every(variant, size)cell; element-levelsxwins over the presetstyleOverrides.@mui/material+docstypecheck, eslint, and the density parity tests all green.Scope
🤖 Generated with Claude Code