Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ffe175f
[material-ui] Add CSS-var density adapter experiment (Button)
siriwatknp Jun 5, 2026
40ea056
[docs] Fix density experiment CI: Head description, prettier, vale prose
siriwatknp Jun 5, 2026
a5c6e86
[docs] Fix Button a11y snapshot (avoid-inline-spacing) + mobile layou…
siriwatknp Jun 5, 2026
8e355ec
[material-ui] Shorten Button internal density var to unprefixed --_pa…
siriwatknp Jun 5, 2026
2748d1c
[material-ui] Refine Button density adapter: variants-resolved --_pad…
siriwatknp Jun 7, 2026
71c782c
[material-ui] Density adapter for OutlinedInput + generic InputLabel …
siriwatknp Jun 7, 2026
ce6e68c
[docs] Local Playwright density-screenshot harness
siriwatknp Jun 7, 2026
a89c062
[material-ui] Add OutlinedInput inline padding base token
siriwatknp Jun 7, 2026
5bb9e6e
[docs] Add OutlinedInput padInline to density-screenshot fixture scopes
siriwatknp Jun 7, 2026
cce0ad0
[material-ui] OutlinedInput: tokenize padding in place, size both axes
siriwatknp Jun 7, 2026
ac28cca
[docs] Density fixture: drive OutlinedInput inline via per-size tokens
siriwatknp Jun 7, 2026
bad674b
[material-ui] Roll out CSS-var density adapter to dashboard components
siriwatknp Jun 7, 2026
40a893d
[docs] Document the dense state-token pattern
siriwatknp Jun 8, 2026
e7342e4
[material-ui] Density adapter: Checkbox, Radio, Switch via SwitchBase
siriwatknp Jun 8, 2026
28d6b6b
[docs] Document the shared internal base pattern (Recipe C)
siriwatknp Jun 8, 2026
ddfcd90
[material-ui] Switch density: derive pad/travel from interlocked dims
siriwatknp Jun 8, 2026
dd57b71
[docs] Update Switch density: interlocked geometry, derive coupled va…
siriwatknp Jun 8, 2026
7d9679b
[material-ui] Switch: tokenize track gutter (--Switch-<size>-pad)
siriwatknp Jun 8, 2026
ada1d5e
[material-ui] Switch: derive track borderRadius from height + gutter
siriwatknp Jun 8, 2026
c8ca3cb
[material-ui] enhanceDensity: add xxl step, wire Switch dims
siriwatknp Jun 8, 2026
0283500
[material-ui] enhanceDensity: compose Switch dims to match default sizes
siriwatknp Jun 8, 2026
d1f3d0d
[material-ui] Fix density seams: OutlinedInput label, MenuItem icon, …
siriwatknp Jun 8, 2026
7c4b598
[docs] Add density-showcase experiment; share demos with fixture
siriwatknp Jun 8, 2026
95ab46b
[material-ui] Chip: scale avatar/icon/deleteIcon with --Chip-height
siriwatknp Jun 8, 2026
9ee251e
[docs] density demos: drop filled-variant inputs (no density support …
siriwatknp Jun 8, 2026
f4f5f9d
Merge branch 'master' into exp/css-var-density-adapter
siriwatknp Jun 11, 2026
38ea6cc
[docs] density-showcase: add outline toggle to reveal density effect
siriwatknp Jun 11, 2026
1a52962
[material-ui] Add CSS-var color/state adapter (Button, MenuItem)
siriwatknp Jun 11, 2026
8f29d04
[docs] Add color/state adapter showcase experiments
siriwatknp Jun 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,8 @@ test-results/
# typescript
*.tsbuildinfo
next-env.d.ts

# density-adapter screenshot harness (local-only outputs)
/density-screenshots/
/scripts/density-screenshots/__baselines__/
/scripts/density-screenshots/.playwright-output/
331 changes: 331 additions & 0 deletions CONTEXT.md

Large diffs are not rendered by default.

286 changes: 286 additions & 0 deletions docs/adr/0001-css-var-density-adapter.md

Large diffs are not rendered by default.

192 changes: 192 additions & 0 deletions docs/adr/0002-css-var-color-state-adapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
# Color/state tuning via a 3-layer CSS-var adapter, standardized across states

Component color (background / foreground / border) is exposed as public CSS
variables — **per variant, per palette color, per interaction state** — layered
**over the pre-existing `--variant-*` seam** and resolved through a private
per-state default, so a designer can tune any state's color without editing
component source. This is the **color axis** sibling of the dimension/density
adapter (ADR-0001); the two share the three-layer reading.

## Context

We want designers to recolor a component per `(variant, color, state)` — e.g.

Check warning on line 12 in docs/adr/0002-css-var-color-state-adapter.md

View workflow job for this annotation

GitHub Actions / test-dev (ubuntu-latest)

[vale] reported by reviewdog 🐶 [Google.We] Try to avoid using first-person plural like 'We'. Raw Output: {"message": "[Google.We] Try to avoid using first-person plural like 'We'.", "location": {"path": "docs/adr/0002-css-var-color-state-adapter.md", "range": {"start": {"line": 12, "column": 1}}}, "severity": "WARNING"}
"make the hover background of contained error buttons hot pink", "tint disabled
buttons per color" — without editing source, and with **today's exact colors
when nothing is set** (Argos zero-diff).

Two facts shape the design:

- **There is already a color seam.** Button has carried `--variant-containedBg`,
`--variant-outlinedColor`, `--variant-outlinedBorder`, `--variant-textBg`, …
since the CSS-extraction conversion (#41378). The per-color `variants` block
sets these, the variant block consumes them, and `&:hover` **reassigns the
value**. It is undocumented and unprefixed. We **adopt it as the seam**, not

Check warning on line 23 in docs/adr/0002-css-var-color-state-adapter.md

View workflow job for this annotation

GitHub Actions / test-dev (ubuntu-latest)

[vale] reported by reviewdog 🐶 [Google.We] Try to avoid using first-person plural like 'We'. Raw Output: {"message": "[Google.We] Try to avoid using first-person plural like 'We'.", "location": {"path": "docs/adr/0002-css-var-color-state-adapter.md", "range": {"start": {"line": 23, "column": 47}}}, "severity": "WARNING"}
replace it (a wholesale rewrite would be a large, pointless diff).
- **The seam is variant-qualified, not agnostic.** Unlike the dimension seam
(`--Button-pad` carries no variant meaning), "background" means a solid for
`contained` and transparent for `text`, so the variant lives in the seam name.
The public token inherits that — variant leads the token, aligning with the
seam.

Constraints inherited from ADR-0001: color-identical default, no JS conditional
in the styles body (states/colors are `variants`/pseudo-class cells), existing
`styleOverrides`/`sx` keep working.

## Decision

### The token shape

```text
--Component-<variant>-<color>-<state>-<prop> public color token (designer knob)
--variant-<variant><Prop> seam (pre-existing consumption point)
--_<variant><Prop> private default (advanced per state)
```

- `<prop> ∈ {bg, fg, border}` — `fg` = foreground/text color (not `color`, which
would collide with the palette `<color>` segment); `border` = border **color**
on this axis (bare name kept for now — see _Accepted trade-offs_).
- `<state> ∈ {hover, focus, active, disabled, selected}`; the **rest** state omits
the `<state>` segment. `focus` maps to the `.Mui-focusVisible` selector
(keyboard focus), not `:focus`.
- `<color>` is built from the per-color loop variable, so **custom palette colors
get tokens for free** (parallel to custom-size routing in ADR-0001).

Resolution is **per `(variant, color, state, prop)`** — the most specific
meaningful granularity, no coarse color-agnostic layer (a single
`--Button-contained-hover-bg` would force every color to one value). This mirrors
the dimension axis dropping the all-sizes base token.

### Three layers — value-states vs inert-states

`--_<vp>` holds the prop's **current resolved value** — the value the element
shows now, *including any rest/hover/disabled token override*. The crux is **where
a state's token lives**:

- **Value-state** (the state genuinely sets the prop today — rest, hover for bg,
disabled): its token goes **inside** `--_<vp>`, with the palette literal as the
fallback; the seam just reads `var(--_<vp>)`. The override is now in the default,
so every later state inherits it.
- **Inert state** (the state changes nothing today — focus, active on Button):
its token routes in the **seam over** `var(--_<vp>)`. Settable, but unset it
tracks the default.

```js
// per-color variants block (one per palette color)
{
props: { color: 'error' /* loop var */ },
style: {
// rest (value-state): token INSIDE the default, seam reads it
'--_containedBg': 'var(--Button-contained-error-bg, var(--mui-palette-error-main))',
'--variant-containedBg': 'var(--_containedBg)',

'@media (hover: hover)': {
'&:hover': {
// hover (value-state for bg): advance the default with the hover token inside
'--_containedBg': 'var(--Button-contained-error-hover-bg, var(--mui-palette-error-dark))',
'--variant-containedBg': 'var(--_containedBg)',
},
},

// active (inert — Button moves only box-shadow): route the token over the default.
// Unset → tracks --_containedBg, which already carries any rest/hover override
// (so a re-coloured button stays re-coloured on press; press-while-hover = dark).
'&:active': {
'--variant-containedBg': 'var(--Button-contained-error-active-bg, var(--_containedBg))',
},

// disabled (value-state): advance with the disabled token, neutral grey fallback
[`&.${buttonClasses.disabled}`]: {
'--_containedBg':
'var(--Button-contained-error-disabled-bg, var(--mui-palette-action-disabledBackground))',
'--variant-containedBg': 'var(--_containedBg)',
},
},
}
```

> **Correction (post-implementation).** This ADR's first draft put *every* state's
> token in the seam *over* a `--_<vp>` that held the bare palette literal. That
> breaks override propagation: setting only the **rest** token left focus/active
> falling back to the palette literal, so a re-coloured button **snapped to the
> palette colour on focus** (a re-greyed secondary flashed default purple). The
> structure above — value-state token *inside* the default — is the fix.

### Standardize the full state set, even where inert

Every component exposes the **same** non-rest states
(`hover/focus/active/disabled/selected`) for each variant×color×prop, **even
where the component changes nothing in that state today** (the routing is then
inert). A designer can rely on `--Button-<v>-<c>-disabled-bg` existing without
reading source. This is the opposite stance from the dimension axis, where a
token exists only where the axis is genuinely density-bearing — colour states are
a small, fixed, universal vocabulary, so a *predictable* surface beats a *minimal*
one.

### Why three vars, not two

The simpler design — seam + public token, with the **palette var as the inline
fallback** (`--variant-containedBg: var(--token, var(--mui-palette-error-main))`)
— works only if we tokenize *just the states the component already styles*. The

Check warning on line 129 in docs/adr/0002-css-var-color-state-adapter.md

View workflow job for this annotation

GitHub Actions / test-dev (ubuntu-latest)

[vale] reported by reviewdog 🐶 [Google.We] Try to avoid using first-person plural like 'we'. Raw Output: {"message": "[Google.We] Try to avoid using first-person plural like 'we'.", "location": {"path": "docs/adr/0002-css-var-color-state-adapter.md", "range": {"start": {"line": 129, "column": 17}}}, "severity": "WARNING"}
moment we standardize an **inert** state, two layers break it:

Check warning on line 130 in docs/adr/0002-css-var-color-state-adapter.md

View workflow job for this annotation

GitHub Actions / test-dev (ubuntu-latest)

[vale] reported by reviewdog 🐶 [Google.We] Try to avoid using first-person plural like 'we'. Raw Output: {"message": "[Google.We] Try to avoid using first-person plural like 'we'.", "location": {"path": "docs/adr/0002-css-var-color-state-adapter.md", "range": {"start": {"line": 130, "column": 8}}}, "severity": "WARNING"}

- An inert `&:active` block must set `--variant-containedBg` (so its token is
live), and an **unset** token must reproduce today's active color — which is the
**hover** color when pressing-while-hovering. A two-layer fallback can only point
at the *rest* palette var, so an unset active token would clobber hover on every
click. A `var(--token, var(--variant-containedBg))` self-reference is
guaranteed-invalid CSS.
- The **private default** `--_<variant><Prop>` captures "the value this element
currently shows": value-states write their token *into* it, inert states read it
as their fallback — so inert states track the live value (including overrides)
instead of overwriting it. (An inert state can't write the default itself: a
`--_<vp>: var(--token, var(--_<vp>))` self-reference is guaranteed-invalid CSS.)

So the three-layer shape is **forced by standardization**, restoring symmetry with
the dimension axis (which is also three-layer).

### No `enhanceColor` — the palette is the holistic surface

The dimension axis needed `enhanceDensity` because it invented a new tier-1 scale
(`--mui-density-*`). Color already **has** its holistic dial: the **palette**.
Tuning `palette.error.dark` reflows every contained-error hover for free (the
tokens fall back to it). The color tokens exist for **per-component-state
deviations** from the palette, not a parallel global scale. So there is no
`enhanceColor` function and no color "scale" node — `createTheme`'s palette is
tier-1, the color tokens are tier-2.

## Alternatives considered

- **Two-layer (seam + token, palette fallback).** Smaller, no private var.
Rejected: cannot keep inert standardized states zero-diff (the hover/active
clobber above).
- **Tokenize only where color varies today.** Smallest bundle. Rejected: the
surface becomes unpredictable — a designer must read each component's source to
learn which state knobs exist.
- **Adopt `--variant-*` as the public API, or replace it.** Rejected: it's
unprefixed and variant-qualified in a way we don't want as a contract; layering

Check warning on line 166 in docs/adr/0002-css-var-color-state-adapter.md

View workflow job for this annotation

GitHub Actions / test-dev (ubuntu-latest)

[vale] reported by reviewdog 🐶 [Google.We] Try to avoid using first-person plural like 'we'. Raw Output: {"message": "[Google.We] Try to avoid using first-person plural like 'we'.", "location": {"path": "docs/adr/0002-css-var-color-state-adapter.md", "range": {"start": {"line": 166, "column": 45}}}, "severity": "WARNING"}
a documented public token over it costs less and keeps the seam an internal
detail.
- **A coarse color-agnostic token** (`--Button-contained-hover-bg`). Rejected:
forces all palette colors to one value, destroying per-color semantics.

## Consequences

- **Color-identical default & non-breaking.** Unset tokens fall back through the
private default to today's palette var; `styleOverrides`/`sx` still win.
- **Per-state color is independently tunable**, including states the component
doesn't style today, via a predictable, uniform token surface.
- **Custom palette colors are tunable for free** (token name built from the loop
variable).
- **Bundle grows** with inert routing per component — the price of the standard.
- The **private default looks redundant** next to the palette var until you hit
the hover/active interplay; it is mechanical and documented here.

### Accepted trade-offs

| Trade-off | Why we can live with it |

Check warning on line 186 in docs/adr/0002-css-var-color-state-adapter.md

View workflow job for this annotation

GitHub Actions / test-dev (ubuntu-latest)

[vale] reported by reviewdog 🐶 [Google.We] Try to avoid using first-person plural like 'we'. Raw Output: {"message": "[Google.We] Try to avoid using first-person plural like 'we'.", "location": {"path": "docs/adr/0002-css-var-color-state-adapter.md", "range": {"start": {"line": 186, "column": 19}}}, "severity": "WARNING"}
| :-- | :-- |
| Inert state routing emitted on every component | Buys a predictable surface (knob exists without reading source); colour states are a small fixed set. |
| Three vars per `(variant, prop)` instead of two | Forced by standardizing inert states (see _Why three vars_); restores symmetry with the dimension axis. |
| `border` under-specifies (width? style? color?) | On this axis it is always **color**; `bc` reads as background-color and `borderColor` is verbose. **Revisit if a border-width (dimension) token ever appears.** |
| `--variant-*` seam stays unprefixed/undocumented as plumbing | It's the consumption point, not the contract; the public token is the documented knob. |
| Disabled becomes per-color-tunable though Button uses a flat grey today | Default is the same `palette.action.*` literal for every colour (zero-diff); the per-color knob is upside, not cost. |
Loading
Loading