Plan the Material 3 alignment, with the audits and Google's references
Every foundations, styles and components page of m3.material.io (238, from the sitemap) extracted into docs/reference/m3, five audit reports with 142 findings in docs/audits/m3-alignment, and the 2.0.0 plan in docs/plans/material-3-alignment.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
b1fc0c9cfa
commit
651a513d1e
@@ -0,0 +1,811 @@
|
||||
# Audit: actions and communication
|
||||
|
||||
Scope: `<x-button>`, `<x-button-group>`, `<x-group>`, `<x-split-button>`, `<x-fab>`, `<x-fab-menu>`,
|
||||
`<x-fab-menu-item>`, `<x-menu>`/`<x-menu-item>`/`<x-menu-group>`/`<x-menu-separator>`, `<x-badge>`,
|
||||
`<x-alert>`, `<x-progress>`, `<x-loading>`, `<x-toast>`, `<x-tooltip>`, `<x-rich-tooltip>`,
|
||||
`<x-stat>`, `<x-empty-state>`, plus `resources/css/components/{groups,menu}.css`,
|
||||
`resources/css/tokens/{state,motion,shape}.css`, `resources/js/{menu,snackbar,tooltip,rich-tooltip,progress,figure}.js`
|
||||
and `src/Concerns/Toasts.php`.
|
||||
|
||||
Sources are named per finding. `RC-A` = `docs/reference/m3/reference-components-a.md`, `RF` =
|
||||
`reference-foundations.md`, `RS` = `reference-styles.md`, `tokens/X.kt` = the androidx Compose
|
||||
Material 3 token file in `docs/reference/m3/tokens/`. Two numbers were fetched live from androidx-main
|
||||
during this audit (`Button.kt`, `ButtonGroup.kt`) and are marked as such.
|
||||
|
||||
## Summary
|
||||
|
||||
The button family is the strongest part of this library: all five Expressive sizes, the icon-button
|
||||
width matrix, the corner/press-morph scale, the outline widths, the per-variant toggle colours and
|
||||
the per-size type roles match the androidx tokens *exactly*, digit for digit — I could not find a
|
||||
single wrong number in `<x-button>`'s own size tables, and the icon-button widths (narrow/default/wide
|
||||
× five sizes, fifteen values) are all correct. The problems are concentrated elsewhere: the split
|
||||
button reuses the connected button group's corner variables, so its inner corners are wrong at four of
|
||||
five sizes and morph in the *opposite* direction to M3 on press; the snackbar's live region is created
|
||||
together with its message (so the announcement is unreliable) and auto-dismisses even when it carries
|
||||
an action, which M3's accessibility page forbids outright; a spinner inside a filled button is drawn
|
||||
in `text-primary` on a `bg-primary` container, i.e. invisible; `<x-group>`'s segments carry no 48 px
|
||||
touch target; and `<x-menu>` has no maximum height, so a long menu runs off the viewport with no way
|
||||
to scroll it. Colour is animated on the overshooting *spatial* spring in four components, which the
|
||||
library's own `motion.css` comment says must never happen. Nothing in this group implements a
|
||||
deprecated M3 component — segmented buttons and the small FAB are correctly absent — and the two
|
||||
non-M3 pieces (alert, stat, empty state) stay inside M3's colour roles, type scale and shape scale.
|
||||
|
||||
Counts: 5 must-fix, 17 should-fix, 14 nice-to-have (36 findings).
|
||||
|
||||
## Findings
|
||||
|
||||
### ACT-01 · button / loading · A button's spinner is invisible on a filled or tonal button
|
||||
- Severity: must-fix
|
||||
- M3 says: "when embedded in another component (e.g. a button), the active indicator should match the
|
||||
label/icon color and the **track should be removed** in that context" (RC-A, Progress indicators →
|
||||
Accessibility; same rule repeated for the loading indicator: "when embedded in another component
|
||||
(e.g. a button), the indicator needs ≥3:1 against that component").
|
||||
- Library does: `resources/views/components/button.blade.php:207` renders
|
||||
`<x-loading :class="$iconSize" :label="false" />` — only a size class. `loading.blade.php:22`
|
||||
detects a caller colour with `preg_match('/(^|\s)text-(?!…)/')`, which `size-5` does not match, so
|
||||
`loading.blade.php:31` adds `text-primary`. The SVG is `fill="currentColor"`
|
||||
(`resources/svg/loading-indicator/*.svg`), so a `spinner` on `<x-button variant="filled">`
|
||||
(`bg-primary`) paints primary on primary — contrast 1:1. Same on `tonal` (primary on
|
||||
secondary-container) and on `variant="filled" color="error"`.
|
||||
- Fix: in `button.blade.php:207` pass `:class="$iconSize.' text-current'"` — `text-current` satisfies
|
||||
`loading.blade.php`'s `$coloured` test, so no `text-primary` is added and the indicator inherits the
|
||||
button's label colour.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### ACT-02 · toast · The snackbar's live region is created together with its message
|
||||
- Severity: must-fix
|
||||
- M3 says: "Announcement: use a **live region with a 'polite'/queued** announcement (not assertive) on
|
||||
Android and web" (RC-A, Snackbar → Accessibility).
|
||||
- Library does: `resources/views/components/toast.blade.php:41` wraps the whole snackbar in
|
||||
`<template x-if="current">`, and the element carrying `role` and `aria-live="polite"` is inside it
|
||||
(`toast.blade.php:45-46`). The region therefore enters the DOM with its text already present; a
|
||||
live region must exist beforehand and have its *contents* change for assistive technology to
|
||||
announce it reliably. The outer wrapper (`toast.blade.php:33`) is permanent but carries no
|
||||
`aria-live`. Secondary issue on the same element: `x-bind:role` sets `alert` (implicitly assertive)
|
||||
while `aria-live="polite"` is hard-coded beside it; the explicit `aria-live` wins, so the intent
|
||||
("errors interrupt") is not realised anyway — and M3 asks for polite regardless.
|
||||
- Fix: move `aria-live="polite"` and `aria-atomic="true"` onto the permanent wrapper at
|
||||
`toast.blade.php:33`, drop `x-bind:role` / `aria-live` from the inner div (or keep `role="status"`
|
||||
on the wrapper). The text nodes then change inside a region that was already there.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### ACT-03 · toast / snackbar.js · A snackbar with an action still auto-dismisses
|
||||
- Severity: must-fix
|
||||
- M3 says: "**Snackbars with an action must never auto-dismiss** — let people read/act at their own
|
||||
pace; snackbars without actions may auto-dismiss (common range 4-10 seconds)" (RC-A, Snackbar →
|
||||
Accessibility). The guidelines page repeats it as a design requirement: "make the snackbar
|
||||
actionable so it doesn't auto-dismiss".
|
||||
- Library does: `resources/js/snackbar.js:60` sets `timeout: sticky || toast.timeout === 0 || … ? 0 :
|
||||
(toast.timeout ?? DEFAULT_TIMEOUT_MS)` — the presence of `action` is not considered, so
|
||||
`materialToast('Share deleted', { action: { label: 'Undo', … } })` (exactly the showcase example,
|
||||
`showcase/sections/communication.blade.php:22`) disappears after 4 s. Hover/focus pauses the timer
|
||||
(`snackbar.js:97-117`), which helps a mouse user but not a screen-reader or keyboard user who has
|
||||
not yet reached it.
|
||||
- Fix: in `snackbar.js:55-63`, treat an entry with `action` as untimed: `timeout: sticky ||
|
||||
toast.action || toast.timeout === 0 … ? 0 : …`. The close button already appears for an actioned
|
||||
toast (`toast.blade.php:76`), so dismissal stays possible. Let an explicit `timeout` still override
|
||||
it if the caller insists.
|
||||
- Effort: S
|
||||
- Breaks API? no (behaviour change only; `timeout` keeps working)
|
||||
|
||||
### ACT-04 · menu · A long menu has no maximum height and cannot be scrolled
|
||||
- Severity: must-fix
|
||||
- M3 says: "Scrolling: a menu that can't fit all items **scrolls and shows a persistent scrollbar**"
|
||||
(RC-A, Menus → Behaviour and guidelines).
|
||||
- Library does: `resources/views/components/menu.blade.php:70` sets
|
||||
`min-w-28 max-w-70 overflow-visible … [inset:auto]` — there is no `max-block-size` and overflow is
|
||||
explicitly *visible*. A popover positioned with `position-area` and `inset: auto` is sized
|
||||
`fit-content`; it is not clipped to the position area, so a menu with (say) 20 items extends past
|
||||
the viewport and the items beyond the edge cannot be reached — the top layer does not scroll with
|
||||
the page. The library's own dropdown CSS already does this correctly:
|
||||
`resources/css/components/menu.css:24-25` (`max-block-size: 18rem; overflow-y: auto`) and
|
||||
`menu.css:99` for `::picker(select)`.
|
||||
- Fix: on `menu.blade.php:70` replace `overflow-visible` with
|
||||
`max-h-72 overflow-y-auto` (18rem, matching `.field-menu`), or
|
||||
`max-h-[min(18rem,calc(100vh-2rem))]`. Verify in Chrome that `position-try-fallbacks` still flips
|
||||
before the cap bites.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### ACT-05 · group / button-group · Connected segments miss M3's 48 dp target and 48 dp minimum width
|
||||
- Severity: must-fix
|
||||
- M3 says: "Each button needs a minimum **48x48dp** target; XS/S groups get extra inner padding
|
||||
specifically to preserve this — **don't reduce it**" and "Minimum widths: XS and S connected button
|
||||
groups have a 48dp target area and a **48dp minimum width**" (RC-A, Button groups → Accessibility
|
||||
and → Specs). RF:78 repeats the 48 × 48 rule, RF:82 gives "at least 48 × 48 **CSS pixels**" for web.
|
||||
- Library does: `<x-button>` solves this for itself with the pseudo-element at
|
||||
`button.blade.php:172` (`after:min-h-12 after:min-w-12` on `xs`/`sm`). `<x-group>` does **not**: its
|
||||
segments are plain `<label>`s (`group.blade.php:75-83`) sized `h-8` (xs) or `h-10` (sm)
|
||||
(`group.blade.php:48-49`) with no target expansion, so the whole `<x-group size="sm">` — the
|
||||
default, and the only form in the showcase — offers a 40 px-tall target. Separately, nothing
|
||||
enforces the 48 dp *minimum width*: a connected `<x-button-group connected>` of `sm` icon buttons
|
||||
is 40 px wide per segment (`button.blade.php:141`), and `groups.css` has no `min-width`.
|
||||
- Fix: (a) add the same pseudo-element to `group.blade.php:75-83` for `xs`/`sm`
|
||||
(`after:absolute after:top-1/2 after:left-1/2 after:size-full after:min-h-12 after:min-w-12
|
||||
after:-translate-x-1/2 after:-translate-y-1/2`); (b) in `groups.css`, add
|
||||
`[data-button-group='connected'][data-size='xs'] > *, [data-button-group='connected'][data-size='sm'] > * { min-inline-size: 3rem; }`.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### ACT-06 · split-button · Inner corners are wrong at four of five sizes and morph the wrong way
|
||||
- Severity: should-fix
|
||||
- M3 says (RC-A, Split button → Specs, from `SplitButton{Size}Tokens.kt`, cross-checked against the
|
||||
site's own "inner corner radius" figure):
|
||||
|
||||
| Size | Inner corner (rest) | Inner corner (hover/press) |
|
||||
| --- | --- | --- |
|
||||
| XS | 4dp | 8dp |
|
||||
| S | 4dp | 12dp |
|
||||
| M | 4dp | 12dp |
|
||||
| L | 8dp | 20dp |
|
||||
| XL | 12dp | 20dp |
|
||||
|
||||
Verified directly: `tokens/SplitButtonSmallTokens.kt` — `InnerCornerCornerSize =
|
||||
CornerValueExtraSmall`, `InnerHoveredCornerCornerSize = InnerPressedCornerCornerSize =
|
||||
CornerValueMedium`; `tokens/SplitButtonMediumTokens.kt` — the same pair. The inner corner **grows**
|
||||
under the finger.
|
||||
- Library does: `resources/views/components/split-button.blade.php:39` emits
|
||||
`data-button-group="split"`, and `resources/css/components/groups.css:54-66` gives `[data-split]`
|
||||
the *connected group's* variables — `--group-inner` (rest) and `--group-inner-pressed` (pressed).
|
||||
Those are 8 px/4 px at `sm` and `md`, 16 px/12 px at `lg`, 20 px/16 px at `xl`
|
||||
(`groups.css:32-36`). So rest is 8/8/16/20 px where M3 wants 4/4/8/12, and pressed *shrinks* to
|
||||
4/4/12/16 px where M3 wants it to *grow* to 12/12/20/20. Only `xs` is right at rest (4 px), and its
|
||||
pressed value (2 px, `groups.css:32`) should be 8 px.
|
||||
- Fix: give `[data-split]` its own variables in `groups.css` rather than reusing `--group-inner*`,
|
||||
e.g. `[data-button-group='split'][data-size='sm'] { --split-inner: var(--md-sys-shape-corner-xs);
|
||||
--split-inner-pressed: var(--md-sys-shape-corner-md); }` per size, and point the `[data-split]` /
|
||||
`[data-split]:active` rules (`groups.css:54-66`) at them.
|
||||
- Effort: M
|
||||
- Breaks API? no
|
||||
|
||||
### ACT-07 · split-button · The trailing menu icon is the wrong size at every size step
|
||||
- Severity: should-fix
|
||||
- M3 says: trailing icon 22dp (XS), 22dp (S), 26dp (M), 38dp (L), 50dp (XL) (RC-A, Split button →
|
||||
Specs; `tokens/SplitButtonSmallTokens.kt` `TrailingIconSize = 22.0.dp`,
|
||||
`SplitButtonMediumTokens.kt` `= 26.0.dp`).
|
||||
- Library does: `split-button.blade.php:56-66` renders the trailing half as a plain icon-only
|
||||
`<x-button>`, which picks its icon size from the icon-button table
|
||||
(`button.blade.php:155`): 20 / 24 / 24 / 32 / 40 px. Every step is undersized (S is 24 vs 22 —
|
||||
oversized — and M/L/XL are 2/6/10 px small). The trailing *button widths* are all correct
|
||||
(48/48/56/96/136 px, `split-button.blade.php:36` plus the icon-button defaults), so only the glyph
|
||||
is off.
|
||||
- Fix: pass an explicit icon size from `split-button.blade.php`, e.g. add
|
||||
`['xs' => 'size-5.5', 'sm' => 'size-5.5', 'md' => 'size-6.5', 'lg' => 'size-9.5', 'xl' => 'size-12.5'][$size]`
|
||||
to the trailing button's `:class` (Tailwind's 4 px scale gives 22 px as `size-5.5`).
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### ACT-08 · fab · The extended FAB's icon↔label gap is too small at md and lg
|
||||
- Severity: should-fix
|
||||
- M3 says: icon↔label gap 8dp (small), **16dp** (medium), **20dp** (large) — RC-A, Extended FAB →
|
||||
Specs; verified in `tokens/ExtendedFabMediumTokens.kt` (`IconLabelSpace = 16.0.dp`) and
|
||||
`tokens/ExtendedFabLargeTokens.kt` (`IconLabelSpace = 20.0.dp`).
|
||||
- Library does: `resources/views/components/fab.blade.php:39` — `gap-2` (8 px) small ✓,
|
||||
`gap-3` (12 px) medium ✗, `gap-4` (16 px) large ✗.
|
||||
- Fix: `gap-4` for `md` and `gap-5` for `lg` in `fab.blade.php:39`. (Heights, corners, icon sizes and
|
||||
the 26 px/28 px paddings on the same line are all correct.)
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### ACT-09 · button · The xs button's horizontal padding is 12 px, not 16 dp
|
||||
- Severity: should-fix
|
||||
- M3 says: XS leading/trailing padding = **16dp** (RC-A, Buttons → Specs;
|
||||
`tokens/ButtonXSmallTokens.kt` `LeadingSpace = 16.0.dp`, `TrailingSpace = 16.0.dp`). The variants
|
||||
table on the same page makes the point twice: 24 dp is "Not recommended. Use 16dp", and 16 dp is
|
||||
the Expressive value.
|
||||
- Library does: `button.blade.php:147` — `'xs' => 'h-8 gap-2 px-3 …'` (12 px). The same 12 px is
|
||||
written into the group spacing (`groups.css:32`, `--group-pad: 0.75rem`) and into `<x-group>`'s
|
||||
segments (`group.blade.php:48`), so all three are consistently 4 px short.
|
||||
- Fix: `px-4` in `button.blade.php:147` and `group.blade.php:48`; `--group-pad: 1rem` in
|
||||
`groups.css:32`.
|
||||
- Effort: S
|
||||
- Breaks API? no (visual width of xs buttons changes by 8 px)
|
||||
|
||||
### ACT-10 · button-group · A standard group is allowed to wrap onto a second line
|
||||
- Severity: should-fix
|
||||
- M3 says: "Adaptive/resizing: groups move through layouts as **a single line and never wrap to a
|
||||
second line**; multiple groups can stack vertically but don't interact with each other" (RC-A,
|
||||
Button groups → Behaviour and guidelines).
|
||||
- Library does: `button-group.blade.php:35` adds `flex-wrap` to every non-connected group. On a
|
||||
narrow window a three-button group breaks into two rows, and the press-expansion rules in
|
||||
`groups.css:38-48` (which only look at `+` siblings) then compress a neighbour on a different line.
|
||||
- Fix: drop `'flex-wrap' => ! $connected` from `button-group.blade.php:35`. If overflow is a concern,
|
||||
M3's answer is a smaller size or fewer buttons, not wrapping.
|
||||
- Effort: S
|
||||
- Breaks API? no (a class hook disappears from the rendered output)
|
||||
|
||||
### ACT-11 · menu-item · Leading and trailing space is 12 px where the Expressive menu wants 16 dp
|
||||
- Severity: should-fix
|
||||
- M3 says: `tokens/SegmentedMenuTokens.kt` — `ItemLeadingSpace = 16.0dp`, `ItemTrailingSpace = 16.0dp`,
|
||||
`ItemBetweenSpace = 12.0dp`. (The site publishes no numbers for the Expressive vertical menu — RC-A,
|
||||
Menus → Specs says the measurements section is diagram-only — so the token file is the only source,
|
||||
and it is the one the component's own header comment cites.)
|
||||
- Library does: `menu-item.blade.php:46` — `px-3` (12 px) with `gap-3` (12 px). The gap is right; the
|
||||
side padding is the *baseline* menu's 12 dp (RC-A, Menus → Specs, baseline table), not the
|
||||
Expressive one. `menu-group.blade.php:6` and `menu-separator.blade.php:3` inherit the same 12 px.
|
||||
- Fix: `px-4` on `menu-item.blade.php:46`, and `px-4`/`mx-4` on the group label and the separator so
|
||||
the list stays aligned.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### ACT-12 · menu-separator · Divider padding is 4 px, half of M3's 8 dp
|
||||
- Severity: should-fix
|
||||
- M3 says: "Divider top/bottom padding | **8dp**; Divider height | 1dp" (RC-A, Menus → Specs).
|
||||
- Library does: `menu-separator.blade.php:3` — `my-1` (4 px) with `h-px` ✓ and
|
||||
`bg-outline-variant` ✓.
|
||||
- Fix: `my-2` in `menu-separator.blade.php:3`.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### ACT-13 · menu.js · Disabled menu items cannot be focused
|
||||
- Severity: should-fix
|
||||
- M3 says: "Interactability: **disabled items can still receive focus** (just aren't selectable);
|
||||
dividers and gaps can never receive focus" (RC-A, Menus → Accessibility). The guidelines page adds
|
||||
"Disable a menu item that's conditionally unavailable rather than removing it" — the point of
|
||||
keeping it is that people can find out it exists.
|
||||
- Library does: `resources/js/menu.js:183` filters them out —
|
||||
`.filter((item) => item.getAttribute('aria-disabled') !== 'true')` — so arrow keys, Home/End and
|
||||
typeahead skip them entirely, and `menu-item.blade.php:53` also sets `pointer-events-none`. A
|
||||
disabled item is announced only if the user happens to read the menu with a virtual cursor.
|
||||
- Fix: keep disabled items in `items()` and instead refuse activation (the check already exists in
|
||||
`menu.js:237`). Two lists are cleanest: `items()` for focus movement, `items().filter(enabled)` for
|
||||
activation. `menu-item.blade.php:53` can keep `pointer-events-none` for the pointer.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### ACT-14 · fab · The FAB's icon is drawn outlined
|
||||
- Severity: should-fix
|
||||
- M3 says: "Icon (should be **filled**, not outlined…)" (RC-A, FAB → Anatomy) and "icon should be
|
||||
filled (not outlined) and unambiguous" (→ Behaviour and guidelines). The same rule is why
|
||||
`<x-button>` fills a *selected* toggle's icon.
|
||||
- Library does: `fab.blade.php:68` renders `<x-icon :name="$icon" :class="$iconSize" />` with no
|
||||
`filled`, so the Material Symbol's outlined face is used. `fab-menu.blade.php:53` has the same
|
||||
issue for the FAB-menu trigger, and `fab-menu-item.blade.php:43` for its items.
|
||||
- Fix: add `filled` to `fab.blade.php:68` and `fab-menu.blade.php:53`. (Leave the FAB-menu *items*
|
||||
outlined only if you decide they read as list items rather than FABs; M3's FAB-menu anatomy shows
|
||||
filled icons there too.)
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### ACT-15 · button / icon button · A default (non-toggle) icon button uses an outlined icon
|
||||
- Severity: should-fix
|
||||
- M3 says: "**Default icon buttons should use filled icons**; toggle buttons use outlined icon
|
||||
unselected → filled icon selected" (RC-A, Icon buttons → Behaviour and guidelines).
|
||||
- Library does: `button.blade.php:213` — `:filled="$selected === true"`. For an icon button that is
|
||||
not a toggle (`$selected === null`) that evaluates to `false`, so the outlined face is drawn where
|
||||
M3 asks for the filled one. The toggle half of the rule is implemented correctly.
|
||||
- Fix: `:filled="$selected === true || ($iconOnly && $selected === null)"` in `button.blade.php:213`.
|
||||
This is a visible, opinionated change across every icon button in an application — if the library
|
||||
prefers outlined icon buttons, say so in the header comment as a deliberate deviation rather than
|
||||
leaving it implicit.
|
||||
- Effort: S
|
||||
- Breaks API? no (visual change)
|
||||
|
||||
### ACT-16 · fab · `disabled` exists on a component M3 says must never be disabled
|
||||
- Severity: should-fix
|
||||
- M3 says: "**Never disable a FAB** — if its action is unavailable, remove the FAB entirely instead"
|
||||
(RC-A, FAB → Accessibility). RF:389 repeats it from the states side: "'if the action… is
|
||||
unavailable, the FAB shouldn't appear' (don't just disable it) — a Don't example specifically about
|
||||
FABs". The same page's inheritance table lists FABs under "Disabled … NOT inherited by" (RF:381).
|
||||
- Library does: `fab.blade.php:23` declares a `disabled` prop and `fab.blade.php:52` paints the full
|
||||
disabled treatment (`disabled:bg-on-surface/10 disabled:text-on-surface/38 disabled:shadow-none`).
|
||||
Nothing in the header comment or in SKILL.md (`### <x-fab>`, line 277) warns against it — SKILL.md
|
||||
simply lists `disabled` among the props.
|
||||
- Fix: cheapest honest fix is documentation — say in `fab.blade.php`'s header and in SKILL.md that M3
|
||||
forbids a disabled FAB and that the prop exists only for a form-submit FAB that must block a double
|
||||
submit. Removing the prop is the M3-pure option and would break callers.
|
||||
- Effort: S
|
||||
- Breaks API? no (if documented), yes (if removed)
|
||||
|
||||
### ACT-17 · toast / button · The snackbar covers the `fab` button on a phone
|
||||
- Severity: should-fix
|
||||
- M3 says: "Placement: bottom of the UI, in front of main content; **nudge upward to avoid
|
||||
overlapping FABs**/docked toolbars… Snackbars should appear **above** FABs — never in front of or
|
||||
behind one" (RC-A, Snackbar → Behaviour and guidelines).
|
||||
- Library does: both sit at the same offset. `toast.blade.php:36` —
|
||||
`fixed inset-x-4 z-50 bottom-[calc(var(--material-bottom-bar,0px)+1rem)]`; `button.blade.php:177`
|
||||
(the `fab` prop, below `sm`) — `max-sm:fixed max-sm:end-4
|
||||
max-sm:bottom-[calc(var(--material-bottom-bar,0px)+1rem)] max-sm:z-30`. The snackbar is full width
|
||||
below `sm` (`sm:w-auto` only applies from 640 px up, `toast.blade.php:51`), so it lands *on top of*
|
||||
the FAB, which is exactly the "never in front of" case. `<x-fab>` placed by the application has the
|
||||
same problem, since the toast knows nothing about it.
|
||||
- Fix: mirror the existing `--material-bottom-bar` mechanism — have `<x-button fab>` (and the
|
||||
documented wrapper for `<x-fab>`) publish `--material-fab: 4.5rem`, and make `toast.blade.php:36`
|
||||
read `bottom-[calc(var(--material-bottom-bar,0px)+var(--material-fab,0px)+1rem)]`. Document the
|
||||
variable next to `--material-bottom-extra` in SKILL.md's "Safe areas".
|
||||
- Effort: M
|
||||
- Breaks API? no (a new opt-in CSS variable)
|
||||
|
||||
### ACT-18 · toast / alert · 40 px controls inside the snackbar and the alert miss the 48 px target
|
||||
- Severity: should-fix
|
||||
- M3 says: web interaction targets are "at least 48 × 48 **CSS pixels**" (RF:82), and "Interaction
|
||||
targets must stay ≥48×48dp even when the visible element is scaled down… a 36dp-tall button can
|
||||
keep a 48dp target" (RF:561-563).
|
||||
- Library does: `toast.blade.php:73` (the action, `h-10` = 40 px), `toast.blade.php:77` (the close
|
||||
button, `size-10` = 40 px) and `alert.blade.php:61` (the dismiss button, `size-10`). None of them
|
||||
has the pseudo-element trick `<x-button>` uses at `button.blade.php:172`. The snackbar container is
|
||||
`min-h-12` (48 px) so there is no room to grow vertically without the pseudo-element.
|
||||
- Fix: add `relative after:absolute after:top-1/2 after:left-1/2 after:size-full after:min-h-12
|
||||
after:min-w-12 after:-translate-x-1/2 after:-translate-y-1/2` to those three buttons (they already
|
||||
carry `state-layer`, which sets `position: relative`).
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### ACT-19 · button / group / menu-item / fab-menu · Colour is animated on the overshooting spatial spring
|
||||
- Severity: should-fix
|
||||
- M3 says: "Two spring **styles**: **Spatial** (movement — x/y position, rotation, size, rounded
|
||||
corners; overshoots and bounces into place) and **Effects** (color, opacity; **no overshoot**)"
|
||||
(RS:472-473). `resources/css/tokens/motion.css:11-13` states the same rule in the library's own
|
||||
words: "effects — colour and opacity. Critically damped; **a colour must never overshoot**."
|
||||
- Library does: four components put colour on the spatial spring, whose fast variant peaks at 1.094
|
||||
(`motion.css:28-30`):
|
||||
- `button.blade.php:163` — `transition-[border-radius,background-color,color,box-shadow,padding,margin] … ease-spatial-fast`
|
||||
- `group.blade.php:77` — `transition-[border-radius,background-color,color] … ease-spatial-fast`
|
||||
- `menu-item.blade.php:48` — `transition-[border-radius,background-color] … ease-spatial-fast`
|
||||
- `fab-menu.blade.php:49` — `transition-[border-radius,background-color,color,box-shadow] … ease-spatial-default`
|
||||
- Fix: split each into two transitions, e.g. on `button.blade.php:163`
|
||||
`transition-[border-radius,padding,margin] duration-(--md-sys-motion-spatial-fast-duration) ease-spatial-fast`
|
||||
plus a second declaration for `background-color,color,box-shadow` with
|
||||
`duration-(--md-sys-motion-effects-fast-duration) ease-effects-fast`. Tailwind cannot emit two
|
||||
`transition` shorthands on one element, so this wants a small utility in
|
||||
`resources/css/components/` (e.g. `.state-transition`) rather than utility classes.
|
||||
- Effort: M
|
||||
- Breaks API? no
|
||||
|
||||
### ACT-20 · toast · The snackbar draws a status icon, which M3 tells you not to
|
||||
- Severity: should-fix
|
||||
- M3 says: "**Avoid icons in a snackbar** (use a dialog instead if an icon feels necessary); avoid
|
||||
stylized text/inline links" (RC-A, Snackbar → Behaviour and guidelines). The specs page does list an
|
||||
optional icon slot, but it is the *close* affordance ("2. Icon (optional close affordance) — …the
|
||||
guidelines page anatomy instead lists 'Close button (optional)' in the same slot", RC-A, Snackbar →
|
||||
Anatomy).
|
||||
- Library does: `toast.blade.php:53-65` draws a `check_circle` / `error` / `warning` / `info` glyph
|
||||
for every typed toast, and `Toasts.php:19-37` makes typing the *only* API — `$this->success(…)`
|
||||
always sets `type: 'success'`. The glyphs are painted in the library's own
|
||||
`inverse-success/error/warning/info` roles (`scheme.css:76-79`), which is a sound extension of
|
||||
`inverse-primary`, so the colour is not the problem; the icon's presence is.
|
||||
- Fix: either drop the leading icon and let the type only choose the announcement role, or document
|
||||
the deviation in `toast.blade.php`'s header (it currently presents the icon as M3-sanctioned: "A
|
||||
type draws its state icon in the inverse state colour"). If the icon stays, keep it out of the
|
||||
accessible name — it is already `aria-hidden` via `<x-icon>`.
|
||||
- Effort: S
|
||||
- Breaks API? no (if documented), yes for the visual contract (if removed)
|
||||
|
||||
### ACT-21 · fab-menu · The trigger has no accessible name unless `label` is passed
|
||||
- Severity: should-fix
|
||||
- M3 says: "Labeling (Web): the FAB menu is a FAB + Menu combo — follow FAB accessibility guidance for
|
||||
the trigger… **the FAB's own accessibility label should describe what menu it opens**"; and for the
|
||||
close button "Label 'Toggle menu', Role Button, State Expanded/Collapsed" (RC-A, FAB menu →
|
||||
Accessibility).
|
||||
- Library does: `fab-menu.blade.php:46` writes `aria-label` only `@if ($label)`. `label` has no
|
||||
default (`fab-menu.blade.php:21`), and the only content of the button is two `<x-icon>`s, which are
|
||||
`aria-hidden="true"` by default (`icon.blade.php:27`). A `<x-fab-menu>` without `label` is therefore
|
||||
an unnamed button. `<x-menu>` has the same shape but delegates naming to the trigger the caller
|
||||
supplies, so the gap is specific to the FAB menu, which builds its own button.
|
||||
- Fix: make `label` required, or default it to `__('Toggle menu')` in `fab-menu.blade.php:21` and
|
||||
always emit `aria-label`. Note the expanded/collapsed state is already handled — `menu.js:140`
|
||||
writes `aria-haspopup`/`aria-controls`/`aria-expanded` onto the button it finds in the trigger.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### ACT-22 · rich-tooltip · The bubble is never associated with its trigger
|
||||
- Severity: should-fix
|
||||
- M3 says: "Assistive tech must be able to **receive the tooltip's message** and activate it via
|
||||
keyboard/switch input" (RC-A, Tooltips → Accessibility); "Role: **Tooltip** (or an equivalent role)
|
||||
on the container".
|
||||
- Library does: `rich-tooltip.blade.php:41-56` gives the bubble `role="tooltip"` (or `dialog` when
|
||||
`persistent`) and an `aria-label` of the title, but nothing points at it: the trigger gets no
|
||||
`aria-describedby`, and for `persistent` no `aria-haspopup="dialog"`/`aria-expanded` either. A
|
||||
screen-reader user focusing the trigger hears only the trigger's own label; the explanation — the
|
||||
whole point of a rich tooltip — is never announced. (`<x-tooltip>` is deliberately `aria-hidden`
|
||||
and documents why, which is defensible for an icon button whose `aria-label` *is* the tooltip text;
|
||||
it is not defensible for the standalone form, e.g.
|
||||
`showcase/sections/communication.blade.php:30-32`, where the tip says something the trigger does
|
||||
not.)
|
||||
- Fix: in `rich-tooltip.blade.php`, put `aria-describedby="material-rich-tooltip-{{ $key }}"` on the
|
||||
wrapper's focusable child (or document that the caller must), and for `persistent` add
|
||||
`aria-haspopup="dialog"` + an `aria-expanded` written by `rich-tooltip.js`. For the standalone
|
||||
`<x-tooltip>`, either drop `aria-hidden` and describe the trigger, or document in SKILL.md that the
|
||||
trigger must carry the same words itself.
|
||||
- Effort: M
|
||||
- Breaks API? no
|
||||
|
||||
### ACT-23 · badge · The floating badge sits 2–4 px off M3's anchor geometry
|
||||
- Severity: nice-to-have
|
||||
- M3 says: "Small badge: distance from top-trailing icon corner to bottom-leading badge corner (H×W)
|
||||
| **6×6dp**"; "Large badge: … | **14×12dp**" (RC-A, Badges → Specs). For a 24 dp icon that puts a
|
||||
small badge flush in the icon's top-trailing corner (top 0, end 0) and a large badge at
|
||||
top −2 px / start `calc(100% - 12px)`.
|
||||
- Library does: `badge.blade.php:79-80` — `absolute top-0.5 end-0.5` for the dot (2 px in on both
|
||||
axes, where M3 wants 0) and `absolute -top-1 start-[calc(100%-0.75rem)]` for the count (the `start`
|
||||
is exactly right; the top is −4 px where M3 wants −2 px).
|
||||
- Fix: `top-0 end-0` for the dot and `-top-0.5` for the count in `badge.blade.php:79-80`.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### ACT-24 · badge · The `outline` status badge draws its edge in `outline-variant`
|
||||
- Severity: nice-to-have
|
||||
- M3 says: a badge "must be ≥**3:1**, same rule whether default or custom color" (RC-A, Badges →
|
||||
Accessibility). `outline-variant` is M3's *decorative* boundary role (dividers); `outline` is the
|
||||
role for a boundary that must be seen.
|
||||
- Library does: `badge.blade.php:68` — `'border-outline-variant text-on-surface-variant'`. On
|
||||
`surface` in the library's own scheme `outline-variant` is around 1.5:1, so the badge's shape is
|
||||
barely visible. (`<x-button variant="outlined">` uses the same role, but there the M3 table
|
||||
explicitly names it: "Outlined container … **Outline variant (outline)**" — RC-A, Buttons → colour
|
||||
table — so the button is inside spec and the badge, which has no M3 table, is not.)
|
||||
- Fix: `border-outline` in `badge.blade.php:68`.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### ACT-25 · tooltip.js · A tooltip disappears the instant the pointer leaves
|
||||
- Severity: nice-to-have
|
||||
- M3 says: "tooltips are **transient by default** — both plain and rich tooltips disappear **1.5
|
||||
seconds** after the cursor/focus leaves the target region; opening a new tooltip immediately closes
|
||||
any other open one (**only one tooltip visible at a time**)" (RC-A, Tooltips → Specs).
|
||||
- Library does: `resources/js/tooltip.js:40-43` hides on `pointerleave`, `pointerdown` and
|
||||
`focusout` with no delay; `rich-tooltip.js:43` uses a 200 ms grace instead of 1500 ms. Nothing
|
||||
closes other open tooltips — because the bubble is `popover="manual"`
|
||||
(`tooltip.blade.php:35`), a tooltip held open by keyboard focus stays up while a second one opens
|
||||
on hover.
|
||||
- Fix: `hide(1500)` in `tooltip.js` (add the same `delay` parameter `rich-tooltip.js:37` already
|
||||
has), and keep a module-level `let open = null` that `show()` hides before showing its own.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### ACT-26 · menu · The open/close transition is a plain fade
|
||||
- Severity: nice-to-have
|
||||
- M3 says: "Motion: menus use an **enter/exit transition tying them visually to their trigger**; the
|
||||
trigger shows a pressed state while the menu is open" (RC-A, Menus → Behaviour and guidelines).
|
||||
- Library does: `menu.blade.php:72` transitions `opacity` only — `translate` is in the
|
||||
`transition-[…]` list but no translate or scale value is ever set, so the menu simply fades in
|
||||
place. The library's own `::picker(select)` does it properly (`menu.css:116-128`: opacity + scale
|
||||
0.95 from `transform-origin: top`, on the spatial spring).
|
||||
- Fix: mirror `menu.css:116-128` on `menu.blade.php:72` — add
|
||||
`origin-top scale-95 open:scale-100 starting:open:scale-95` and put the scale on the spatial spring
|
||||
while opacity stays on effects.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### ACT-27 · menu-item · A selected item is told by colour and shape only
|
||||
- Severity: nice-to-have
|
||||
- M3 says: "by default, selected items change **both shape and color**; default contrast between
|
||||
selected/unselected items is 3:1; it's **recommended to add yet another cue** on top (e.g. a
|
||||
checkmark) so selection isn't conveyed by color/shape alone" (RC-A, Menus → Accessibility).
|
||||
- Library does: `menu-item.blade.php:51` changes container colour and corner, and
|
||||
`menu-item.blade.php:87` fills the leading icon; `aria-checked` is set
|
||||
(`menu-item.blade.php:57`), so assistive tech is fine. No visible tick. Again the library's own
|
||||
select menu does it — `menu.css:179-188` draws a `::checkmark` at the row's end.
|
||||
- Fix: when `$selected === true` and no `iconRight` is given, render a trailing `check` icon in
|
||||
`menu-item.blade.php:105-107`.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### ACT-28 · menu-item · 44 px rows, where the rest of the library's menus use 48 px
|
||||
- Severity: nice-to-have
|
||||
- M3 says: `tokens/SegmentedMenuTokens.kt` `Item = 44.0.dp` (what the component's header cites, and it
|
||||
is correct), while the baseline menu's published spec is "List item height | **48dp**" (RC-A, Menus
|
||||
→ Specs) and the same page's accessibility section asks for "≥**48×48dp** targets" inside menu item
|
||||
slots.
|
||||
- Library does: `menu-item.blade.php:46` — `min-h-11` (44 px). `resources/css/components/menu.css:55`
|
||||
(`.field-option`) and `menu.css:134` (native `option`) both use `min-block-size: 3rem` (48 px), so
|
||||
a `<x-menu>` and a `<x-select>` dropdown in the same form do not match.
|
||||
- Fix: either `min-h-12` on `menu-item.blade.php:46` (consistent, and clears the 48 px target), or
|
||||
keep 44 px and note the inconsistency deliberately. Both readings are defensible; the token is the
|
||||
more specific source, the accessibility page the more binding one.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### ACT-29 · split-button · The trailing chevron is not nudged off centre
|
||||
- Severity: nice-to-have
|
||||
- M3 says: "Menu icon offset from center when unselected (site, specs page): XS/S = **−1dp**, M =
|
||||
−2dp, L = −3dp, XL = −6dp" (RC-A, Split button → Specs) — the chevron sits slightly towards the
|
||||
leading button so the pair reads as one control.
|
||||
- Library does: `split-button.blade.php:56-66` centres the icon (`justify-center` from
|
||||
`button.blade.php:162`).
|
||||
- Fix: add a per-size negative `ms-px`/`-me-*` or `translate-x` to the trailing button's `:class` in
|
||||
`split-button.blade.php:36`.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### ACT-30 · split-button · The chevron rotates on the expressive spring
|
||||
- Severity: nice-to-have
|
||||
- M3 says: "The trailing (menu) button uses the **standard motion scheme, not expressive**, when
|
||||
rotating; it rotates 180° inward on open/close" (RC-A, Split button → Behaviour and guidelines).
|
||||
- Library does: `groups.css:85-87` — `transition: rotate var(--md-sys-motion-spatial-fast-duration)
|
||||
var(--md-sys-motion-spatial-fast)`, i.e. the expressive spring, which overshoots past 180° and
|
||||
comes back. The 180° rotation itself (`groups.css:89-90`) is right.
|
||||
- Fix: the library has no standard-scheme spring token; `--md-sys-motion-easing-standard` with
|
||||
`--md-sys-motion-duration-short` (`motion.css:52,58`) is the closest published equivalent.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### ACT-31 · fab · The small extended FAB's minimum width is 56 px, not 80 dp
|
||||
- Severity: nice-to-have
|
||||
- M3 says: baseline extended FAB "Container width | Dynamic, **80dp min**" (RC-A, Extended FAB →
|
||||
Specs). `ExtendedFabSmallTokens.kt` publishes no minimum, and the small extended FAB is the
|
||||
baseline's replacement, so 80 dp is the only number on record.
|
||||
- Library does: `fab.blade.php:39` — `min-w-14` (56 px) for `sm`, `min-w-20`/`min-w-24` for md/lg
|
||||
(which match those sizes' heights rather than any published minimum).
|
||||
- Fix: `min-w-20` (80 px) for the small extended FAB in `fab.blade.php:39`.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### ACT-32 · fab-menu · Margins do not grow in large windows, and items cannot scroll
|
||||
- Severity: nice-to-have
|
||||
- M3 says: "Menu/FAB margin from screen edge | 16dp (compact/medium windows), **24dp** (large/extra-large
|
||||
windows)"; "Extra bottom margin when opened from medium FAB | 40dp… large FAB | 56dp"; "On short
|
||||
viewports (e.g. landscape phone), items can **scroll — behind the close button**, which stays
|
||||
fixed" (RC-A, FAB menu → Specs and → Behaviour).
|
||||
- Library does: `<x-fab-menu>` does not place itself (by design), and both the header comment
|
||||
(`fab-menu.blade.php:3`) and SKILL.md line 283 show a fixed `end-4 bottom-4` (16 px) with no
|
||||
large-window variant. The list (`fab-menu.blade.php:69-75`) has no `max-height`/`overflow`, so on a
|
||||
landscape phone six 56 px items plus gaps (~360 px) push the menu off screen.
|
||||
- Fix: document `sm:end-6 sm:bottom-6`-style margins in the header and SKILL.md (Tailwind has no
|
||||
1200 px breakpoint by default — see the breakpoint map), and add
|
||||
`max-h-[calc(100vh-8rem)] overflow-y-auto` to `fab-menu.blade.php:70`.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### ACT-33 · fab-menu / menu · Items enter but never exit
|
||||
- Severity: nice-to-have
|
||||
- M3 says: "Motion: FAB transforms into the close button; items animate in/out via an **enter-exit
|
||||
transition**, originating from one of the FAB's trailing corners" (RC-A, FAB menu → Behaviour).
|
||||
- Library does: `fab-menu-item.blade.php:27` has `starting:translate-y-2 starting:opacity-0` for the
|
||||
entry, but the popover is hidden with `display: none` on close and the items carry no
|
||||
`transition-discrete`/`@starting-style` exit pair, so they vanish instantly. `<x-menu>` fades out
|
||||
correctly (`menu.blade.php:72` includes `display,overlay` and `transition-discrete`).
|
||||
- Fix: add `transition-[translate,opacity,display,overlay] transition-discrete` to
|
||||
`fab-menu-item.blade.php:27` and drive the closed state from the popover's `:not(:popover-open)`.
|
||||
- Effort: M
|
||||
- Breaks API? no
|
||||
|
||||
### ACT-34 · toast · Escape does not dismiss a focused snackbar
|
||||
- Severity: nice-to-have
|
||||
- M3 says: "Keyboard: Tab moves between interactive elements; **Esc dismisses the focused snackbar**";
|
||||
and "on web, provide a documented keyboard shortcut (e.g. Alt+G) to jump focus to a snackbar with an
|
||||
action" (RC-A, Snackbar → Accessibility).
|
||||
- Library does: `snackbar.js` binds no key handler; `toast.blade.php:41-82` binds only pointer and
|
||||
focus events. Escape does nothing, and there is no way to reach an actioned snackbar from the
|
||||
keyboard except by tabbing past the whole page (the host is the last element in `<body>`).
|
||||
- Fix: add a `keydown` listener in `snackbar.js:38` that calls `dismiss()` on Escape while the
|
||||
snackbar contains `document.activeElement`; optionally a documented shortcut that focuses
|
||||
`[data-toast-action]`.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### ACT-35 · toast · The description line is dimmed with an opacity instead of a role
|
||||
- Severity: nice-to-have
|
||||
- M3 says: the snackbar's roles are "container = **Inverse surface**; supporting text/icon = **Inverse
|
||||
on surface**; action label = **Inverse primary**" (RC-A, Snackbar → Specs) — there is no fourth
|
||||
role, and supporting text is one block of up to two lines.
|
||||
- Library does: `toast.blade.php:69` — `type-body-md opacity-80` on the description, i.e.
|
||||
inverse-on-surface at 80 %, a colour outside the roles that also drops the contrast of the longer
|
||||
half of the message.
|
||||
- Fix: drop `opacity-80` and let both lines be `inverse-on-surface`; M3 distinguishes them by position,
|
||||
not tone.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### ACT-36 · alert · `role="alert"` on content that is present at page load
|
||||
- Severity: nice-to-have
|
||||
- M3 says: for the nearest equivalent (the snackbar) "use a **live region with a 'polite'/queued**
|
||||
announcement (**not assertive**)" (RC-A, Snackbar → Accessibility). M3 publishes no alert/banner
|
||||
component at all — the current site has no banner page; the only survivals are legacy mentions in
|
||||
the states inheritance list (RF:384) and the elevation table (RS:320).
|
||||
- Library does: `alert.blade.php:38` — `role="{{ in_array($color, ['error','warning']) ? 'alert' :
|
||||
'status' }}"`. `role="alert"` is an assertive live region; when the alert is part of the rendered
|
||||
page (the normal case — the showcase renders four of them statically,
|
||||
`showcase/sections/communication.blade.php:47-53`) some screen readers announce it over the page
|
||||
title on load, and a Livewire morph that re-renders the region can re-announce it.
|
||||
- Fix: default both to `role="status"` and add an opt-in prop (`assertive`) for the case where the
|
||||
alert really is injected in response to an action.
|
||||
- Effort: S
|
||||
- Breaks API? yes (the rendered role changes; tests asserting `role="alert"` would need updating)
|
||||
|
||||
## Deliberate deviations
|
||||
|
||||
- **The text button's label is `primary`, not `on-surface-variant`** (`button.blade.php:20-22`,
|
||||
`:100-127`). Holds up. The M3 specs table reads "Text icon & label | **Primary** | -- | --" (RC-A,
|
||||
Buttons → colour table), and the M2→M3 notes say "neutral text button no longer recommended". The
|
||||
library is right, and it is also right that an *icon-only* text button (M3's "standard icon button")
|
||||
takes `on-surface-variant` — "Standard icon | On surface variant | On surface variant | Primary"
|
||||
(RC-A, Icon buttons → colour table). Both readings are implemented in the same `match` at
|
||||
`button.blade.php:123-126`, which is exactly correct.
|
||||
- **A selected round toggle button turns square** (`button.blade.php:132-136`). Ambiguous, and the
|
||||
library picked the site's reading. The guidelines page says "By default, toggle buttons change from
|
||||
**round to square when selected**" (RC-A, Buttons → Behaviour), but `tokens/ButtonSmallTokens.kt`
|
||||
and `ButtonMediumTokens.kt` both set `SelectedContainerShapeRound = CornerFull`, i.e. a selected
|
||||
round *label* button stays round in Compose. For *icon* buttons the tokens agree with the site
|
||||
(`SmallIconButtonTokens.SelectedContainerShapeRound = CornerMedium`,
|
||||
`…ShapeSquare = CornerFull`; `LargeIconButtonTokens`/`XLargeIconButtonTokens` are the same pattern
|
||||
with ExtraLarge), and the library implements that swap exactly. Worth a line in the header comment
|
||||
saying which source won for label buttons.
|
||||
- **A selected text button takes the tonal container** (`button.blade.php:18`, `:123-126`). Holds up as
|
||||
an extension: M3 gives text buttons no toggle states at all, so anything is an invention, and
|
||||
borrowing the tonal selected colours keeps it inside the roles and satisfies the two-indicator rule.
|
||||
- **Press expansion is a fixed step per size, not 15 % of width** (`groups.css:9`, `:32-48`). Holds up.
|
||||
`ButtonGroupDefaults.ExpandedRatio = 0.15f` and the layout applies `expandedRatio * width / 2` per
|
||||
side, clamped to the neighbour's padding (fetched live from androidx-main `ButtonGroup.kt:186`,
|
||||
`:482`, `:500-511`). CSS cannot express a percentage of an intrinsic width, and every size's
|
||||
`--group-grow` (4/6/8/16/20 px) is safely below its `--group-pad`, so the neighbour never reaches
|
||||
negative padding. The animation uses `ease-spatial-fast`, matching Compose's
|
||||
`MotionSchemeKeyTokens.FastSpatial` (`ButtonGroup.kt`, defaultAnimationSpec).
|
||||
- **`--group-full` is half the height rather than `corner-full`** (`groups.css:19-23`, `:32-36`).
|
||||
Holds up, and the values are right: 16/20/28/48/68 px are exactly half of 32/40/56/96/136 dp, which
|
||||
is what `ConnectedButtonGroupSmallTokens.SelectedInnerCornerCornerSizePercent = 50%` means.
|
||||
- **The `fab` prop on `<x-button>`** (`button.blade.php:177`). Inside spec as far as the *component*
|
||||
goes: an extended FAB at 56 px, corner 16 dp, title-medium, primary-container, elevation 3 and a
|
||||
16 px margin, which is the small extended FAB exactly (`ExtendedFabSmallTokens.kt`). Two caveats:
|
||||
M3 says an extended FAB needs "**No tooltip** — the visible label already serves that role
|
||||
(explicit Don't)" while `button.blade.php:225` will happily attach one; and the elevation never
|
||||
reaches Level 4 on hover because `max-sm:shadow-elevation-3` outranks `hover:shadow-elevation-1`
|
||||
in Tailwind's variant order.
|
||||
- **`<x-group>` binds native radios/checkboxes, so the arrow keys move the choice**
|
||||
(`group.blade.php:9-10`, `:84-91`). Ambiguous. M3's button-group accessibility says "Keyboard: **Tab**
|
||||
navigates to the next button; Space or Enter activates/selects the focused button" (RC-A, Button
|
||||
groups → Accessibility), which is roving-tabindex-free but also not a radio group. The library's
|
||||
choice matches WAI-ARIA's radiogroup pattern and gives `wire:model` for free; it is a defensible
|
||||
reading of "single-select, selection-required", and the `multiple` form (checkboxes) does behave
|
||||
exactly as M3 describes.
|
||||
- **The alert exists at all** (`alert.blade.php:1-10`). Holds up, with a caveat. M3 publishes no
|
||||
banner and no alert; its own guidance routes in-page notices to a snackbar (transient, low priority)
|
||||
or a dialog (blocking, high priority) — and an "your storage is almost full" notice is neither. The
|
||||
component stays inside the foundations: container roles only, `rounded-corner-md`, title-small +
|
||||
body-medium, text buttons in the actions slot. The header comment should add *when not to* use it
|
||||
(a confirmation belongs in `<x-modal>`, a result of an action in `<x-toast>`), since the M3 site
|
||||
will not tell a reader that.
|
||||
- **`<x-stat>` and `<x-empty-state>`** (`stat.blade.php:3-8`, `empty-state.blade.php:8-11`). Hold up.
|
||||
Both draw only from M3's roles (`surface-container`, `secondary-container`/`on-secondary-container`),
|
||||
the shape scale (`rounded-corner-lg`), the Expressive emphasized type roles
|
||||
(`type-emphasized-headline-md`, which `type.css` does define) and the Expressive shape library
|
||||
(`cookie-9`). The counting figure honours reduced motion by reading
|
||||
`--md-sys-motion-spatial-slow-duration`, which `motion.css:63-74` zeroes (`figure.js:59-64`, `:75`).
|
||||
Nothing here draws a colour outside the roles.
|
||||
- **`<x-progress>`'s non-primary track colours** (`progress.blade.php:64-68`). Holds up. M3 fixes the
|
||||
track at `secondary-container` for the primary indicator (RC-A, Progress indicators → Specs), which
|
||||
the library does for `primary` and `secondary`; the extra colours take their own container, which is
|
||||
the only consistent extension.
|
||||
- **`thick` grows the container beyond M3's `WaveHeight`** (`progress.blade.php:11-13`,
|
||||
`:154-162`). Holds up: M3 publishes 10 dp only for the 4 dp stroke, "Track thickness | Configurable"
|
||||
is an Expressive configuration with no published table, and adding the extra stroke to the container
|
||||
keeps the 3 dp amplitude intact (`progress.js:1333`, `waveHeight = height - stroke`).
|
||||
|
||||
## Aligned
|
||||
|
||||
Verified correct — no need to re-check:
|
||||
|
||||
- **Button sizes**: 32/40/56/96/136 px heights, 8/8/8/12/16 px gaps, 20/20/24/32/40 px icons,
|
||||
16/24/48/64 px padding at sm/md/lg/xl, 1/1/1/2/3 px outlines (`button.blade.php:147-158` against
|
||||
RC-A Buttons → Specs and `tokens/Button*.kt`).
|
||||
- **Button corners**: square 12/12/16/28/28 px and pressed 8/8/12/16/16 px, and the pressed value is
|
||||
applied to round buttons too, as M3 requires ("round and square buttons should have the same pressed
|
||||
shape") — `button.blade.php:129-136`, `:166`.
|
||||
- **Button type roles per size**: label-large / label-large / title-medium / headline-small /
|
||||
headline-large. Confirmed live against androidx-main `Button.kt:1645-1648`
|
||||
(`buttonHeight < mediumHeight -> labelLarge; < largeHeight -> titleMedium; < xLargeHeight ->
|
||||
headlineSmall; else -> headlineLarge`).
|
||||
- **Every toggle colour pair**, all four variants × three states, matches the specs table exactly
|
||||
(`button.blade.php:118-127`): filled unselected = surface-container/on-surface-variant, tonal
|
||||
selected = secondary/on-secondary, outlined selected = inverse-surface/inverse-on-surface, elevated
|
||||
selected = primary/on-primary, standard icon button selected = primary.
|
||||
- **Icon-button widths**: all fifteen values (narrow/default/wide × XS–XL) derive exactly from the
|
||||
token paddings — 28/32/40, 32/40/52, 48/56/72, 64/96/128, 104/136/184 px (`button.blade.php:140-144`).
|
||||
- **Icon-button selected shape swap** (round ⇄ square, `button.blade.php:133`) matches
|
||||
`Small/Large/XLargeIconButtonTokens.SelectedContainerShape*` at every size. (RC-A's note that the
|
||||
swap is "inverted for XS/S/M vs L/XL" is wrong — the token files are consistent, and the library is
|
||||
right.)
|
||||
- **Disabled**: content at 38 %, container at 10 % (`button.blade.php:174-175`) matches
|
||||
`FilledButtonTokens/TonalButtonTokens/ElevatedButtonTokens.DisabledContainerOpacity = 0.1f` and
|
||||
`DisabledLabelTextOpacity = 0.38f`; elevation removed when disabled ✓.
|
||||
- **Elevation**: filled/tonal rest 0 → hover Level 1; elevated rest Level 1 → hover Level 2
|
||||
(`button.blade.php:169-170`); FAB rest Level 3 → hover Level 4 (`fab.blade.php:49`); menu Level 2
|
||||
(`menu.blade.php:70`); snackbar Level 3 (`toast.blade.php:51`); rich tooltip Level 2
|
||||
(`rich-tooltip.blade.php:50`); plain tooltip none (`tooltip.blade.php:40`). All match.
|
||||
- **48 px touch target on xs/sm buttons** (`button.blade.php:172`).
|
||||
- **State layer**: content colour at 8 % hover / 10 % focus / 10 % press, hover gated behind
|
||||
`@media (hover: hover)`, suppressed when disabled (`state.css:13-44`) — matches RF:367-375 exactly,
|
||||
including "the state layer's color equals the content's 'on' color" (it uses `currentColor`).
|
||||
- **Focus ring**: 3 px `secondary`, 2 px offset, keyboard-only (`state.css:46-53`), and
|
||||
`SegmentedMenuTokens.ItemFocusIndicatorColor = Secondary` confirms the role for menus.
|
||||
- **Standard group spacing**: 18/12/8/8/8 px (`button-group.blade.php:36-38`).
|
||||
- **Connected group**: 2 px gaps and inner corners 4/8/8/16/20 px, pressed smaller, selected at 50 %
|
||||
of the height (`button-group.blade.php:34`, `groups.css:32-36`, `:54-71`).
|
||||
- **Split button**: 2 px between halves, leading padding 12/10 (xs) and 16/12 (sm) px, symmetric
|
||||
24/48/64 px at md/lg/xl, trailing button widths 48/48/56/96/136 px, outer corners full, trailing
|
||||
half rounds fully while open, chevron rotates 180°, `text` variant excluded, menu 4 px away,
|
||||
`aria-expanded` on the trailing button, default label "More options"
|
||||
(`split-button.blade.php:33-66`, `groups.css:68-90`).
|
||||
- **FAB**: 56/80/96 px with 16/20/28 px corners and 24/28/32 px icons; primary/secondary/tertiary
|
||||
container by default with the Expressive non-container styles under `variant="filled"`; no small FAB
|
||||
and no surface style (`fab.blade.php:34-42`).
|
||||
- **FAB menu**: 56 px close button, full corner when open, 20 px close icon, 8 px to the first item,
|
||||
56 px items with full corners, 24 px icons, 8 px icon gap, 24 px padding, 4 px between items,
|
||||
elevation 3 throughout, trailing-edge alignment (`fab-menu.blade.php:48-75`,
|
||||
`fab-menu-item.blade.php:25`) — every number matches `tokens/FabMenuBaselineTokens.kt`.
|
||||
- **Menu container**: surface-container-low / tertiary-container for `vibrant`, 16 px corner,
|
||||
elevation 2, 112–280 px width (`menu.blade.php:70-79`) — matches
|
||||
`StandardMenuTokens.ContainerColor`, `VibrantMenuTokens`, `SegmentedMenuTokens.ContainerShape =
|
||||
CornerLarge`, `ContainerElevation = Level2`, and the baseline width table.
|
||||
- **Menu item**: body-large label, 20 px icons, 4 px corners opening to 12 px at the ends, selected =
|
||||
tertiary-container with a 12 px corner, body-medium description, label-small trailing text — all
|
||||
five confirmed against `SegmentedMenuTokens` (`ItemLabelTextFont = BodyLarge`, `ItemLeadingIconSize
|
||||
= 20dp`, `ItemShape = CornerExtraSmall`, `ItemFirstChildShape = CornerMedium`, `ItemSelectedShape =
|
||||
CornerMedium`, `ItemSupportingTextFont = BodyMedium`, `ItemTrailingSupportingTextFont = LabelSmall`).
|
||||
- **Menu keyboard**: Enter/Space/Down open on the first item, Up on the last, arrows/Home/End move,
|
||||
typeahead, Tab closes, Escape closes and returns focus to the trigger, activation closes unless
|
||||
`keep-open` (`menu.js:153-242`, `menu.blade.php:53-55`) — the WAI-ARIA menu-button pattern, and it
|
||||
matches M3's own keyboard table apart from submenus.
|
||||
- **`current` uses `secondary-container`** (`menu-item.blade.php:52`) — the navigation-indicator role,
|
||||
correctly distinguished from a checked choice.
|
||||
- **Badge**: 6 px dot, 16 px tall count with a 16 px minimum width and 4 px padding, label-small,
|
||||
full corners, error/on-error by default, `aria-hidden` unless labelled, `max` overflow to "999+"
|
||||
within M3's four-character limit (`badge.blade.php:74-85`) — matches `tokens/BadgeTokens.kt`.
|
||||
- **Progress**: 4 px (8 px thick) stroke, 40 px circular (48 px wavy), 4 px track gap, 4 px stop
|
||||
indicator on linear determinate only, primary indicator with a secondary-container track, 40/20/15 px
|
||||
wavelengths, 3 dp linear amplitude, round caps, reduced-motion handling, `role="progressbar"` with
|
||||
`aria-valuenow` only while determinate (`progress.blade.php:70-197`, `progress.js:61-79`, `:1333`).
|
||||
- **Loading indicator**: 48 px container, 38 px shape, primary by default,
|
||||
on-primary-container-on-primary-container when `contained`, `role="progressbar"`, rests under
|
||||
reduced motion (`loading.blade.php:27-42`) — matches `LoadingIndicatorTokens` and the contained
|
||||
colour rule exactly.
|
||||
- **Snackbar colours and type**: inverse-surface container, inverse-on-surface text, inverse-primary
|
||||
label-large action rendered as a text button, body-medium supporting text, 4 px corner, elevation 3,
|
||||
48 px single-line height, one at a time, 4 s default inside M3's 4–10 s range, hover/focus pauses,
|
||||
never steals focus (`toast.blade.php:51-73`, `snackbar.js:13`, `:84-117`) — matches
|
||||
`tokens/SnackbarTokens.kt` value for value.
|
||||
- **Plain tooltip**: inverse-surface, inverse-on-surface, body-small, 4 px corner, 8 px horizontal
|
||||
padding giving a 24 px container, 4 px from the target, no elevation, flips when there is no room
|
||||
(`tooltip.blade.php:40-45`).
|
||||
- **Rich tooltip**: surface-container, 12 px corner, elevation 2, 12/8/16 px padding, title-small
|
||||
subhead and body-medium text in on-surface-variant, label-large primary actions, persistent opens on
|
||||
press and light-dismisses, the pointer can travel onto the bubble to reach the actions
|
||||
(`rich-tooltip.blade.php:50-65`, `rich-tooltip.js:20-46`).
|
||||
- **Motion springs**: damping 0.6 / stiffness 800 fast-spatial and 1.0 / 3800 fast-effects
|
||||
(`motion.css:28-42`) match RS:524-525 exactly, and every duration is zeroed under reduced motion.
|
||||
|
||||
## Missing
|
||||
|
||||
- **Submenus** (`<x-menu>`): M3's Expressive vertical menu specifies submenus, the Left/Right arrow
|
||||
keys that open and close them, and the shape morph that marks the active one ("the focused
|
||||
submenu's corners become more rounded while others become less rounded"). Nothing in the library
|
||||
implements them; `menu.js:201-219` has no Left/Right case.
|
||||
- **Grouped menu layout by gap**: M3's Expressive "Grouped" layout separates clusters with a *gap*
|
||||
(`SegmentedMenuTokens.SegmentedGap = 2dp`, `GroupPadding = 4dp`, `GroupShape = CornerSmall`).
|
||||
`<x-menu-group>` implements the labelled form and `<x-menu-separator>` the divider form — which is
|
||||
what M3 recommends for web ("on web, use dividers to separate items") — but the gap layout itself,
|
||||
and the 2 px gap between adjacent items that `ItemShape = 4dp` corners imply, are absent.
|
||||
- **Menus as a filtering surface / embedded text field** ("autocomplete", RC-A Menus → Behaviour).
|
||||
Partly covered by `<x-choices searchable>` in another group.
|
||||
- **Square-by-default button groups**: M3 lists "Default shape | Round, **square**" as a button-group
|
||||
configuration. `<x-button-group>` has no `shape` prop; the corner variables in `groups.css` assume a
|
||||
round group, so a group of `shape="square"` buttons gets round outer corners.
|
||||
- **Selection-required / multi-select semantics on `<x-button-group connected>`**: M3 lists
|
||||
single-select, multi-select and selection-required as configurations. `<x-group>` covers
|
||||
single/multi with real inputs; the plain `<x-button-group connected>` leaves selection entirely to
|
||||
the caller's `aria-pressed`.
|
||||
- **Snackbar keyboard shortcut** to jump focus to an actioned snackbar (M3 suggests Alt+G on web).
|
||||
- **Two-line snackbar height** (68 dp in `SnackbarTokens.TwoLinesContainerHeight`, 64 dp in the site's
|
||||
prose) is not expressed — the container grows organically from `min-h-12`, which lands close but is
|
||||
not pinned.
|
||||
- **FAB ↔ extended FAB scroll collapse** ("an extended FAB can collapse to a FAB on scroll and
|
||||
re-expand at the bottom of the view"). `<x-button fab>` swaps on window width only.
|
||||
- **Adaptive menu → bottom sheet at compact** (M3: "at compact breakpoints, consider swapping a menu
|
||||
for a bottom sheet"). `<x-bottom-sheet>` exists, but nothing connects the two.
|
||||
- Deliberately absent and correctly so: segmented buttons (deprecated in favour of the connected
|
||||
button group, which `<x-group>` provides), the small FAB ("Not recommended. Use a larger size"), the
|
||||
baseline extended FAB ("Not recommended. Use small extended FAB"), surface-coloured FABs ("no longer
|
||||
recommended").
|
||||
|
||||
## Breakpoint map
|
||||
|
||||
| Component | Library breakpoint used | M3 window size class it stands in for | Gap |
|
||||
| --- | --- | --- | --- |
|
||||
| `<x-button fab>` (`button.blade.php:177`) | `max-sm` (< 640 px) → extended FAB pinned bottom-end; ≥ 640 px → filled button | Compact (< 600) gets the FAB; medium+ (≥ 600) gets the inline button | 40 px: windows 600–639 px are "medium" in M3 but still get the phone FAB |
|
||||
| `<x-button responsive>` (`button.blade.php:218`) | `max-lg` (< 1024 px) hides the label | M3 has no label-hiding rule; the nearest is "on large screens, buttons can reposition" at expanded (≥ 840) | 184 px, and the rule itself is an invention — an icon-only button at 840–1023 px loses its visible label while keeping its `aria-label` |
|
||||
| `<x-toast>` (`toast.blade.php:36-51`) | `sm` (≥ 640 px) switches from full-width to `w-auto min-w-86`, and `bottom-start` moves to `start-6` | Compact (< 600) full-width fixed insets; medium+ (≥ 600) scales horizontally for line length | 40 px. Also: M3 allows a full-width snackbar only "when the UI has no persistent nav components", and `<x-app-shell>` shows a bottom navigation bar in exactly that range |
|
||||
| `<x-fab-menu>` margins (documented, `fab-menu.blade.php:3`, SKILL.md:283) | none — a fixed `end-4 bottom-4` (16 px) in the docs | 16 dp at compact/medium, **24 dp** at large (≥ 1200) / extra-large (≥ 1600) | The 24 dp step is missing; Tailwind's `xl` is 1280 px and `2xl` is 1536 px, so neither lines up with 1200/1600 |
|
||||
| `<x-fab>` size choice | none — the caller picks `sm`/`md`/`lg` | M3: medium FAB "most recommended" for compact/medium, large FAB for expanded+ | No adaptive guidance in the component or SKILL.md; the default is the 56 px baseline FAB, which M3 calls the *smallest* of the three |
|
||||
| `<x-menu>` | none | M3: "at compact breakpoints, consider swapping a menu for a bottom sheet" | Not implemented (see Missing) |
|
||||
| `<x-button-group>`, `<x-group>`, `<x-split-button>`, `<x-badge>`, `<x-progress>`, `<x-loading>`, `<x-tooltip>`, `<x-rich-tooltip>`, `<x-alert>`, `<x-stat>`, `<x-empty-state>` | none | M3 defines no width-dependent behaviour for these (button groups are "fixed or flexible" by author choice; `<x-group>` already fills its row, which is M3's connected-group rule) | none |
|
||||
@@ -0,0 +1,597 @@
|
||||
# Audit: containment
|
||||
|
||||
## Summary
|
||||
|
||||
The containment group is the most accurate part of the library I have measured: the card variants, the
|
||||
dialog (28dp corner, 24dp padding, 560/280 width, surface-container-high, 32% scrim, headline-small), the
|
||||
divider (1px outline-variant, 16dp inset), the list heights (56/72/88) and leading sizes (24/40/56), the
|
||||
bottom sheet's 32×4 handle / 28dp top corner / 640px cap, and the carousel's ported Compose keylines all
|
||||
match the published numbers or the androidx token files. The failures are concentrated in three places:
|
||||
**accessibility of selection and disabling in lists** (colour-only selection with no `aria-selected`, a
|
||||
`disabled` item whose link is still focusable and activatable), **the bottom-sheet drag handle** (a 32×4px
|
||||
touch target where M3 requires 48dp, achieved by its 22dp padding), and **adaptive breakpoints** (the
|
||||
list-detail pane opens at `xl`/1280 where M3 puts two panes from expanded/840). Beyond those, a handful of
|
||||
measurable deviations: a cascade-layer bug that kills the state layer on segmented list rows, a basic
|
||||
dialog whose headline and action row scroll away, side-sheet actions right-aligned where M3 says left,
|
||||
carousel end padding of 0 where M3 says 16dp, a full-screen carousel that scrolls horizontally where M3's
|
||||
scrolls vertically, and cards with no per-state elevation at all. Collapse, the error pages and the mail
|
||||
theme are clean against M3's foundations and styles; the mail theme reproduces the typescale correctly in
|
||||
px and the error pages use the roles and typescale properly.
|
||||
|
||||
## Findings
|
||||
|
||||
### C-01 · bottom-sheet · The drag handle is a 32×4px target where M3 requires 48dp
|
||||
- Severity: must-fix
|
||||
- M3 says: "drag handle has an accessible **48dp hit target**" and the specs table gives "Drag handle
|
||||
padding top/bottom | 22dp" (reference-components-a.md § Bottom sheets → Specs; raw
|
||||
`components_bottom-sheets_specs.md`). Confirmed in androidx: `SheetDefaults.kt` line 788,
|
||||
`private val DragHandleVerticalPadding get() = 22.dp`, applied as
|
||||
`modifier.padding(vertical = DragHandleVerticalPadding)` around the 32×4 handle → 4 + 22 + 22 = 48dp.
|
||||
Foundations also require a ≥48×48 touch target for any interactive element.
|
||||
- Library does: `resources/views/components/bottom-sheet.blade.php:60` — the `<button>` that is the handle
|
||||
is `class="h-1 w-8 …"` (4×32px) with no padding of its own; the padding lives on the wrapper `<div>` at
|
||||
line 59 (`py-4` = 16px, not 22px), and the wrapper is not the control. The clickable/focusable target is
|
||||
therefore 32×4px, and the whole handle region is only 36px tall.
|
||||
- Fix: move the padding onto the button: `class="h-1 w-8 box-content py-[22px] …"` (or wrap with
|
||||
`py-[22px]` and give the button `before:absolute before:inset-x-0 before:-inset-y-[22px]`), and change
|
||||
the wrapper at line 59 from `py-4` to `py-0`. Total sheet-top region becomes 48px, matching M3.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### C-02 · list-item · A `disabled` item's link stays keyboard-focusable and activatable
|
||||
- Severity: must-fix
|
||||
- M3 says: disabled list items carry `ItemDisabled*Opacity = 0.38` and a disabled state layer
|
||||
(`ListTokens.kt:70,75`); foundations' states model treats disabled as "not interactive". A control that
|
||||
looks disabled but still responds to Enter is an interaction bug.
|
||||
- Library does: `resources/views/components/list-item.blade.php:50` adds only
|
||||
`'pointer-events-none text-on-surface/38' => $disabled`. `pointer-events: none` blocks the pointer but
|
||||
not the keyboard; the `<a href="…" data-list-open …>` at lines 73-78 is still rendered, still in the tab
|
||||
order and still navigates on Enter. No `aria-disabled` is emitted, so a screen reader announces the item
|
||||
as an ordinary link.
|
||||
- Fix: in `list-item.blade.php`, when `$disabled` render the title as the `<p>` branch (skip the `<a>`
|
||||
entirely, or add `tabindex="-1" aria-disabled="true"` and strip `href`), drop `data-list-row` when
|
||||
disabled, and add `aria-disabled="true"` to the row `<div>` at line 41.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### C-03 · list-item · `selected` is colour-only and is never announced
|
||||
- Severity: must-fix
|
||||
- M3 says: "**Indicate selection with more than color** … don't rely on color as the only visual cue" and
|
||||
"Use two visual cues to show a list item is selected, like a leading checkmark and filled color"
|
||||
(raw `components_lists_accessibility.md:26-44`). Role mapping, same page: single-select and multi-select
|
||||
lists on **Web** → container role **List box**, item role **Option**, state **Selected / Not-selected**.
|
||||
- Library does: `list-item.blade.php:44` emits only `data-selected`; `resources/css/components/list.css:46-49`
|
||||
paints `secondary-container` / `on-secondary-container`. There is no `aria-selected`, no `aria-current`,
|
||||
no icon or checkmark, and the container stays `role="list"` / `role="listitem"`
|
||||
(`list.blade.php:19`, `list-item.blade.php:41`), where `listitem` cannot carry a selected state at all.
|
||||
- Fix: two parts. (a) Add a `selectable` (or `selection="single"|"multi"`) prop to `<x-list>` that switches
|
||||
the container to `role="listbox"` and each item to `role="option" aria-selected="true|false"`; keep
|
||||
`role="list"` as the default for non-selectable lists and use `aria-current="true"` there instead of
|
||||
nothing. (b) Render a second cue when `selected` — e.g. a trailing `check` icon in
|
||||
`on-secondary-container` — or document that callers must supply one (a `leading` checkbox/radio).
|
||||
- Effort: M
|
||||
- Breaks API? yes (a new prop; `role` on the container changes for selectable lists)
|
||||
|
||||
### C-04 · drawer · No close affordance by default, which M3 states as a requirement
|
||||
- Severity: must-fix
|
||||
- M3 says: "Material **requires** a close affordance (e.g. close icon button) to always be present —
|
||||
without one, users can't predict the sheet's open/close flow or tell if it's transient or permanent"
|
||||
(reference-components-a.md § Side sheets → Accessibility). Anatomy lists the close icon button for both
|
||||
the standard and the modal side sheet.
|
||||
- Library does: `resources/views/components/drawer.blade.php:33` — `'withCloseButton' => false`. With the
|
||||
default, a side sheet renders with a headline and no close control at all; the only exits are Escape
|
||||
(which `close-on-escape=false` removes) and the scrim (which `without-backdrop-close` removes). With both
|
||||
of those off, the sheet is undismissable.
|
||||
- Fix: flip the default to `'withCloseButton' => true` in `drawer.blade.php:33`, and make the close button
|
||||
unconditional (ignore the prop) when `closeOnEscape` is false or `withoutBackdropClose` is set. Same for
|
||||
`pane` mode, which has no scrim and, by default, no Escape.
|
||||
- Effort: S
|
||||
- Breaks API? yes (the default rendering of every existing `<x-drawer>` gains a close button)
|
||||
|
||||
### C-05 · carousel · Reduced motion still resizes items
|
||||
- Severity: must-fix
|
||||
- M3 says: "When reduced motion settings are turned on, the parallax effect should be removed and carousel
|
||||
items should no longer expand as they come into view. **All items are the same size**" (quoted verbatim
|
||||
in reference-styles.md § Motion → Accessibility requirements, from
|
||||
`components/carousel/accessibility`; also reference-components-a.md § Carousel → Accessibility).
|
||||
- Library does: `resources/js/carousel.js:966` sets `const pinned = state.reducedMotion.matches` and then
|
||||
uses it only for the content pin (`const pin = pinned ? … : 0`, line 979). The mask itself
|
||||
(`const inset = clamp((size - keyline.size) / 2, 0, size / 2)`, line 978) is still written to
|
||||
`--material-carousel-inset` on every frame, so items keep growing and shrinking between keylines — the
|
||||
exact behaviour M3 says to switch off. Only the parallax half of the rule is honoured.
|
||||
- Fix: in `render()` (carousel.js ~line 966-995), when `state.reducedMotion.matches`, write `inset = 0`
|
||||
(and `shift = 0`, `opacity = 1`) for every item so all items stay at `strategy.itemSize`; keep the
|
||||
keyline maths for snap positions. M3's extra note for hero under reduced motion — "the small item shows
|
||||
only partially" — then falls out of the scroll position rather than the mask.
|
||||
- Effort: M
|
||||
- Breaks API? no
|
||||
|
||||
### C-06 · modal · A scrolling dialog scrolls its headline and its action row away
|
||||
- Severity: should-fix
|
||||
- M3 says: "Scrolling: dialog content generally shouldn't scroll; if it must, **the title stays pinned at
|
||||
top and buttons pinned at bottom**, and the dialog never scrolls with background content"
|
||||
(reference-components-a.md § Dialogs → Behaviour and guidelines).
|
||||
- Library does: `resources/views/components/modal.blade.php:66` puts `overflow-y-auto` on the outer box,
|
||||
which contains the header block (lines 81-100), the body (line 102) and the actions (lines 105-112). The
|
||||
body wrapper at line 80 is `min-h-0 flex-1` with no overflow of its own, so once the content exceeds
|
||||
`max-h-[calc(100dvh-3rem)]` everything scrolls together. Only the `fullscreen` branch pins correctly
|
||||
(`max-sm:overflow-y-auto` on the inner div, line 80).
|
||||
- Fix: in `modal.blade.php`, remove `overflow-y-auto` from line 66 (keep `overflow-hidden`), give the
|
||||
header block `shrink-0`, and move `overflow-y-auto` onto the `min-h-0 flex-1` wrapper at line 80 for all
|
||||
cases (not just `max-sm:`). The actions already have `shrink-0`. Move `p-6` from the box to the three
|
||||
regions so the pinned header/footer keep their 24dp padding.
|
||||
- Effort: S
|
||||
- Breaks API? no (`box-class` callers that relied on the outer scroll would change)
|
||||
|
||||
### C-07 · drawer · The list-detail pane opens at 1280px; M3 puts two panes from 840px
|
||||
- Severity: should-fix
|
||||
- M3 says: list-detail visible panes — "Compact (0–599): 1 pane; Medium (600–839): 1 (recommended) or 2;
|
||||
**Expanded (840+): 2**; Large (1200–1599): 2; Extra-large (1600+): 2"
|
||||
(reference-foundations-supplement.md § Canonical layout examples → List-detail).
|
||||
- Library does: `drawer.blade.php:55` (`window.matchMedia('(min-width: 80rem)')`) and the `xl:` classes at
|
||||
lines 64-65 and 100 — the pane appears only from 1280px. Between 840 and 1279px (the whole expanded class
|
||||
and most of large) the detail still opens as a modal sheet over a scrim with the list inert, which is
|
||||
M3's compact behaviour.
|
||||
- Fix: change `80rem` to `52.5rem` (840px) in `drawer.blade.php:55` and swap the `xl:` prefixes for a
|
||||
custom `expanded:` variant defined as `@custom-variant expanded (@media (min-width: 52.5rem))` in
|
||||
`resources/css/tokens/theme.css`; update `SKILL.md:463` and the drawer header comment. If that is too
|
||||
aggressive for narrow laptop layouts, make the threshold a prop (`pane-from`) with 840 as the default.
|
||||
- Effort: M
|
||||
- Breaks API? yes (pages laid out with `xl:flex xl:items-start xl:gap-6` around the drawer would need the
|
||||
same breakpoint changed; that wrapper is documented in `SKILL.md:463`)
|
||||
|
||||
### C-08 · list.css · Segmented list rows lose their hover and press state layer to the cascade
|
||||
- Severity: should-fix
|
||||
- M3 says: list items show Hovered / Focused / Pressed / Dragged states (reference-components-a.md § Lists
|
||||
→ Specs), and "Cursor: hover shows a visible cue that the item is interactive" (§ Accessibility). State
|
||||
layer opacities: hover 8%, focus 10%, pressed 10% (reference-foundations, states).
|
||||
- Library does: `resources/css/components/list.css:42-44` sets
|
||||
`[data-list='segmented'] > [data-list-item] { background-color: var(--md-sys-color-surface-container); }`
|
||||
**unlayered**, while the hover (line 22-24), focus (27-31) and press (33-35) rules live inside
|
||||
`@layer components`. `resources/css/material.css` imports `list.css` without wrapping it, so the
|
||||
unlayered declaration wins over every layered one regardless of the `:where()` specificity — a segmented
|
||||
row gets no background state layer on hover or press. Only the corner morph (lines 93-100, unlayered)
|
||||
and the focus outline (a different property) still show.
|
||||
- Fix: move the `[data-list='segmented'] > [data-list-item]` background rule (lines 42-44, a duplicate of
|
||||
the selector at 79-81) into `@layer components` alongside the state rules, or paint the state layer with
|
||||
a `::before`/`background-image` instead of `background-color` so the two never collide.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### C-09 · bottom-sheet · Drag handle colour is on-surface-variant at 40%, not the role colour
|
||||
- Severity: should-fix
|
||||
- M3 says: "drag handle = On surface variant" (specs page colour roles, reference-components-a.md §
|
||||
Bottom sheets). `SheetBottomTokens.kt:30` — `DockedDragHandleColor get() = ColorSchemeKeyTokens.OnSurfaceVariant`;
|
||||
`SheetDefaults.kt:576` uses it undiluted (`color: Color = SheetBottomTokens.DockedDragHandleColor.value`),
|
||||
with no opacity multiplier.
|
||||
- Library does: `bottom-sheet.blade.php:60` — `bg-on-surface-variant/40`. The component's own header comment
|
||||
(line 10) says "a 32×4px drag handle in on-surface-variant", so the code contradicts its own doc.
|
||||
- Fix: `bg-on-surface-variant` in `bottom-sheet.blade.php:60`.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### C-10 · drawer · Bottom actions are right-aligned; the side-sheet spec says left
|
||||
- Severity: should-fix
|
||||
- M3 says: side sheet specs table (both standard and modal): "Bottom actions alignment (horizontal) |
|
||||
**Left**"; "Bottom actions height 72dp; top padding 16dp; bottom padding 24dp"
|
||||
(raw `components_side-sheets_specs.md:125` and `:199`).
|
||||
- Library does: `drawer.blade.php:135` — `flex shrink-0 flex-wrap items-center justify-end gap-2 pt-6`
|
||||
(right-aligned, 24px top padding, no bottom padding of its own beyond the sheet's `p-6`).
|
||||
- Fix: `justify-start` in `drawer.blade.php:135`, and `pt-4 pb-0` inside a 72px-min row
|
||||
(`min-h-18 pt-4`) to match 16/24/72. Note this deliberately differs from the dialog, whose actions are
|
||||
trailing-aligned — M3 specifies them differently.
|
||||
- Effort: S
|
||||
- Breaks API? yes (visual position of every existing drawer action row)
|
||||
|
||||
### C-11 · carousel · The full-screen layout scrolls horizontally; M3's scrolls vertically
|
||||
- Severity: should-fix
|
||||
- M3 says: "The full-screen carousel layout shows one edge-to-edge large item at a time and **scrolls
|
||||
vertically**"; "This layout works best with content that is taller than it is wide, and scrolls
|
||||
vertically. It only works in **portrait orientation in compact and medium breakpoints**. Don't use this
|
||||
layout in landscape orientation." (raw `components_carousel_guidelines.md:31,177-183`). Specs table:
|
||||
full-screen padding 0dp all round, 16dp between elements, edge-to-edge with no item radius.
|
||||
- Library does: `carousel.blade.php:127-131` renders every layout, full-screen included, as a horizontal
|
||||
`overflow-x-auto` row; `carousel-item.blade.php:35` gives every item `rounded-corner-xl` and the 28px
|
||||
clip-path. The header comment (line 22-23) describes full-screen as "one item the width of the carousel
|
||||
at a time", i.e. horizontal by design.
|
||||
- Fix: either (a) add a vertical mode for `layout="full-screen"` (`flex-col`, `overflow-y-auto`,
|
||||
`snap-y snap-mandatory`, items `h-full w-full` with no corner radius, arrow keys Up/Down) and clamp it to
|
||||
compact/medium widths, or (b) rename the layout so it does not claim to be M3's full-screen carousel and
|
||||
say so in `SKILL.md:471`. Also drop the 28px corner and the 8px gap for this layout (M3: edge-to-edge,
|
||||
16dp between elements).
|
||||
- Effort: L
|
||||
- Breaks API? yes if renamed; no for (a) if `layout="full-screen"` keeps its name
|
||||
|
||||
### C-12 · carousel · Leading/trailing padding defaults to 0 where M3 specifies 16dp
|
||||
- Severity: should-fix
|
||||
- M3 says: specs table — Multi-browse / Hero / Center-aligned hero: "Leading/trailing padding **16dp**,
|
||||
Top/bottom padding **8dp**, Padding between elements 8dp"; Uncontained: "16dp (leading only)"
|
||||
(reference-components-a.md § Carousel → Specs).
|
||||
- Library does: `carousel.blade.php:55` — `'padding' => 0`; the scroller at lines 127-131 has no vertical
|
||||
padding at all. Every showcase example (`resources/views/showcase/sections/carousel.blade.php`) therefore
|
||||
renders with 0 end padding, so items sit flush against the container edge.
|
||||
- Fix: default `'padding' => 16` in `carousel.blade.php:55` (and pass leading-only for `uncontained`, 0 for
|
||||
`full-screen`); add `py-2` to the scroller class list at line 128. Update `SKILL.md:471` where it
|
||||
documents "`padding` (px at the ends, 0)".
|
||||
- Effort: S
|
||||
- Breaks API? no (a default value changes; explicit `padding="0"` still works)
|
||||
|
||||
### C-13 · card · No elevation change on hover, focus, press or drag
|
||||
- Severity: should-fix
|
||||
- M3 says: per-state elevation from `ElevatedCardTokens.kt` / `FilledCardTokens.kt` / `OutlinedCardTokens.kt`
|
||||
(reference-components-a.md § Cards → cross-check table): elevated 1dp rest → **3dp hover** → 1dp
|
||||
focus/pressed → **8dp dragged**; filled 0 → **1dp hover** → 0 → **6dp dragged**; outlined 0 → **1dp
|
||||
hover** → 0 → 6dp dragged. The specs page shows Hovered / Focused / Pressed / Dragged / Disabled for all
|
||||
three variants (raw `components_cards_specs.md:78,128,180`).
|
||||
- Library does: `card.blade.php:34-36` sets one elevation and never changes it
|
||||
(`shadow-elevation-1` for elevated, none for filled/outlined). The only interactive response is in
|
||||
`resources/css/components/list.css:52-72`, and that is a state layer plus a corner morph — no
|
||||
`box-shadow` level change. There is no dragged state anywhere in the group.
|
||||
- Fix: add hover/focus elevation to `card.blade.php` for interactive cards — e.g. in `list.css`
|
||||
`[data-card][data-list-row]:hover { box-shadow: var(--md-sys-elevation-2), inset … }` for the elevated
|
||||
variant and `var(--md-sys-elevation-1)` for filled/outlined. Needs a variant marker on the element
|
||||
(`data-card="elevated|filled|outlined"` instead of the bare `data-card` at `card.blade.php:31`).
|
||||
- Effort: M
|
||||
- Breaks API? no (`data-card` gains a value; the bare attribute selector still matches)
|
||||
|
||||
### C-14 · list-item · Leading/trailing gap is 12px where M3 uses 16dp
|
||||
- Severity: should-fix
|
||||
- M3 says: `ListItem.kt` (androidx-main) lines 1269 and 1273 —
|
||||
`internal val LeadingContentEndPadding = 16.dp`, `internal val TrailingContentStartPadding = 16.dp`;
|
||||
`ListTokens.kt:171,338` — `ItemLeadingSpace = 16.dp`, `ItemTrailingSpace = 16.dp`.
|
||||
- Library does: `list-item.blade.php:46` — `flex items-center gap-3 px-4` (12px gap). The container padding
|
||||
(`px-4` = 16px) is correct; only the internal gaps are short.
|
||||
- Fix: `gap-4` in `list-item.blade.php:46`.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### C-15 · list-item · Three-line items are middle-aligned; M3 top-aligns them
|
||||
- Severity: should-fix
|
||||
- M3 says: "Alignment: elements are middle-aligned by default; **top-aligned if the item is 88dp+ or has 3+
|
||||
lines of text**" (reference-components-a.md § Lists → Specs, from the overview page). Compose confirms
|
||||
the vertical padding also changes: `ListItem.kt:1259,1261` —
|
||||
`ListItemVerticalPadding = 8.dp`, `ListItemThreeLineVerticalPadding = 12.dp`.
|
||||
- Library does: `list-item.blade.php:46` — `items-center` for every case; padding is `py-2` (8px) for one
|
||||
line and `py-2.5` (10px) for both two- and three-line items (lines 47-49), where M3 wants 8 and 12.
|
||||
- Fix: in `list-item.blade.php:45-51`, add `'items-start' => $lines === 2` (alongside `items-center` for
|
||||
the others) and change `'min-h-22 py-2.5' => $lines === 2` to `'min-h-22 py-3' => $lines === 2`; leave
|
||||
`py-2` for `$lines === 0` and use `py-2` for `$lines === 1` too.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### C-16 · list · Dividers between items run edge to edge; the list token insets them 16dp
|
||||
- Severity: should-fix
|
||||
- M3 says: `ListTokens.kt:30,36` — `DividerLeadingSpace = 16.dp`, `DividerTrailingSpace = 16.dp` (top/bottom
|
||||
space 0). Guidelines: inset dividers "separate related content within one section (e.g. emails in a
|
||||
list); indented equally from both sides by default; pair with anchoring elements like icons/avatars"
|
||||
(reference-components-a.md § Divider).
|
||||
- Library does: `list.blade.php:25` — `'divide-y divide-outline-variant' => $dividers && ! $segmented`,
|
||||
which draws a full-bleed 1px rule between items. `<x-divider>` has `inset` and `middle` props, but the
|
||||
list never uses them and there is no way to ask for an inset list divider.
|
||||
- Fix: in `list.blade.php`, render the divide with a 16px inset — e.g. add
|
||||
`[&>[data-list-item]:not(:last-child)]:after` or keep `divide-y` and add `mx-4` to the rule via a small
|
||||
CSS block in `resources/css/components/list.css`: `[data-list='plain'][data-dividers] > [data-list-item]:not(:last-child) { box-shadow: inset 0 -1px 0 … }` inset by 16px. Alternatively give `<x-list>` a
|
||||
`dividers="full"|"inset"|"middle"` value instead of a boolean.
|
||||
- Effort: M
|
||||
- Breaks API? no if `dividers` stays boolean-compatible
|
||||
|
||||
### C-17 · modal · Full-screen dialog header is 64px where M3 specifies 56dp
|
||||
- Severity: should-fix
|
||||
- M3 says: full-screen dialog specs table — "Header height | **56dp**"; "Bottom action bar height | 56dp";
|
||||
"Top/left/right padding 24dp" (reference-components-a.md § Dialogs → Specs).
|
||||
- Library does: `modal.blade.php:71` — `flex h-16 shrink-0 items-center gap-1 px-1 sm:hidden` (64px), and
|
||||
the bottom action bar at line 108 is `max-sm:px-6 max-sm:py-4` around a 40px button ≈ 72px.
|
||||
- Fix: `h-14` (56px) at `modal.blade.php:71`; `max-sm:min-h-14 max-sm:py-2` at line 108. Keep `px-1` on the
|
||||
bar so the close icon button's 48px target still reaches the 24dp text margin (M3 aligns the header
|
||||
headline to 24dp from the edge; the icon button's own padding supplies it).
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### C-18 · carousel · Tab lands on the container, which M3 explicitly tells you not to do
|
||||
- Severity: should-fix (two readings — see below)
|
||||
- M3 says: "When navigating to a carousel using assistive technology, use **Tab** to place initial focus on
|
||||
the **first carousel item**"; and the caption under the Don't image: "**Avoid focusing on the carousel
|
||||
container**" (raw `components_carousel_accessibility.md:130,146`). Keyboard table: "Tab or Arrows — Moves
|
||||
to the previous or next carousel item; Space or Enter — Activates the focused carousel item".
|
||||
- Library does: `carousel.blade.php:123-126` gives the scroller `role="region"`,
|
||||
`aria-roledescription="carousel"` and **`tabindex="0"`**; the items (`carousel-item.blade.php:23-32`) are
|
||||
`role="group"` with no `tabindex`, so they are never focusable and Space/Enter cannot activate one. The
|
||||
arrow keys only work while the container itself has focus (`carousel.js:1065-1068` returns early unless
|
||||
`event.target === this.$refs.scroller`). The header comment (lines 37-43) says this is deliberate:
|
||||
"WAI-ARIA's carousel pattern: the row is a focusable `region`".
|
||||
- Both readings: the library's choice satisfies WCAG 2.1.1 for a scrollable region whose content may be
|
||||
non-focusable (an `<img>`-only slide), which is the ARIA-APG "scrollable region" practice; M3's rule
|
||||
assumes every item is itself an actionable target. They conflict; M3's is the stated rule here.
|
||||
- Fix: give each `<x-carousel-item>` `tabindex="0"` and handle Arrow/Home/End/Space/Enter on the focused
|
||||
item (roving tabindex), keeping the container out of the tab order — or, if the current pattern is kept,
|
||||
record it in the "deliberate deviations" of `SKILL.md:471` with the M3 quote so a reviewer does not
|
||||
re-litigate it.
|
||||
- Effort: M
|
||||
- Breaks API? no
|
||||
|
||||
### C-19 · bottom-sheet · Default height is 90dvh; M3 caps a modal sheet's initial position at 50%
|
||||
- Severity: should-fix
|
||||
- M3 says: "Modal: … Initial vertical position is **capped at 50% of screen height**; if content exceeds
|
||||
that, it can be pulled to full screen and scrolled internally." Specs table: "Top margin 72dp; Top margin
|
||||
(window width > 640dp) 56dp; Start/end margin (window width > 640dp) 56dp"
|
||||
(reference-components-a.md § Bottom sheets).
|
||||
- Library does: `bottom-sheet.blade.php:17` — `'height' => '90dvh'`, applied as
|
||||
`max-h-(--sheet-max-height)` (line 55). There is one height and no preset-height cycling, so a sheet may
|
||||
open at 90% of the viewport where M3 would open at 50% and let the user pull it up. The 56dp side margin
|
||||
above 640px is also absent (`mx-auto … max-w-160`, line 55, with no horizontal margin).
|
||||
- Fix: default `'height' => '50dvh'` with a `max-height` ceiling of `calc(100dvh - 72px)` in
|
||||
`bottom-sheet.blade.php:17,55`, and add `sm:px-14` (56px) to the wrapper or `sm:max-w-[calc(100vw-7rem)]`.
|
||||
A second preset height, cycled by the drag handle's click, would complete M3's "selecting the drag handle
|
||||
toggles preset heights" rule; today the click only closes.
|
||||
- Effort: M
|
||||
- Breaks API? yes (default sheet height changes)
|
||||
|
||||
### C-20 · modal · No `role="alertdialog"` on a basic dialog
|
||||
- Severity: should-fix (two readings)
|
||||
- M3 says: "On web, basic dialogs should have the **alert dialog** role"; "Basic dialogs are known as alert
|
||||
dialogs on web" (raw `components_dialogs_accessibility.md:130,134`).
|
||||
- Library does: `modal.blade.php:41` uses a native `<dialog>` opened with `showModal()`
|
||||
(line 51), which the browser maps to `role="dialog"` + `aria-modal="true"`. No `role` is set, and no
|
||||
`aria-describedby` points at the supporting text (line 93).
|
||||
- Both readings: ARIA-APG restricts `alertdialog` to dialogs that "interrupt … to communicate an important
|
||||
message" and requires an `aria-describedby` message; applying it to every `<x-modal>` (including forms,
|
||||
which the `fullscreen` variant is explicitly for) would over-announce. A middle path matches both.
|
||||
- Fix: add an `alert` boolean prop to `modal.blade.php` that sets `role="alertdialog"` plus
|
||||
`aria-describedby="{{ $id }}-body"`, and use it in the showcase's destructive-confirmation example
|
||||
(`showcase/sections/containment.blade.php:73`). Add `aria-describedby` for the subtitle unconditionally.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### C-21 · list · Segmented items use `surface-container`; the token says `Surface`
|
||||
- Severity: should-fix
|
||||
- M3 says: `ListTokens.kt:201` — `ItemSegmentedContainerColor get() = ColorSchemeKeyTokens.Surface`
|
||||
(`ItemContainerColor` is also `Surface`). `SegmentedGap = 2.0.dp` (line 353).
|
||||
- Library does: `resources/css/components/list.css:43` and `:79-81` —
|
||||
`background-color: var(--md-sys-color-surface-container)`. The 2px gap (`list.blade.php:24`,
|
||||
`gap-0.5`) and the 4px/16px corner morph are right; only the fill is a tone off.
|
||||
- Fix: `var(--md-sys-color-surface)` in `list.css:43`. Note this only reads as "segmented" when the page
|
||||
behind it is a container tone; if the library prefers the stronger tone, say so in the file comment,
|
||||
because the header currently claims "each item its own surface-container tile … (ListTokens)".
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### C-22 · list.css · A focused segmented row does not morph to the large corner
|
||||
- Severity: nice-to-have
|
||||
- M3 says: "Interaction-state expressive shapes: hovered = Medium (12dp); **focused**/pressed/dragged/
|
||||
selected-any-state = Large (16dp)" (reference-components-a.md § Lists → Specs, cross-checked against
|
||||
`ListTokens.kt` — `ItemPressedContainerExpressiveShape` = CornerLarge,
|
||||
`ItemDraggedContainerExpressiveShape` = CornerLarge).
|
||||
- Library does: `resources/css/components/list.css:93-100` covers `:hover` (→ md) and
|
||||
`:is([data-selected], [data-list-row]:active)` (→ lg). `:focus-visible` is not in either selector, so a
|
||||
keyboard-focused segmented row keeps the 4px corner.
|
||||
- Fix: extend the selector at `list.css:99` to
|
||||
`[data-list='segmented'] > [data-list-item]:is([data-selected], [data-list-row]:active, [data-list-row]:has([data-list-open]:focus-visible))`.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### C-23 · card · The corner morphs 12→16 on hover, which M3 cards do not do
|
||||
- Severity: nice-to-have
|
||||
- M3 says: cards have one shape — "Shape | 12dp corner radius" for all three variants, with no press or
|
||||
hover shape listed in `ElevatedCardTokens.kt` / `FilledCardTokens.kt` / `OutlinedCardTokens.kt`
|
||||
(reference-components-a.md § Cards → Specs). Shape morph is specified for buttons, FABs and list items,
|
||||
not cards.
|
||||
- Library does: `resources/css/components/list.css:59-62` —
|
||||
`[data-card][data-list-row]:hover { border-radius: var(--md-sys-shape-corner-lg); … }`. The
|
||||
`card.blade.php` header (lines 10-14) documents this: "It answers with a state layer and its corner
|
||||
opening a step."
|
||||
- Fix: either drop the `border-radius` line at `list.css:60` (keeping the state layer, which M3 does
|
||||
require for a directly-actionable card) or keep it and note in the card header that this is an Expressive
|
||||
extension M3 does not specify for cards. The state layer itself is correct and should stay.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### C-24 · collapse · `interpolate-size` is set but nothing animates the height
|
||||
- Severity: nice-to-have
|
||||
- M3 says: n/a — collapse is not an M3 component; foundations require only that motion respect reduced
|
||||
motion, which the token system already does (`resources/css/tokens/motion.css:63-75`).
|
||||
- Library does: `collapse.blade.php:49` adds `[interpolate-size:allow-keywords]` and the header comment
|
||||
(lines 7-8) promises "where the browser supports animating `details` content (`interpolate-size`) — a
|
||||
height that eases open". There is no `transition: height` and no `::details-content` rule anywhere in
|
||||
`resources/css/` (grepped: `interpolate-size` and `details-content` appear only in this file), so the
|
||||
content snaps open. `interpolate-size` on its own changes nothing.
|
||||
- Fix: add to a component stylesheet:
|
||||
`details.group\/collapse::details-content { block-size: 0; overflow: hidden; transition: block-size var(--md-sys-motion-spatial-fast-duration) var(--md-sys-motion-spatial-fast), content-visibility var(--md-sys-motion-spatial-fast-duration) allow-discrete; }`
|
||||
and `details[open].group\/collapse::details-content { block-size: auto; }` — or delete the
|
||||
`interpolate-size` utility and the sentence in the header comment.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### C-25 · carousel-item · The overlay label is `text-white`, outside the role set
|
||||
- Severity: nice-to-have
|
||||
- M3 says: "Layering text/icons on images is **not recommended**; if necessary, add a translucent scrim or
|
||||
a bounding shape behind the text/icon to guarantee accessible contrast" (reference-components-a.md §
|
||||
Cards → Behaviour; the same rule is the reason the carousel's item content is art). Colour must come from
|
||||
a scheme role.
|
||||
- Library does: `carousel-item.blade.php:46` — `type-title-md text-white` over
|
||||
`bg-linear-to-t from-scrim/60` (line 44). The scrim is right; `white` is a literal, not a role, so it
|
||||
does not follow a scheme or a high-contrast profile.
|
||||
- Fix: `text-inverse-on-surface` (which is near-white in a light scheme and dark in a dark one — check the
|
||||
intent) or add an explicit `--md-sys-color-on-scrim` style token if a fixed light-on-dark is wanted for
|
||||
both schemes; a literal `white` is defensible over a 60% black scrim but should be stated as such in the
|
||||
file comment.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### C-26 · list-item · Leading icon stays 24px in a segmented (expressive) list
|
||||
- Severity: nice-to-have
|
||||
- M3 says: `ListTokens.kt:153,156,332,335` — `ItemLeadingIconExpressiveSize = 20.dp` /
|
||||
`ItemLeadingIconSize = 24.dp`; `ItemTrailingIconExpressiveSize = 20.dp` / `ItemTrailingIconSize = 24.dp`.
|
||||
The library's segmented list is explicitly the expressive variant (`list.blade.php:4-6`).
|
||||
- Library does: `list-item.blade.php:64` and `:97` — `size-6` (24px) in both cases, regardless of the
|
||||
parent list's mode.
|
||||
- Fix: the item does not know its parent, so use CSS:
|
||||
`[data-list='segmented'] > [data-list-item] svg { width: 20px; height: 20px }` in
|
||||
`resources/css/components/list.css`, scoped to the leading/trailing icons only.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### C-27 · mail · The card corner is 24px, a value not on M3's shape scale
|
||||
- Severity: nice-to-have
|
||||
- M3 says: the corner scale is 0 / 4 / 8 / 12 / 16 / 20 / 28 / 32 / 48 (reference-styles.md § Shape; the
|
||||
library's own `resources/css/tokens/shape.css:11-20` reproduces it). 24 is not a step.
|
||||
- Library does: `resources/views/mail/theme.blade.php` `.inner-body { border-radius: 24px; }`; the panel
|
||||
uses `16px` (on-scale) and `code` uses `4px` (on-scale).
|
||||
- Fix: `border-radius: 28px` (extra-large) on `.inner-body`, matching the dialog/bottom-sheet surface tone
|
||||
this card stands in for. Everything else in the theme is on-scale.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### C-28 · mail · The header app name is title-large at weight 500, not 400
|
||||
- Severity: nice-to-have
|
||||
- M3 says: title-large = weight 400 (Regular), 22/28, tracking 0 — the library's own
|
||||
`resources/css/tokens/type.css:39-41` has `--md-sys-typescale-title-lg` at regular and
|
||||
`--md-sys-typescale-emphasized-title-lg` at medium.
|
||||
- Library does: `resources/views/mail/theme.blade.php` `.header a { font-size: 22px; font-weight: 500; … }`,
|
||||
while the section comment says "the app name in title-lg". `h2` right below it uses 22/28 at weight 400,
|
||||
so the two disagree within the same file.
|
||||
- Fix: either set `font-weight: 400` on `.header a`, or change the comment to say
|
||||
title-large-**emphasized** — which is the more likely intent for a brand line and is a real M3 role.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
## Deliberate deviations
|
||||
|
||||
- **`card.blade.php:16-17` — "Do not pass a `bg-*` class … it races the card's own in Tailwind's emit
|
||||
order."** True and correctly explained; the CSS in `list.css` is unlayered for the same reason
|
||||
(`list.css:10-11`). Holds up, and is the cause of C-08 (the same unlayered trick applied to the segmented
|
||||
background, where it is not needed).
|
||||
- **`card.blade.php:10-14` / `list-rows.js` — the `data-list-row` + `data-list-open` contract instead of a
|
||||
stretched link or a wrapping `<a>`.** M3's accessibility page for cards says exactly this: "on a
|
||||
**directly actionable card** Tab moves to the next card container; on a **non-actionable card with
|
||||
actionable elements**, Tab moves through each actionable element inside before moving to the next card"
|
||||
and forbids stacking an action on an already-actionable surface. The library implements the second case
|
||||
with a single real control. Holds up, and is a better answer than M3 gives for the web.
|
||||
- **`drawer.blade.php:9-11` — "it enters on emphasized decelerate rather than a spring — a sheet anchored
|
||||
to the edge that overshot would open a gap."** M3 Expressive's motion scheme is springs for spatial
|
||||
change, but `styles/motion` keeps the emphasized-decelerate curve for exactly this kind of entrance and
|
||||
the reference records no rule against it. Holds up.
|
||||
- **`modal.blade.php:24` — "It opens on the fast spatial spring and closes at once, as M3's do."** The
|
||||
first half is fine; the second is not supported by Google's text, which says a dialog "appears via an
|
||||
**enter/exit** transition" (reference-components-a.md § Dialogs → Behaviour). Native `<dialog>` makes an
|
||||
exit transition awkward (`@starting-style` only covers entry), which is the real reason — worth saying so
|
||||
instead. Folded into no finding of its own; it is a doc accuracy point.
|
||||
- **`list.blade.php:8` — `role="list"` rather than `listbox`.** Correct for a plain list of links; it is
|
||||
only wrong once `selected` is used, which is C-03.
|
||||
- **`carousel.blade.php:37-43` — the WAI-ARIA carousel pattern rather than M3's "focus the first item".**
|
||||
See C-18; a genuine standards conflict, but M3's Don't is explicit and is not acknowledged in the code.
|
||||
- **`mail/theme.blade.php:9-26` — hexes only, light only, no `@media`, no elevation.** Every reason given
|
||||
is correct (CssToInlineStyles' `doCleanup()` does strip `@media`; Outlook does drop alpha). Accepted by
|
||||
the brief and by the constraints.
|
||||
- **`collapse.blade.php:1-2` — "Not an M3 component; built on the native `<details>`."** Correct; M3 has
|
||||
expandable list items and menu expansion but no standalone disclosure. Judged against foundations it is
|
||||
sound: 48px summary (`min-h-12`), the 8/10% state layer, the 3px secondary focus ring, reduced motion via
|
||||
the duration tokens, and the native `aria-expanded`.
|
||||
|
||||
## Aligned
|
||||
|
||||
- **Card**: filled `surface-container-highest`, elevated `surface-container-low` + elevation 1, outlined
|
||||
`surface` + 1px `outline-variant` — all three match the specs page and the token files. `rounded-corner-md`
|
||||
= 12dp; `p-4` = the 16dp left/right padding; `overflow-hidden` + a full-bleed `figure` slot is the media
|
||||
anatomy. (Card typography is not specified by M3 at all — the specs page lists no type roles — so
|
||||
`type-title-md` / `type-body-md` cannot be marked wrong.)
|
||||
- **List heights and leading sizes**: `min-h-14` / `min-h-18` / `min-h-22` = 56 / 72 / 88dp exactly
|
||||
(`ListTokens.kt:180,323,347`); avatar `size-10` = 40dp (`ItemLeadingAvatarSize`), image `size-14` = 56dp
|
||||
(`ItemLeadingImageWidth`) with `rounded-corner-sm` = the expressive `CornerSmall`; icon `size-6` = 24dp
|
||||
(baseline `ItemLeadingIconSize`); `px-4` = 16dp container padding.
|
||||
- **List type roles**: title `type-body-lg` (`ItemLabelTextFont` = BodyLarge), description `type-body-md`
|
||||
(`ItemSupportingTextFont` = BodyMedium), overline and trailing text `type-label-sm`
|
||||
(`ItemOverlineFont` / `ItemTrailingSupportingTextFont` = LabelSmall), `line-clamp-2` on the description.
|
||||
- **Selected colours**: `secondary-container` / `on-secondary-container` is right —
|
||||
`ListTokens.kt:204,281,284` (`ItemSelectedContainerColor` = SecondaryContainer,
|
||||
`ItemSelectedLabelTextColor` / `ItemSelectedLeadingIconColor` = OnSecondaryContainer). (The reference's
|
||||
specs-page note about "Primary container" is contradicted by the token file; the library follows the
|
||||
tokens.) Disabled content at 38% matches `ItemDisabled*Opacity = 0.38f`.
|
||||
- **Segmented list geometry**: 2px gap = `SegmentedGap`; 4px item corner = `ItemContainerExpressiveShape`
|
||||
(CornerExtraSmall); 16px at the list's ends and while pressed/selected = `ContainerShape` /
|
||||
`ItemSelectedContainerExpressiveShape` (CornerLarge); 12px on hover = the specs page's Medium.
|
||||
- **`data-list-row` state layer**: hover 8%, focus 10%, pressed 10% on `on-surface`, hover gated behind
|
||||
`@media (hover: hover)`; the focus ring is 3px `secondary` (matching `ListTokens.kt:39`
|
||||
`FocusIndicatorColor` = Secondary); the opener's own ring is suppressed so the row shows one indicator.
|
||||
- **Divider**: 1px (`DividerTokens.Thickness`), `outline-variant`, `inset` = 16px start / 0 end,
|
||||
`middle` = 16px both, a vertical variant, `role="separator"` with `aria-orientation`, and `decorative` to
|
||||
hide it — a complete match to the specs table.
|
||||
- **Dialog**: `surface-container-high` + `rounded-corner-xl` (28dp) + `shadow-elevation-3` =
|
||||
`DialogTokens` exactly; `p-6` = 24dp all round; `max-w-[35rem]` / `min-w-70` = 560 / 280dp;
|
||||
`backdrop:bg-scrim/32` = `ScrimTokens.ContainerOpacity = 0.32f`; `type-headline-sm` =
|
||||
`DialogTokens.HeadlineFont`; `type-body-md` on-surface-variant = `SupportingTextFont`; a 24px
|
||||
`text-secondary` hero icon that centres the headline (M3: "Alignment with icon: Center-aligned");
|
||||
`gap-2` = the 8dp between buttons; `mt-4` = the 16dp title↔body and icon↔title gaps; `pt-6` = the 24dp
|
||||
body↔actions gap; `justify-end` = trailing-edge actions, and the showcase orders Cancel before Delete
|
||||
(`showcase/sections/containment.blade.php:75-76`), which is M3's "dismissive to the left of confirming".
|
||||
`wire:ignore.self` + native `showModal()` gives top-layer, inert background, focus-in/focus-return and
|
||||
Escape for free.
|
||||
- **Bottom sheet**: `surface-container-low` (`DockedContainerColor`), `rounded-t-corner-xl` (28dp top,
|
||||
`CornerExtraLargeTop`), `shadow-elevation-1` (`DockedModalContainerElevation` = Level1),
|
||||
`max-w-160` = the 640dp max width, 32×4px handle geometry, 32% scrim, `x-trap.inert.noscroll`,
|
||||
dismissal by scrim / Escape / downward drag, and a single-pointer alternative to the drag (the handle is
|
||||
a real `<button>` with an accessible name and `role=button`, which is M3's "label only the drag handle").
|
||||
- **Side sheet**: `surface-container-low`, a 16px corner on the inner edge only
|
||||
(M3's "16dp corner radius for modal side sheets"), 400px default width = the specs max-width, `p-6` =
|
||||
24dp start/end padding, end placement by default (M3: "usually the right"), `role="dialog"` (M3's stated
|
||||
role), full height, independent vertical scroll and no horizontal scroll, `x-trap.inert.noscroll`, a
|
||||
container query on the body so contents lay out by the sheet's width.
|
||||
- **Carousel**: the Compose keyline maths is ported with attribution and a commit hash; small items clamp
|
||||
to 40-56dp (`MIN_SMALL_ITEM_SIZE` / `MAX_SMALL_ITEM_SIZE`, carousel.js:64-65); 28px item corner
|
||||
(`CarouselDefaults`); 8px between items (`gap-2` = the specs' "Padding between elements 8dp"); snap for
|
||||
multi-browse / hero / full-screen and free scroll for uncontained, exactly M3's recommendation;
|
||||
per-item "n of m" labels; controls placed **below** the row, never over it (M3's explicit Don't); RTL
|
||||
mirroring; re-measure on resize and after a morph.
|
||||
- **Collapse**: 48px summary, the shared state layer and focus ring, a chevron on the fast spatial spring,
|
||||
reduced motion via zeroed duration tokens, native disclosure semantics, `wire:ignore.self` for morphs.
|
||||
- **Error pages**: `bg-surface` / `text-on-surface`, `type-emphasized-display-lg` in `on-primary-container`
|
||||
over a `primary-container` shape (a correct contrast pair), `type-headline-md`/`lg` for the headline,
|
||||
`type-body-lg` in `on-surface-variant` for the message, a filled primary action with a text secondary
|
||||
(M3's action hierarchy), 24px page gutters, and the decorative shape's rotation gated behind
|
||||
`prefers-reduced-motion: no-preference`.
|
||||
- **Mail theme**: the typescale is reproduced correctly in px — h1 24/32/400/0 (headline-small),
|
||||
h2 22/28 (title-large), h3 16/24/500/0.15 (title-medium), p 16/24/400/0.5 (body-large),
|
||||
`p.sub` and table cells 14/20/0.25 (body-medium), table head 14/20/500/0.1 (title-small),
|
||||
footer 12/16/0.4 (body-small), the button 16/24/500/0.15 (title-medium, the Expressive medium button's
|
||||
label) with a full corner. Roles are used properly throughout (`on-surface` for emphasis,
|
||||
`on-surface-variant` for body, `outline-variant` for every rule, `surface-container-lowest` for the card
|
||||
against `surface-container` for the page), and separation is by tone rather than shadow, as M3 does.
|
||||
|
||||
## Missing
|
||||
|
||||
- **Lists**: leading **video** slot (56×100dp small, 64×114dp large — `ListTokens.kt:129,132,177`); the
|
||||
**expand/collapse** list-item interaction (M3: "items containing nested items can expand/collapse …
|
||||
container-transform"); explicit **selection modes** (single-select / multi-select / single-action /
|
||||
multi-action) with their role and keyboard mappings; **inset and middle dividers** between list items
|
||||
(only a full-bleed `dividers` boolean exists); a **dragged** state (16% layer, elevation 4) for
|
||||
reorderable lists.
|
||||
- **Cards**: per-state **elevation** (C-13) and the **dragged** state; a first-class "directly actionable
|
||||
card" that takes a `button`/`link` role — M3 says such a card gets one, and the `data-list-row` pattern
|
||||
deliberately keeps the role on the inner opener instead.
|
||||
- **Dialogs**: no divider pinned between a scrolling body and the header/actions (the `separator` prop
|
||||
scrolls with the content); no 56dp edge-margin rule for custom-positioned dialogs on large screens; no
|
||||
"discard unsaved changes" confirmation helper for the full-screen variant, which M3 requires of that
|
||||
variant.
|
||||
- **Bottom sheets**: **preset heights** and the handle's "cycle through heights on activation" behaviour
|
||||
(M3 requires a non-drag alternative whenever more than one height exists); a peek/collapsed height for
|
||||
the `standard` variant; the 56dp side margin above a 640dp window; the swap to a side sheet at expanded
|
||||
widths that M3 recommends.
|
||||
- **Side sheets**: a genuine **standard** (co-planar, non-modal, 0dp elevation) variant — `pane` is close
|
||||
but is scoped to list-detail and starts at `xl`; the **back icon button** in the modal anatomy; a divider
|
||||
above the action row; the 16dp "detached" inset M3 allows.
|
||||
- **Carousel**: the **uncontained multi-aspect-ratio** layout (added November 2025 — items from 9:16 to
|
||||
16:9); a **"Show all"** affordance opening a vertical list of every item, which M3 requires on
|
||||
vertically-scrolling pages; a vertically-scrolling full-screen layout (C-11).
|
||||
- **Divider**: the divider-with-text / subheader configuration (4dp gap to the supporting text, 8dp right
|
||||
and bottom margins in the specs table).
|
||||
|
||||
## Breakpoint map
|
||||
|
||||
| Component | Library breakpoint used | M3 window size class it stands in for | Gap |
|
||||
| --- | --- | --- | --- |
|
||||
| `<x-modal fullscreen>` — full-screen below, basic above (`modal.blade.php:61,67,71,80,83,89,93,108`) | `max-sm` → < 640px | Compact (< 600dp): "full-screen dialogs are used only in compact breakpoints" | 40px too wide — a 600-639px window (small tablet portrait, split-screen) gets a full-screen dialog where M3 wants a basic one |
|
||||
| `<x-drawer pane>` — pane vs modal sheet (`drawer.blade.php:55,64,65,100`; `xl:` = 1280px) | `xl` → ≥ 1280px | Expanded (≥ 840dp): list-detail shows **2 panes** from expanded through extra-large | 440px late — the entire expanded class (840-1199) and the bottom of large get the compact single-pane modal behaviour |
|
||||
| `<x-drawer>` — fixed width and inner corner switch on (`drawer.blade.php:97-99`) | `sm` → ≥ 640px | Compact (< 600dp) is where a modal side sheet should be full-bleed; medium (600-839) onwards it has its fixed width | 40px too wide — a 600-639px window still gets a full-width sheet |
|
||||
| `<x-bottom-sheet>` — 640px cap applied at every width (`bottom-sheet.blade.php:55`, `max-w-160`) | none (a `max-width`, not a breakpoint) | Specs: "Width: full width, up to max-width 640dp"; above a 640dp window also "Start/end margin 56dp" and "Top margin 56dp" | The 640 cap is right; the 56dp side/top margins above 640dp are missing entirely |
|
||||
| `<x-modal>` — dialog gutters (`modal.blade.php:58`, `w-[calc(100vw-3rem)]`, `max-h-[calc(100dvh-3rem)]`) | none | Guidelines: custom-positioned dialogs on larger screens "must respect a 56dp margin from screen edges" | 24px used where 56dp is specified — only binding for custom-positioned dialogs, which the component does not offer, so informational |
|
||||
| `<x-carousel controls>` (`carousel.blade.php:139`, `pointer-fine:`) | input-media query, not a width | M3 gives no breakpoint for carousel controls; it only says put them above or below the row | No gap — an input-capability query is a reasonable substitute and the placement rule is followed |
|
||||
| `<x-carousel layout="full-screen">` (`carousel.blade.php:62`) | none — rendered at every width | Compact and medium only, portrait only ("Don't use this layout in landscape orientation") | Unbounded — nothing stops the layout above 840dp or in landscape |
|
||||
@@ -0,0 +1,292 @@
|
||||
# Audit: core (tokens, scheme, theme, icons, shapes, guard, showcase foundations, AI docs)
|
||||
|
||||
Ground truth: `docs/reference/m3/styles.md`, `docs/reference/m3/foundations.md`, raw pages under `m3/raw/`.
|
||||
Library paths are relative to `/Users/surtic86/Privat/Development/livewire-material`.
|
||||
|
||||
## Summary
|
||||
|
||||
The core is in good shape: every 2025-spec colour role (the `*-dim` roles and the fixed roles included) is generated and mapped, the legacy `surface-tint`/`surface-variant` are correctly left out, the corner scale is M3 Expressive's ten steps, the five shadow levels are Google's web geometry, the six springs carry the exact Compose damping/stiffness constants, the state layer uses M3's 8/10/10 % and hover is pointer-only, the typescale sizes/line heights/weights match, and theme switching honours both the OS and the visitor's choice. The 35 shapes and the full Symbols set are ported from Google's sources.
|
||||
|
||||
The gaps are around the edges of the system rather than in it. (1) The three custom state colours (`success`/`warning`/`info`) are built with the 2021 `customColor` recipe, so they ignore the `--contrast` level and the 2025 spec's tone rules that `error` follows. (2) Only the standard contrast level exists; M3 (May 2025) makes standard/medium/high part of the colour system. (3) The semantic ink utilities (`text-meta` 60 %, `text-quiet` 38 %, `border-divider` 40 %) are M2-style opacity emphasis, which M3 replaced with `on-surface-variant`/`outline-variant`, and 38 % collides with M3's disabled opacity. (4) Tailwind's own radius, shadow, type-size, weight, easing and duration utilities still compile, so an application (or an AI agent) can silently leave the M3 scale; only colours were cleared. (5) There are no tokens or variants for M3's five breakpoints, no `dragged` state, and no shared touch-target utility. (6) Type tracking follows the older material-web numbers rather than Compose's, and the emphasized styles reuse baseline tracking. (7) The AI docs list the utilities but explain none of M3's rules for choosing between them; that is the "core concept" document the user asked for.
|
||||
|
||||
Counts: must-fix 1 · should-fix 12 · nice-to-have 11.
|
||||
|
||||
## Findings
|
||||
|
||||
### C1 · scheme command · custom colours ignore the contrast level and the 2025 spec
|
||||
- Severity: must-fix
|
||||
- M3 says: "Custom components support contrast levels simply by using Material color roles… the role's resolved value changes per contrast level automatically" (reference-styles §Color/Contrast levels; site `styles/color/roles`). Aug 2024: on-container roles became "more colourful while remaining accessible"; the 2025 spec that the library asks for (`--spec=2025`) applies those curves to `error`. `define-new-colors`: custom colours should behave like the built-in roles.
|
||||
- Library does: `resources/node/scheme.mjs` (tail) builds `success`/`warning`/`info` with material-color-utilities' `customColor()` (`pe(...)`, `blend:false`): fixed tones 40/100/90/10 in light and 80/20/30/90 in dark, no contrast curve, no spec awareness. So `php artisan material:scheme --contrast=1` raises every M3 role but leaves the three state colours at standard contrast, and their `on-*-container` pairs are tone 10/90 while `on-error-container` (2025) is `#6e0523`-style tone ~25. Visible in `resources/css/tokens/scheme.css:66-77` vs `:62-65`.
|
||||
- Fix: in `bin/scheme.mjs` (source of the bundle), define the three colours as `DynamicColor`s on their own `TonalPalette` exactly as `color_spec_2025.js` defines `error`/`onError`/`errorContainer`/`onErrorContainer` (same `ContrastCurve`s and `ToneDeltaPair`s), evaluated against the same `DynamicScheme` (so `contrastLevel`, `isDark`, `specVersion`, platform all apply). Rebuild `resources/node/scheme.mjs`; regenerate `tokens/scheme.css`, `scheme.json`, the workbench scheme. Add a `SchemeCommandTest` case: contrast 1 changes `success`.
|
||||
- Effort: M
|
||||
- Breaks API? no (role names unchanged; values change)
|
||||
|
||||
### C2 · scheme · only the standard contrast level is generated
|
||||
- Severity: should-fix
|
||||
- M3 says: "Three levels of contrast: standard, medium (3:1 minimum), high (7:1)… contrast settings apply automatically in both light and dark theme" (reference-styles §Color; `styles/color/roles`, "What's new May 2025"). Accessibility principle "Honor individuals — build in customizable features" (reference-foundations §Accessibility).
|
||||
- Library does: `SchemeCommand` (`src/Console/SchemeCommand.php:24`) takes `--contrast` once and writes a single light and dark block; nothing at runtime can raise contrast; `theme.js` has no contrast state; no `@media (prefers-contrast: more)`.
|
||||
- Fix: have `material:scheme` also emit `[data-contrast="medium"]` (contrast 0.5) and `[data-contrast="high"]` (1.0) blocks for light and dark (and per profile), plus `@media (prefers-contrast: more) { :root:not([data-contrast]) {…} }` as the OS default; add `contrast` to `$store.theme` (`resources/js/theme.js`) and to the head script's attribute set (`theme-script.blade.php`, kept across `wire:navigate` like `data-theme`); `<x-theme-toggle mode="picker">` or a new `<x-contrast-picker>` exposes it. Document in SKILL.md §Theme.
|
||||
- Effort: M
|
||||
- Breaks API? no (opt-in)
|
||||
|
||||
### C3 · theme.css · M2-style opacity inks and lines instead of M3 roles
|
||||
- Severity: should-fix
|
||||
- M3 says: "Default typography color is on surface (or on surface variant as a strong alternative)"; dividers use **outline variant**, not a lighter mix; "Always apply color roles, never static hex values"; 0.38 is the **disabled** opacity (reference-foundations §States values; reference-styles §Color rules, §Typography accessibility).
|
||||
- Library does: `resources/css/tokens/theme.css:99-106` defines `--color-meta` = on-surface 60 %, `--color-quiet` = on-surface 38 %, `--color-chrome` = outline-variant 60 %, `--color-divider` = outline-variant 40 % via `color-mix()`. `text-quiet` therefore looks exactly like disabled text (`text-on-surface/38`, used 21× in components), and `border-divider` is lighter than M3's divider. None of these utilities is used by a package component (grep: only the showcase swatches and one `border-divider`/`divide-divider`), so they exist for ReStride's templates.
|
||||
- Fix: keep the names, repoint them to roles: `--color-body` → on-surface-variant (already), `--color-meta` → on-surface-variant, `--color-quiet` → outline, `--color-structure` → outline-variant (already), `--color-chrome` → outline-variant, `--color-divider` → outline-variant. Say in SKILL.md that emphasis is a role, not an opacity, and that 38 % means disabled. If ReStride needs the lighter divider, it overrides the variable in its own `@theme`.
|
||||
- Effort: S
|
||||
- Breaks API? no (names stay; rendered colour changes in ReStride)
|
||||
|
||||
### C4 · theme.css · Tailwind's default radius, shadow, type-size, weight, easing and duration utilities still compile
|
||||
- Severity: should-fix
|
||||
- M3 says: "Do use design tokens instead of hardcoded values" (reference-foundations §Design tokens); the corner scale, elevation levels, typescale and motion tokens are the only values (reference-styles §Shape, §Elevation, §Typography rule "avoid changing type size", §Motion).
|
||||
- Library does: `theme.css:19-25` clears `--color-*` only. `rounded-lg` (Tailwind: 8 px, M3 "large" is 16 px), `rounded-xl` (12 px vs 28 px), `shadow-md`, `text-sm`/`text-lg`, `font-bold`, `leading-*`, `tracking-*`, `ease-in-out`, `duration-300` all still compile with Tailwind's values. The package's own views are clean (only `rounded-full` ×4 and `rounded-none` ×1, both fine), but consuming apps and AI agents are not stopped. `DesignGuard` does not look for them.
|
||||
- Fix: two steps. (a) Now, non-breaking: `DesignGuard` gains a "value outside the M3 scale" check for `rounded-(xs|sm|md|lg|xl|2xl|3xl|4xl)`, `shadow-(2xs|xs|sm|md|lg|xl|2xl)`, `text-(xs|sm|base|lg|xl|\dxl)`, `font-(thin|…|black)`, `leading-*`, `tracking-*`, `ease-(in|out|in-out)`, `duration-\d+`, `blur-*`? (no), each with the M3 utility to use instead. (b) In the next major: `@theme { --radius-*: initial; --shadow-*: initial; --ease-*: initial; --text-*: initial; --font-weight-*: initial; --leading-*: initial; --tracking-*: initial; }` before re-declaring the M3 ones, mirroring what was done for colour (keep `--radius-full`/`rounded-none` behaviour by declaring `--radius-full: 9999px`).
|
||||
- Effort: S (guard) + S (theme, but a major-version change)
|
||||
- Breaks API? (a) no · (b) yes for applications using Tailwind defaults
|
||||
|
||||
### C5 · type.css · tracking follows material-web, not Compose; emphasized styles reuse baseline tracking
|
||||
- Severity: should-fix
|
||||
- M3 says (reference-styles §Typography, `TypeScaleTokens.kt`): Display Large −0.2 sp; Title Medium 0.2 sp; Body Medium 0.2 sp; emphasized: Display Large 0, Title Medium 0.15, Body Large 0.15, Body Medium 0.25 (others unchanged).
|
||||
- Library does: `resources/css/tokens/type.css:21` display-lg tracking −0.015625 rem (−0.25 px, material-web); `:39` title-md 0.009375 rem (0.15 px, M3: 0.2 sp = 0.0125 rem); `:48` body-md 0.015625 rem (0.25 px, M3: 0.2 sp); every `type-emphasized-*` utility (`:101` onward) uses the baseline `-tracking` variable, so emphasized display-lg keeps −0.25 px instead of 0, emphasized body-lg keeps 0.5 px instead of 0.15.
|
||||
- Fix: add `--md-sys-typescale-emphasized-*-tracking` variables with Compose's values and use them in the emphasized utilities; correct the three baseline values to Compose's (sp/16 rem). Sub-pixel, but it is what the Expressive token file says and the header comment cites material-web, which is pre-Expressive.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### C6 · type.css · regular styles do not reset `font-variation-settings`
|
||||
- Severity: nice-to-have
|
||||
- M3 says: emphasized styles are applied deliberately, per element (reference-styles §Typography rules).
|
||||
- Library does: `type-emphasized-*` sets `font-variation-settings: "ROND" 100` (`type.css:107`), an inherited property; `type-*` utilities set none, so `type-body-md` text inside an element carrying `type-emphasized-title-md` (a card slot under an emphasized wrapper) renders fully rounded.
|
||||
- Fix: every regular utility sets `font-variation-settings: normal` (or `"ROND" 0`).
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### C7 · font · variable axes of the subset are unverified here
|
||||
- Severity: nice-to-have (verification)
|
||||
- M3 says: brand/plain typefaces are the product's choice; Roboto is the default (reference-styles §Typography fonts). Google Sans Flex is Google's Expressive typeface (the site's shape page: "M3 shapes and Google Sans Flex share roundness attributes"), so the choice is aligned.
|
||||
- Library does: `font.css` claims weight 400–700 and `ROND` 0–100 are kept. I could not open the woff2 (no fontTools on this machine).
|
||||
- Fix: add a test (or a `bin/` check) that reads the `fvar` table and asserts the `wght` and `ROND` axes exist, so a future re-subset cannot silently drop the axis the emphasized styles depend on.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### C8 · motion.css · spring durations differ from the web equivalents Google publishes
|
||||
- Severity: should-fix (with a counter-argument)
|
||||
- M3 says (`styles/motion/overview/specs`, reference-styles §Motion "Web curve equivalents"): expressive spatial fast/default/slow = 350/500/650 ms, effects = 150/200/300 ms, each with a cubic-bezier; "springs aren't natively supported on the web, so Web should use these approximated curves with matching duration".
|
||||
- Library does: `motion.css:33-52` samples the real springs into `linear()` (more faithful than Google's cubic-bezier) but stops at the settle time: 360/440/600 ms and 150/240/330 ms. Default spatial is 12 % quicker than Google's web number, slow effects 10 % slower.
|
||||
- Fix: either (a) sample each spring over Google's published web duration (the curve reaches 1 earlier and holds, so the bounce timing is unchanged) so the `*-duration` tokens read 350/500/650/150/200/300, or (b) keep the settle times and record why in the header. Also add the six published cubic-bezier curves as `--md-sys-motion-spring-*-bezier` fallbacks for `@supports not (animation-timing-function: linear(0,1))` (Safari < 17.2 is below the floor, so this is documentation more than need). (a) is "what Google says".
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### C9 · motion.css · only the Expressive motion scheme exists
|
||||
- Severity: nice-to-have
|
||||
- M3 says: two preset schemes, Expressive (default) and Standard ("minimal bounce, for utilitarian products"), swappable product-wide (reference-styles §Motion principles; `StandardMotionTokens.kt`: fast spatial 0.9/1400, default 0.9/700, slow 0.9/300; effects identical).
|
||||
- Library does: six springs, Expressive only.
|
||||
- Fix: sample the three Standard spatial springs too and let `[data-motion="standard"]` on `<html>` (or a config flag rendered by the theme script) swap the `--md-sys-motion-spatial-*` variables. Effects are the same in both schemes.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### C10 · state.css · no `dragged` state, no disabled tokens, no shared touch target
|
||||
- Severity: should-fix
|
||||
- M3 says: state layer opacities hover 0.08, focus 0.10, pressed 0.10, **dragged 0.16**; disabled content 0.38; "the size of state layers is 40dp while the interactive target size is 48dp"; target spacing 8 dp (reference-foundations §States, §Accessibility values).
|
||||
- Library does: `state.css:26-40` covers hover/focus/active; nothing for dragged (cards, chips, list items and the slider are the components M3 gives a dragged state); no `--md-sys-state-*` custom properties, so components hard-code `/38`, `/12`, `/10` (21, 10 and 4 occurrences); the 48 px target is built ad hoc in `button.blade.php:167` with an `after:` pseudo-element and nowhere shared.
|
||||
- Fix: in `state.css` declare `--md-sys-state-hover-opacity: 0.08`, `focus 0.10`, `pressed 0.10`, `dragged 0.16`, `disabled-content 0.38`, `disabled-container 0.12` and use them in `state-layer` (add `&[data-dragged]::before { opacity: var(--md-sys-state-dragged-opacity) }`); add a `touch-target` utility (the `after:` pseudo-element from the button, `min 48×48`) so small icon buttons, chips, checkboxes, radios and switch share one implementation.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### C11 · state.css · focus ring spec has no source on the M3 site
|
||||
- Severity: nice-to-have (documentation)
|
||||
- M3 says: only "a ring-like keyboard focus indicator" (reference-foundations §States; the site defines no thickness/offset/colour — see its "Cross-cutting gaps" 1).
|
||||
- Library does: `focus-ring` = 3 px `secondary`, offset 2 px (`state.css:47-53`), which is material-web's `md-focus-ring` default (3 px, outward offset 2 px, secondary).
|
||||
- Fix: none to the code; cite material-web in the header so nobody "corrects" it to the site's silence.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### C12 · core · no tokens or variants for M3's breakpoints
|
||||
- Severity: should-fix
|
||||
- M3 says: five breakpoints — compact < 600, medium 600–839, expanded 840–1199, large 1200–1599, extra-large ≥ 1600 dp — with the navigation component, pane count and dialog/menu choice per breakpoint (reference-foundations §Layout breakpoints tables).
|
||||
- Library does: nothing in the tokens; components switch on Tailwind's `sm` 640 / `md` 768 / `lg` 1024 / `xl` 1280 (the navigation audit maps each use). An application cannot write "at the medium breakpoint" at all.
|
||||
- Fix: `theme.css` adds `--breakpoint-medium: 600px; --breakpoint-expanded: 840px; --breakpoint-large: 1200px; --breakpoint-extra-large: 1600px` (Tailwind 4 turns these into `medium:`, `expanded:`, `large:`, `extra-large:` variants; `max-medium:` etc. come for free), keeps `sm/md/lg/xl` for now, and the components migrate to the M3 variants in their own findings. Add pane tokens as custom properties: `--md-sys-layout-pane-fixed: 360px` / `412px`, `--md-sys-layout-side-sheet-max: 400px`, margins 16 px compact / 24 px otherwise (older M3 layout page values; the current site gives no numeric margin table — mark as such).
|
||||
- Effort: S (tokens) — the component migration is counted in the other audits
|
||||
- Breaks API? no
|
||||
|
||||
### C13 · DesignGuard · no check for values outside the M3 scale, hex colours, or `white`/`black`
|
||||
- Severity: should-fix
|
||||
- M3 says: "Always apply color roles, never static hex values or raw tonal-palette values" (reference-styles §Color rules).
|
||||
- Library does: `src/Testing/DesignGuard.php:26-33` flags the Tailwind palette and daisyUI colours; it does not flag arbitrary values (`bg-[#1d7afc]`, `text-[rgb(…)]`), `bg-white`/`text-black` (re-added in `theme.css:21-22` and not M3 roles; the M3 white is `surface-container-lowest` in light), or any of the non-token utilities in C4.
|
||||
- Fix: add patterns for `(bg|text|border|…)-\[#`, `-\[rgb`, `-\[hsl`, `-\[oklch`, and an opt-in `forbidAbsolutes()` for `white`/`black`; add the C4 list with a hint per match ("`rounded-lg` → `rounded-corner-lg`").
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### C14 · theme-toggle · the picker is drawn as M3's deprecated segmented button
|
||||
- Severity: should-fix
|
||||
- M3 says: segmented buttons are "M3 only", replaced in M3 Expressive by the connected button group (reference-components-a §Segmented buttons / §Button groups; the plan doc lists segmented button among the six deprecated components).
|
||||
- Library does: `resources/views/components/theme-toggle.blade.php:27-49` (`mode="picker"`) draws an outlined 40 px group with `border-outline` and `aria-checked` fills — the segmented-button anatomy — while `<x-group>` (the connected button group) already exists.
|
||||
- Fix: render the picker with `<x-livewire-material::group>` (three `wire:model`-less radio options bound to `$store.theme`), or reuse its classes; keep `role="radiogroup"` and the arrow-key behaviour.
|
||||
- Effort: S
|
||||
- Breaks API? no (`data-theme-option` hooks can stay)
|
||||
|
||||
### C15 · scheme · harmonisation of the custom colours is not offered
|
||||
- Severity: nice-to-have
|
||||
- M3 says: "you can choose to harmonize your static colors to the scheme's primary color… while retaining the semantic meaning"; "colors can stay completely static and forgo harmonization if their values are tied to literal sources such as brand colors" (`styles/color/advanced/define-new-colors`).
|
||||
- Library does: `customColor(..., blend:false)` always (scheme.mjs tail); no option.
|
||||
- Fix: `--harmonize` flag on `material:scheme` (and `harmonize` per profile) passed as `blend:true`; default off, as today. Do together with C1.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### C16 · icons · one weight/grade/optical size for every context
|
||||
- Severity: nice-to-have
|
||||
- M3 says: grade −25 "for a light icon on dark background"; optical size 20 for 20 dp icons in dense/desktop layouts; 40/48 for display pairing (reference-styles §Icons axes table).
|
||||
- Library does: Material Symbols Rounded 400/0/24 only (`bin/fetch-symbols`, `icon.blade.php` header); buttons `xs`/`sm` draw 20 px icons from the 24-opsz outlines (`button.blade.php:147-149`), so strokes are ~17 % thinner than a true opsz-20 glyph; dark theme keeps grade 0.
|
||||
- Fix: none that is cheap — each extra cut is another 4,135 SVGs (~2.6 MB). Record the trade-off in the icon header and SKILL.md; if ever needed, ship `opsz20` for the outlined set only and let `<x-icon size="20">` pick it.
|
||||
- Effort: L
|
||||
- Breaks API? no
|
||||
|
||||
### C17 · scheme · `background`/`on-background` are emitted, `surface-tint`/`surface-variant` are not
|
||||
- Severity: nice-to-have (informational, aligned)
|
||||
- M3 says: background/on-background are legacy roles with the same values as surface/on-surface; surface-variant was superseded by the surface-container family (Feb 2023); surface tint is deprecated (reference-styles §Color roles table).
|
||||
- Library does: emits and maps `background`/`on-background` (`scheme.css:16-17`, `theme.css:29-30`), omits the two deprecated ones. Correct. Consider marking `bg-background` as "same as `bg-surface`; prefer surface" in SKILL.md so agents do not treat them as two surfaces.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### C18 · scheme · the package default is the 2025 rendering of the baseline seed, not the site's baseline table
|
||||
- Severity: nice-to-have (informational)
|
||||
- M3 says: the baseline palette table (`PaletteTokens.kt`, site `styles/color/static/baseline`) is the 2021 spec: primary 40 = `#6750A4`, on-primary = white.
|
||||
- Library does: `tokens/scheme.css` is `#6750a4` tonal-spot through the 2025 spec (`primary #655789`, `on-primary #fdf7ff`). Both are Google's; the header says so. Nothing to fix; the showcase colour page could say "spec 2025" so a reader comparing with the site's swatches is not puzzled.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### C19 · elevation · no expression of "which component rests at which level"
|
||||
- Severity: nice-to-have (documentation)
|
||||
- M3 says: level 3 = dialogs, FAB, pickers, search; level 2 = menus, scrolled app bar, navigation bar, rich tooltip, toolbar; level 1 = elevated button/card/chip, modal sheets; level 0 = everything else; +1 level on hover (reference-styles §Elevation tokens table); scrim 32 %.
|
||||
- Library does: shadow tokens only (`elevation.css`); the showcase shows five boxes with no mapping; components pick levels individually (correctly where I looked: menu 2, FAB 3, elevated button 1→2 on hover, `bg-scrim/32` ×4).
|
||||
- Fix: put the table in the new guideline/skill and in the showcase elevation section, with M3's rule "tonal separation first, shadows for floating things and interaction".
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### C20 · showcase · the foundation pages show tokens but not M3's rules for them
|
||||
- Severity: nice-to-have
|
||||
- M3 says: (as in C19, plus) surface container hierarchy by emphasis; outline vs outline-variant; type roles and when emphasized is used; spatial vs effects; corner scale per component class; optical roundness (outer − padding = inner).
|
||||
- Library does: `showcase/sections/{colour,type,shape,elevation,motion,icons}.blade.php` list every utility with one sentence each; the colour page groups "Ink and lines" as if they were roles (see C3).
|
||||
- Fix: one short "when to use" paragraph per section, drawn from the same guideline text (C24), so the showcase and the AI docs cannot drift.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### C21 · theme script · `data-theme` only, no `prefers-color-scheme` in CSS — aligned
|
||||
- Severity: nice-to-have (no change)
|
||||
- M3 says: light/dark is a user preference; honour the OS and the person's choice (reference-foundations §Accessibility principles; §Color "built-in dark theme").
|
||||
- Library does: resolves `system` before first paint, follows OS changes while `system`, keeps the attributes across `wire:navigate`, declares `color-scheme` per block. Correct.
|
||||
- Fix: none. (C2 adds contrast to the same mechanism.)
|
||||
|
||||
### C22 · reduced motion · stricter than the site, aligned with its intent
|
||||
- Severity: nice-to-have (no change)
|
||||
- M3 says: no global reduced-motion rule on `styles/motion`; component pages ask for parallax/expansion to be removed under reduced motion (reference-styles §Motion accessibility).
|
||||
- Library does: zeroes every duration token (`motion.css:66-77`); carousel, progress, tabs and figure check the media query themselves. Correct; the header's "anything that animates without them is a bug" is a good rule to repeat in the guideline.
|
||||
|
||||
### C23 · icons · accessibility of `<x-icon>` — aligned
|
||||
- Severity: nice-to-have (no change)
|
||||
- M3 says: decorative icons hidden; meaningful icons labelled; don't put the role in the label (reference-foundations §Accessibility labelling).
|
||||
- Library does: `aria-hidden` by default, `role="img"` + `aria-label` when `label` is given, `focusable="false"` (`icon.blade.php:27-33`). Correct.
|
||||
|
||||
### C24 · AI docs · the M3 core concepts are not written down anywhere an agent reads
|
||||
- Severity: should-fix (this is the user's explicit request)
|
||||
- M3 says: see the list under "Concepts missing from the AI docs" below.
|
||||
- Library does: `resources/boost/guidelines/core.blade.php` (6 bullets: what the package is, no maryUI, theme script, showcase, error pages/mail) and SKILL.md §Tokens/§Theme/§Conventions list the utilities and three rules (elevation is for floating things; pair easing with duration; `dark:` follows `data-theme`). Nothing says what a role, a container level, a corner size, a type role or a spring is *for*, nor any layout/accessibility rule.
|
||||
- Fix: a second always-on guideline `resources/boost/guidelines/material-3.blade.php` (short: the rules that change what an agent writes) and a fuller `resources/boost/skills/material-3-design/SKILL.md` (the concept reference with the tables), both generated from one source or tested against each other; Boost 2.8 loads every file under `resources/boost/guidelines` and every skill directory (verified in `laravel/boost/src/Install/GuidelineComposer.php:320-351`, `SkillComposer.php:127-141`). Add the drift test.
|
||||
- Effort: M
|
||||
- Breaks API? no
|
||||
|
||||
## Deliberate deviations
|
||||
|
||||
| Where | Deviation | Reason given | Holds up? |
|
||||
| --- | --- | --- | --- |
|
||||
| `shape.css` | `corner-full` = 9999px instead of material-web's `50cqmin` | container units need a declared container | Yes. Compose's `CircleShape` is 50 % of the smaller dimension = a stadium; 9999px gives the same on rectangles, `50%` would not. |
|
||||
| `elevation.css` | `color-mix()` instead of `hsl(from …)` | browser floor | Yes; same result. |
|
||||
| `motion.css` | springs sampled to `linear()` rather than Google's cubic-bezier approximations | closer to the physics | Yes for the curve; the durations differ from Google's published web numbers (C8). |
|
||||
| `motion.css` | reduced motion zeroes durations | one place instead of per component | Stricter than the site; consistent with "Honor individuals". |
|
||||
| `theme.css` | colour blocks are `@theme inline` | nested `data-theme` sections | Sound; verified by a browser test per the plan doc. |
|
||||
| `theme.css` | white/black re-added | absolutes | M3 has no such roles; white is `surface-container-lowest` (light). Harmless, but the guard should at least be able to flag them (C13). |
|
||||
| `theme.css` | `--color-meta/quiet/divider/chrome` opacity mixes | ReStride's templates | Does **not** hold against M3's text (C3): M3 dropped opacity-based emphasis with M3; 38 % is disabled. |
|
||||
| `type.css` | Google Sans Flex for brand and plain, `ROND` 100 on emphasized | Expressive's typeface | Holds: the site names Google Sans Flex on the shape page and leaves the typeface to the product; `ROND` is that font's own axis, not an M3 token. |
|
||||
| scheme | `surface-tint`/`surface-variant` omitted | superseded/deprecated | Holds (reference-styles §Color roles). |
|
||||
| scheme | success/warning/info via `customColor`, unharmonised | "harmonisation off" | Off is allowed; the 2021 recipe and contrast-blindness are not (C1, C15). |
|
||||
| `icon` | one Symbols cut, 400/0/24, SVG not font | any name works, no runtime download | Holds for weight; grade/optical size are lost (C16). |
|
||||
| `theme-script` | OS never read by CSS | the script resolves `system` | Holds. |
|
||||
|
||||
## Aligned (keep as is)
|
||||
|
||||
- All 2025-spec colour roles generated and mapped, `*-dim` and fixed roles included; `scheme.json` and `Scheme.php` fill missing roles from the default; every pair (`x` / `on-x`) comes from Google's algorithm.
|
||||
- Corner scale: none 0, xs 4, sm 8, md 12, lg 16, lg-increased 20, xl 28, xl-increased 32, xxl 48, full — exactly M3 Expressive's ten steps, same names.
|
||||
- Elevation: five levels with material-web's shadow geometry; tonal separation preferred; scrim at 32 % where used.
|
||||
- Motion: spatial 0.6/800, 0.8/380, 0.8/200; effects 1.0/3800, 1.0/1600, 1.0/800 — the Compose Expressive constants; legacy easing tokens present; reduced motion handled centrally and in the four scripted components.
|
||||
- Typescale: all 15 sizes, line heights and weights match `TypeScaleTokens.kt`, emphasized set present (weights right).
|
||||
- State layer: 8/10/10 %, content colour, hover only on `(hover: hover)`, disabled removes it; focus ring only on `:focus-visible`.
|
||||
- Theme: `data-theme` resolved before paint, OS followed while `system`, choice persisted, legacy keys adopted, attributes survive `wire:navigate`, `color-scheme` declared, `dark:` variant on `data-theme`.
|
||||
- Shapes: the 35 Expressive shapes ported from androidx, all fill the same box, decorative and hidden from AT.
|
||||
- Symbols: Google's own 24 px Rounded files (not the 48-opsz npm cut), outlined and filled, `currentColor`, hidden unless labelled.
|
||||
- Safe areas: `--material-safe-*` = M3's safety region.
|
||||
- DesignGuard already enforces "roles, not palette" and "no maryUI/daisyUI".
|
||||
|
||||
## Missing (M3 things the core does not offer at all)
|
||||
|
||||
- Contrast levels medium/high (C2).
|
||||
- Breakpoint tokens/variants and pane/margin tokens (C12).
|
||||
- Dragged state, disabled tokens, a shared touch target (C10).
|
||||
- Standard motion scheme (C9).
|
||||
- Harmonisation option for custom colours (C15).
|
||||
- An M3 concept guideline for agents (C24).
|
||||
|
||||
## Breakpoint map (core only)
|
||||
|
||||
| Item | Library | M3 breakpoint | Gap |
|
||||
| --- | --- | --- | --- |
|
||||
| tokens | none; Tailwind `sm` 640 / `md` 768 / `lg` 1024 / `xl` 1280 / `2xl` 1536 | compact <600 / medium 600 / expanded 840 / large 1200 / extra-large 1600 | no M3 breakpoint is expressible; nearest Tailwind values are 40–88 px off (see navigation audit for each component) |
|
||||
|
||||
## Concepts missing from the AI docs (input for the "Material 3 core concept" guideline)
|
||||
|
||||
Each line: what M3 says → whether the package docs say it today.
|
||||
|
||||
Colour
|
||||
- Roles are "paint-by-number" slots; a colour is always a role, never a hex or palette tone → docs say "every colour class names an M3 role" (yes) but not why or the hex rule.
|
||||
- Pair `x` with `on-x` only; a container is a fill, "on" is for text/icons on it; don't mix pairs (primary + secondary-container + on-surface) → no.
|
||||
- Primary = high-emphasis actions; secondary = less prominent, tonal fills; tertiary = complementary accent; error = static semantic → no.
|
||||
- Surface = page; surface-container-lowest…highest = emphasis hierarchy, not elevation; navigation on surface-container; same mapping for a region across breakpoints → no.
|
||||
- `outline` for boundaries that must read (text fields), `outline-variant` for dividers and cards; never `outline` on dividers → no.
|
||||
- Fixed/dim roles: "if you aren't sure, you probably shouldn't"; never where contrast matters → no.
|
||||
- Inverse roles are for the snackbar/inverse surfaces only → no.
|
||||
- success/warning/info are the package's custom roles, built like error; semantic colours are never dynamic-coloured away → no.
|
||||
- Low emphasis is `on-surface-variant`, 38 % means disabled; links are primary **and** underlined → no (the opposite: `text-quiet` is documented as decoration).
|
||||
- Contrast targets 4.5:1 small text, 3:1 large text/graphics, 3:1 clustered elements; disabled exempt → no.
|
||||
- Three contrast levels exist → no.
|
||||
|
||||
Elevation
|
||||
- Tonal separation first; shadows for floating elements and interaction only; the level table per component; +1 on hover; scrim 32 % → partly ("for what floats over content").
|
||||
|
||||
Shape
|
||||
- Which corner for what: full for buttons/chips/FAB-menu, xs 4 for text fields/snackbar, sm 8 for chips, md 12 for cards, lg 16 for FAB, xl 28 for dialogs/sheets/menus in Expressive, xxl 48 for large containers → no.
|
||||
- Optical roundness: inner radius = outer radius − padding → no.
|
||||
- Shapes are decoration, never semantic; use sparingly; press morph is the interaction cue → no.
|
||||
|
||||
Typography
|
||||
- Role purposes: display (big, short), headline (short, high emphasis), title (medium, short), body (paragraphs), label (inside components; buttons use label-lg) → no.
|
||||
- Emphasized styles are opt-in, for selection, primary actions, headlines, badges — not decoration → no.
|
||||
- 40–60 characters per line; tabular numbers where values change; don't change sizes when customising → no (only "never assemble text-* by hand").
|
||||
|
||||
Motion
|
||||
- Spatial springs for position/size/shape (they overshoot), effects for colour/opacity (never overshoot); fast for small elements, default for most, slow for large → partly (the pairing rule, one sentence).
|
||||
- Enter = decelerate, permanent exit = accelerate, temporary exit = emphasized; exits shorter than enters → no.
|
||||
- Reduced motion: everything through the tokens goes instant; parallax/expansion removed → no.
|
||||
|
||||
States
|
||||
- Six states; two visual indicators per state; hover 8 % / focus 10 % / press 10 % / drag 16 %; disabled 38 % content, 12 % container, no hover; state layer takes the content colour → no (utilities only).
|
||||
- 48 × 48 target, 8 dp between targets, never below 48 by default, density is opt-in → no.
|
||||
|
||||
Layout
|
||||
- Five breakpoints and what changes at each (navigation bar → collapsed rail → expanded rail; 1 → 2 → 3 panes; bottom sheet → menu; full-screen dialog → basic dialog) → no.
|
||||
- Scaffold: bars, rails, panes; safety regions; pane widths 360/412, side sheet ≤ 400 → partly (safe areas documented).
|
||||
- RTL: leading/trailing, mirror directional icons, use logical utilities (`ps-`, `ms-`, `start-`) → no.
|
||||
|
||||
Accessibility
|
||||
- Landmarks (one main/banner/contentinfo; label repeated navs; no role in the label); headings in order, one H1; dialog focus in/out; labels for icon-only controls; decorative images hidden; keyboard shortcuts need a modifier → no.
|
||||
|
||||
Icons
|
||||
- Filled = active/selected; keep one weight per group; 24 default, 20 in dense UI; label complex icons below 20; 48 target → partly (`filled` documented).
|
||||
@@ -0,0 +1,692 @@
|
||||
# Audit: inputs
|
||||
|
||||
Scope: text fields and their chrome (`form`, `field`, `input`, `password`, `textarea`, `select`, `file`),
|
||||
selection controls (`checkbox`, `radio`, `toggle`), `slider`, `chip` / `chip-set` / `choices`,
|
||||
`datepicker`, `timepicker`, `search`, and the data pieces (`table`, `sort-header`, pagination views).
|
||||
|
||||
Sources cited below: `docs/reference/m3/reference-components-b.md` (RB) and `docs/reference/m3/reference-foundations.md`
|
||||
(RF) — both built from m3.material.io; raw page dumps `docs/reference/m3/raw/components_*_specs.md`; androidx
|
||||
token files in `docs/reference/m3/tokens/*.kt`. `reference-styles.md` did not exist while this audit ran, so
|
||||
shape/type/elevation numbers come from the raw `styles_*` dumps and the token files.
|
||||
|
||||
## Summary
|
||||
|
||||
This is an unusually faithful M3 implementation. The text-field chrome, the three selection controls, the
|
||||
chip family and both pickers reproduce the androidx token numbers almost exactly — the switch matches
|
||||
`SwitchTokens` in every dimension, the date picker matches `DatePickerModalTokens` (360 × 120 header,
|
||||
40 px day in a 48 px cell, today's 1 px primary outline), and the time picker matches the site's own
|
||||
spec table for the dial, the selector handle and the time-selector boxes. The biggest problems are
|
||||
accessibility rather than geometry: `<x-search>` announces nothing when results appear and puts
|
||||
`aria-expanded` on a plain `<input type="search">` with orphan `role="listitem"` results; a disabled text
|
||||
field still lights its outline on hover because the hover declaration sits on a descendant of the element
|
||||
carrying the disabled value; and a label-less checkbox, radio or switch has an 18–52 px hit target instead
|
||||
of 48 px. Measurable deviations cluster in the slider (the M-size handle is 44 px where M3 says 52 px, the
|
||||
value indicator is a 32 × 36 pill where M3 says 44 × 48, and the stop/tick/icon colours are inverted from
|
||||
the site's roles) and in the search view (it draws the deprecated *divided* style's divider and uses
|
||||
surface-container-high where the full-screen layout should be surface-container-low). Missing entirely:
|
||||
the slider's Expressive vertical orientation, the full-screen range date picker, and any width bound on
|
||||
the search bar or a text field at medium/expanded widths. Several of the library's own androidx-token
|
||||
citations in header comments name numbers that the M3 site's spec tables contradict — all listed in
|
||||
**Token-citation disagreements** below.
|
||||
|
||||
## Findings
|
||||
|
||||
### IN-01 · search · results appear with no announcement, and the input's ARIA is invalid
|
||||
- Severity: must-fix
|
||||
- M3 says: "Screen reader must announce when suggestions/results appear (autosuggest change)" and
|
||||
"Suggestions/results use the list component and are announced as a list by screen readers (follow list
|
||||
accessibility guidelines for item labels)" — RB §Search/Accessibility (m3.material.io/components/search/accessibility).
|
||||
- Library does: `resources/views/components/search.blade.php:47-64` puts `aria-controls` and
|
||||
`aria-expanded` on a bare `<input type="search">` with no `role="combobox"`. ARIA 1.2 does not list
|
||||
`aria-expanded` as supported on `textbox`, so the state is dropped by conforming AT. The view
|
||||
(`search.blade.php:75-90`) has no role and no live region, and `[data-search-results]`
|
||||
(`search.blade.php:86`) is a plain `<div>` whose children are `<x-list-item>`s rendered as
|
||||
`role="listitem"` (`resources/views/components/list-item.blade.php:41`) — an orphan role with no
|
||||
`role="list"` owner. `grep -n 'aria-live' resources/views/components/search.blade.php` returns nothing.
|
||||
`<x-choices>` already does this correctly (`choices.blade.php:124-130,154-157`: `role="combobox"`,
|
||||
`aria-autocomplete="list"`, `role="listbox"`/`role="option"`, `aria-activedescendant`).
|
||||
- Fix: in `search.blade.php`, add `role="list"` to the `[data-search-results]` div (or wrap the slot in
|
||||
`<x-list>`), and either (a) mirror `<x-choices>`: `role="combobox" aria-autocomplete="list"` on the
|
||||
input and roving `aria-activedescendant` over the results, or (b) keep the plain search field, drop
|
||||
`aria-expanded`, and add a visually hidden `<span aria-live="polite">` that `search.js` fills with the
|
||||
result count whenever `[data-search-results]` mutates (the `MutationObserver` plumbing already exists in
|
||||
`resources/js/chips.js:99-117` as a pattern).
|
||||
- Effort: M
|
||||
- Breaks API? no
|
||||
|
||||
### IN-02 · field · a disabled text field still lights its outline on hover
|
||||
- Severity: must-fix
|
||||
- M3 says: "**Disabled**: communicated via color change + reduced elevation; not focusable, draggable, or
|
||||
pressable; **no hover/other state layer**" — RF §States (foundations/interaction/states/applying-states).
|
||||
- Library does: `resources/css/components/field.css:390-392` declares
|
||||
`.field-box:hover { --field-edge: var(--md-sys-color-on-surface); }` **on `.field-box`**, while the
|
||||
disabled value at `field.css:423-425` is declared on `.field` and only *inherits* down. A direct
|
||||
declaration beats an inherited one regardless of selector specificity, so hovering a disabled outlined
|
||||
field paints a full-opacity `on-surface` outline instead of `on-surface/12`. The rule is also outside
|
||||
`@media (hover: hover)`, unlike the filled variant's background at `field.css:458-462`, so the state
|
||||
sticks after a tap on touch. (The filled background *is* correctly suppressed — `field.css:514-519`
|
||||
wins on specificity there; only `--field-edge` breaks.)
|
||||
- Fix: `resources/css/components/field.css:390` → wrap in `@media (hover: hover)` and scope to
|
||||
`.field:not(:has(.field-control:disabled)) .field-box:hover`.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### IN-03 · checkbox / radio / toggle · a control without a label has an 18–52 px hit target
|
||||
- Severity: must-fix
|
||||
- M3 says: checkbox "Target size 48dp", radio "Target size 48dp", switch "Target | Size | 48dp"
|
||||
(RB §Checkbox/Specs, §Radio Button/Specs, §Switch/Specs); "The size of state layers is 40dp while the
|
||||
interactive target size is 48dp" (RF §States); and for all three, "Don't apply density by default —
|
||||
this drops targets below the 48x48 CSS px minimum" (RB §Checkbox/Accessibility, §Radio/Accessibility,
|
||||
§Switch/Accessibility).
|
||||
- Library does: in `resources/views/components/checkbox.blade.php:27-55` the `<label>` wraps only
|
||||
`<span data-checkbox>` when `label` and `hint` are both blank, and `[data-checkbox]` is `1.125rem`
|
||||
square (`resources/css/components/selection.css:105-108`). The 40 px state layer is a `::before` with
|
||||
`pointer-events: none` (`selection.css:51-59`), so it adds nothing to the target. Result: an 18 × 18
|
||||
target. `[data-radio]` is 20 × 20 (`selection.css:169-172`) and `[data-switch]` is 52 × 32
|
||||
(`selection.css:225-231`) — 32 px tall, also under 48. A label-less checkbox is a real use (the
|
||||
select-all in a table header, a row checkbox), and `<x-toggle>` documents `aria-label` as the label-less
|
||||
path (`toggle.blade.php:5`), so the case is expected.
|
||||
- Fix: in `resources/css/components/selection.css`, add a pointer-catching pseudo-element rather than
|
||||
growing the visual box — e.g.
|
||||
`[data-checkbox]::after { content:""; position:absolute; inset:-0.9375rem; }` (18 → 48),
|
||||
`[data-radio]::after { inset:-0.875rem; }` (20 → 48), and
|
||||
`[data-switch]::after { content:""; position:absolute; inset:-0.5rem 0; }` (32 → 48). Keep them behind
|
||||
the input so clicks still reach it, or put `min-block-size:3rem` on the `[data-selection]` row.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### IN-04 · slider · the M-size handle is 44 px where M3 Expressive specifies 52 dp
|
||||
- Severity: should-fix
|
||||
- M3 says: sliders/specs, "Handle height | 44dp | 44dp | **52dp** | 68dp | 108dp" for XS/S/M/L/XL
|
||||
(RB §Sliders/Specs, verbatim site table).
|
||||
- Library does: `resources/views/components/slider.blade.php:247-250` maps
|
||||
`'md' => 'h-11 group-data-focused/thumb:h-9.5'` — 44 px, the same as XS and S. The header comment at
|
||||
`slider.blade.php:44` states the intent as "handles of 44, 44, 44, 68 and 108px", citing MDC-Android's
|
||||
`md.comp.slider.*` tokens, which disagrees with the site table for M.
|
||||
- Fix: `slider.blade.php:248` → `'md' => 'h-13 group-data-focused/thumb:h-11.5'` (52 px, 46 px focused,
|
||||
keeping the 6 px focus-ring decrease the comment describes), and correct the header comment.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### IN-05 · slider · the value indicator is a 32 × 36 pill where M3 Expressive specifies 44 × 48
|
||||
- Severity: should-fix
|
||||
- M3 says: sliders/specs, "Label container height | 44dp (all sizes)" and "Label container width | 48dp
|
||||
(all sizes)" (RB §Sliders/Specs). Container `Inverse surface`, label `Inverse on surface`, font
|
||||
`Label large` (`SliderTokens.ValueIndicatorContainerColor / ValueIndicatorLabelTextColor /
|
||||
ValueIndicatorLabelTextFont`).
|
||||
- Library does: `resources/views/components/slider.blade.php:346` — `h-8 min-w-9 ... px-2.5` = 32 px tall,
|
||||
36 px minimum width. The colours and font are right (`bg-inverse-surface text-inverse-on-surface
|
||||
type-label-lg`). The header at `slider.blade.php:39-41` explains the 32 px as Flutter's
|
||||
`RoundedRectSliderValueIndicatorShape`, i.e. a non-Expressive source.
|
||||
- Fix: `slider.blade.php:346` → `h-11 min-w-12`; then re-derive the `$labelBottom` offsets at
|
||||
`slider.blade.php:252` (they are hard-coded against a 32 px pill: `calc(50%+1.625rem)` = handle half +
|
||||
4 px gap + 32 px). Note M3 also gives `ValueIndicatorActiveBottomSpace = 12dp` in `SliderTokens.kt`
|
||||
where the library uses 4 px — worth reconciling in the same change.
|
||||
- Effort: M
|
||||
- Breaks API? no
|
||||
|
||||
### IN-06 · slider · stop indicators and the inset icon use inverted colour roles
|
||||
- Severity: should-fix
|
||||
- M3 says: the 9 slider colour roles, in anatomy order, are "1. Inverse surface 2. Inverse on surface
|
||||
3. Primary 4. **On primary** 5. Primary 6. Secondary container 7. **On secondary container**
|
||||
8. On secondary container 9. On primary" — `docs/reference/m3/raw/components_sliders_specs.md:150-160`
|
||||
(verbatim). That is: a stop/icon **on the active track** is `On primary`; a stop/icon **on the inactive
|
||||
track** is `On secondary container`.
|
||||
- Library does: `resources/views/components/slider.blade.php:227-238` inverts both —
|
||||
`$tickInk['primary'] = 'bg-primary data-active:bg-secondary-container'` (so an inactive-track tick is
|
||||
`primary` and an active-track tick is `secondary-container`), and `$iconInk['primary'] =
|
||||
'text-primary data-active:text-secondary-container'`. The end stop indicators at
|
||||
`slider.blade.php:311` use `$activeInk` (`bg-primary`) even though they sit on the inactive track.
|
||||
Contested reading: `SliderTokens.kt` has `StopIndicatorColor = SecondaryContainer` and
|
||||
`StopIndicatorColorSelected = SecondaryContainer`, which would make the active-track stop
|
||||
secondary-container (the library's choice) and the inactive-track stop invisible. The site's own colour
|
||||
list is the more coherent of the two and is the newer Expressive text.
|
||||
- Fix: in `slider.blade.php:227-238`, swap to
|
||||
`'primary' => 'bg-on-secondary-container data-active:bg-on-primary'` (and the parallel
|
||||
`text-on-…` for `$iconInk`), and give `$stops` at `slider.blade.php:311` `bg-on-secondary-container`.
|
||||
Repeat for the `secondary`/`tertiary`/`error`/`success`/`warning`/`info` rows.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### IN-07 · search · the view draws the *divided* style's divider, which M3 Expressive deprecates
|
||||
- Severity: should-fix
|
||||
- M3 says: "| Style | Contained | -- | Available | | | Divided | Available | **Not recommended. Use
|
||||
contained.** |" and "In the divided (baseline) style, **a divider separates the search bar and
|
||||
results**" — `docs/reference/m3/raw/components_search_specs.md:106-107,134`. The divided style "Doesn't have
|
||||
the latest visual style, motion, or flexibility" (ibid. :46).
|
||||
- Library does: `resources/css/components/search.css:160` —
|
||||
`[data-search-results] { border-top: 1px solid var(--md-sys-color-outline); }`. The file's own header
|
||||
(`search.css:6-9`) describes the contained style. Second reading: Compose's `SearchBar` does draw a
|
||||
`HorizontalDivider` between the input and the content, so this may be tracking the Compose
|
||||
implementation rather than the site; the site's contained-style images show no divider.
|
||||
- Fix: `resources/css/components/search.css:160` → drop the `border-top` (keep `padding-block: 0.5rem`).
|
||||
If a separation cue is wanted, use `outline-variant`, not the darker `outline` role.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### IN-08 · search · the full-screen layout uses the docked layout's container colour
|
||||
- Severity: should-fix
|
||||
- M3 says: "Full-screen search color roles used in light and dark themes: 1. **Surface container low**
|
||||
2. On surface variant 3. On surface variant 4. Surface container high 5. On surface variant 6. On
|
||||
surface" vs "Docked search color roles: 1. **Surface container high** …" —
|
||||
`docs/reference/m3/raw/components_search_specs.md:196-232`. RB §Search/Behaviour also warns: "Never use
|
||||
Surface container high on a Surface container background (contrast too low, blends in) — keep container
|
||||
roles at least one step apart."
|
||||
- Library does: `resources/css/components/search.css:140` sets
|
||||
`[data-search-view] { background-color: var(--md-sys-color-surface-container-high); }` for both
|
||||
presentations; `search.css:174-181` changes only geometry for `[data-full-screen]`.
|
||||
- Fix: add
|
||||
`[data-search][data-full-screen] [data-search-view] { background-color: var(--md-sys-color-surface-container-low); }`
|
||||
to `resources/css/components/search.css` around line 174.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### IN-09 · search · the docked view has no scrim
|
||||
- Severity: should-fix
|
||||
- M3 says: "**Docked** (opens a results list below the bar **with a scrim over the rest of the content**
|
||||
— best for medium/expanded windows)" — RB §Search/Variants and configurations.
|
||||
- Library does: `resources/views/components/search.blade.php:27-37` raises the root to `z-index: 50`
|
||||
when open (`search.css:24-26`) and closes on `pointerdown.outside`, but paints nothing over the page.
|
||||
The full-screen presentation covers the page instead (`search.css:174-181`), which is correct for
|
||||
compact.
|
||||
- Fix: in `search.blade.php`, add a `<div data-search-scrim x-show="open && ! fullScreen">` before
|
||||
`[data-search-bar]` and style it in `search.css` as
|
||||
`position: fixed; inset: 0; z-index: -1; background-color: color-mix(in srgb, var(--md-sys-color-scrim) 32%, transparent);`
|
||||
— the same 32 % the datepicker backdrop uses (`datepicker.css:111-113`).
|
||||
- Effort: M
|
||||
- Breaks API? no
|
||||
|
||||
### IN-10 · timepicker · the 24-hour vertical time selector stays 96 px where M3 specifies 114 dp
|
||||
- Severity: should-fix
|
||||
- M3 says: time-pickers/specs, "Time selector container | Width | 96dp" and "| Width (24h vertical) |
|
||||
**114dp**" (RB §Time Pickers/Specs); confirmed by
|
||||
`docs/reference/m3/tokens/TimePickerTokens.kt` → `TimeSelector24HVerticalContainerWidth = 114.0.dp`.
|
||||
- Library does: `resources/css/components/timepicker.css:106-107` sets
|
||||
`[data-timepicker-box] { width: 6rem; height: 5rem; }` unconditionally. The dial already carries the
|
||||
cycle as `data-cycle="24"` on `[data-timepicker-dial]` (`timepicker.blade.php:265`), but the display
|
||||
boxes never read it. The header comment (`timepicker.blade.php:33`) names only "96×80 time selector
|
||||
boxes", so the 114 dp token was not carried over.
|
||||
- Fix: put the cycle on the display too (`timepicker.blade.php:214` → `x-bind:data-cycle="is24 ? '24' : '12'"`)
|
||||
and add `[data-timepicker-display][data-cycle="24"] [data-timepicker-box] { width: 7.125rem; }` in
|
||||
`timepicker.css`, scoped out of the landscape block at `timepicker.css:403-447`.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### IN-11 · timepicker · the selected AM/PM button is primary-container, not tertiary-container
|
||||
- Severity: should-fix
|
||||
- M3 says: the dial colour list includes "Tertiary container, On tertiary container" (RB §Time
|
||||
Pickers/Specs, 17 site roles), and `docs/reference/m3/tokens/TimePickerTokens.kt` gives
|
||||
`PeriodSelectorSelectedContainerColor = TertiaryContainer`,
|
||||
`PeriodSelectorSelectedLabelTextColor = OnTertiaryContainer`,
|
||||
`PeriodSelectorOutlineColor = Outline`, `PeriodSelectorOutlineWidth = 1.0.dp`,
|
||||
`PeriodSelectorContainerShape = CornerSmall`. The time **selector** (hour/minute), separately, is
|
||||
`PrimaryContainer` / `OnPrimaryContainer`.
|
||||
- Library does: `resources/css/components/timepicker.css:160-169` gives the selected period button
|
||||
`background-color: var(--md-sys-color-primary-container); color: var(--md-sys-color-on-primary-container)`
|
||||
— identical to the selected hour/minute box at `timepicker.css:120-124`, so the two different M3
|
||||
emphases collapse into one. It also drops the 1 dp `Outline` stroke and replaces the shared
|
||||
corner-small container with two separate pills (full → 12 px on selection). The header
|
||||
(`timepicker.blade.php:37-39`) justifies this as Compose's current `isUpdatedTimepickerToggleEnabled`
|
||||
default and mentions "not the outlined pair its tokens still describe" — but not the tertiary→primary
|
||||
colour swap, which is the part a reader would not expect.
|
||||
- Fix: either restore `tertiary-container` / `on-tertiary-container` at `timepicker.css:165-167` (keeping
|
||||
the shape morph, which is a defensible Expressive reading), or document the colour swap explicitly in
|
||||
the header comment. Recommend the former: it restores the hour/minute vs. AM/PM distinction M3 draws.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### IN-12 · checkbox · the tick and dash icons are 16 px where M3 specifies 18 dp
|
||||
- Severity: should-fix
|
||||
- M3 says: checkbox/specs, "Icon size | 18dp"; `docs/reference/m3/tokens/CheckboxTokens.kt` →
|
||||
`IconSize = 18.0.dp` (= `ContainerSize`, i.e. the icon fills the box).
|
||||
- Library does: `resources/views/components/checkbox.blade.php:41-42` renders both
|
||||
`<x-icon name="check" class="size-4" />` and `name="remove" class="size-4"` — 16 px inside an 18 px box
|
||||
(`selection.css:105-108`). `resources/css/components/selection.css:1-3` cites `CheckboxTokens` as the
|
||||
source, so the 18 dp figure was rounded to Tailwind's nearest step.
|
||||
- Fix: `checkbox.blade.php:41-42` → `class="size-4.5"` (Tailwind 4's 4.5 = 18 px; the chip already uses
|
||||
`size-4.5` for its 18 dp icons, `chip.blade.php:233,254,282`).
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### IN-13 · chip-set · chips are Tab stops only; M3's arrow-key navigation is missing
|
||||
- Severity: should-fix
|
||||
- M3 says: chips/accessibility keyboard table — "Tab | Moves focus to enabled chip or chip group;
|
||||
Space or Enter | Activates, selects, or deselects the focused chip; Backspace or Delete | Removes
|
||||
currently focused input chip; **Arrows | Moves focus between chips**" (RB §Chips/Accessibility), and
|
||||
"only one chip can be in focus even though many can be selected".
|
||||
- Library does: `resources/views/components/chip-set.blade.php:31-54` renders `role="group"` with every
|
||||
chip its own natural tab stop; `resources/js/chips.js` implements Backspace/Delete
|
||||
(`chips.js:38-52`) and focus hand-off on removal (`chips.js:60-79`) but binds no arrow keys. A ten-chip
|
||||
filter set therefore costs ten Tab presses to pass.
|
||||
- Fix: add an `x-on:keydown.arrow-right/left/home/end` handler to the `[data-chip-set]` div in
|
||||
`chip-set.blade.php` (both the `scroll` and wrap branches) that moves focus across
|
||||
`[data-chip]` using the existing `CONTROLS` selector from `chips.js:13`, with a roving `tabindex`
|
||||
so the set is one tab stop. Note the trade-off: a filter chip is a real `<input type="checkbox">` in a
|
||||
`<label>` (`chip.blade.php:181-189`), so roving tabindex must be applied to the inputs, not the labels.
|
||||
- Effort: M
|
||||
- Breaks API? no
|
||||
|
||||
### IN-14 · chip (input) · the remove button's target is 34 px wide against M3's 48 dp minimum
|
||||
- Severity: should-fix
|
||||
- M3 says: input chip specs, "Target size for close icon | **Min 48dp**"; and "minimum target size 48dp
|
||||
regardless of placement/density (target may extend beyond the visible chip container)" —
|
||||
RB §Chips/Specs and §Chips/Behaviour.
|
||||
- Library does: `resources/views/components/chip.blade.php:250` gives the remove button
|
||||
`after:-inset-x-2 after:-inset-y-3.75` on an 18 px icon (`size-4.5`, line 247) → 18 + 2×15 = 48 px tall
|
||||
but 18 + 2×8 = **34 px wide**.
|
||||
- Fix: `chip.blade.php:250` → `after:-inset-x-3.75 after:-inset-y-3.75` (48 × 48). The chip's own 48 px
|
||||
target strip (`chip.blade.php:93`) already overlaps the label, so the extra 7 px each side only
|
||||
overlaps the label's target, which M3 explicitly permits ("target may extend beyond the visible chip
|
||||
container"); the remove button is later in source order and sits above it.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### IN-15 · pagination · page and step controls are 40 px targets
|
||||
- Severity: should-fix
|
||||
- M3 says: "The size of state layers is 40dp while the interactive target size is 48dp" (RF §States);
|
||||
accessibility: keep targets ≥ 48 × 48 CSS px (RF §Accessibility/Values). Pagination is not an M3
|
||||
component, so foundations are the yardstick per the brief.
|
||||
- Library does: `resources/views/pagination/livewire/tailwind.blade.php:22` —
|
||||
`$step = 'grid size-10 place-items-center …'` (40 px) is used for the page buttons, the prev/next
|
||||
buttons and the current page, with `gap-1` between them (line 35); same at
|
||||
`resources/views/pagination/laravel/tailwind.blade.php:7-9`. (The two `simple-tailwind` views are not
|
||||
affected — they use `<x-button variant="outlined">`, whose sizing belongs to the buttons group.) The
|
||||
file's own header (line 7) calls them "40px icon-button targets", conflating M3's 40 dp *state layer*
|
||||
with its 48 dp *target*.
|
||||
- Fix: keep the 40 px visual and add the target — in the two full pagination views change `$step` to
|
||||
`'relative grid size-10 place-items-center after:absolute after:-inset-1 …'`, or wrap each button in a
|
||||
48 px grid cell. Also correct the header comment.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### IN-16 · table · density is applied by default on every fine-pointer device
|
||||
- Severity: should-fix
|
||||
- M3 says: "**Don't apply density by default** — this drops targets below the 48x48 CSS px minimum;
|
||||
offer an explicit density opt-in instead, keeping opt-out targets at ≥48x48 CSS px" — RF
|
||||
§Accessibility/Rules, repeated on checkbox, chips, radio, switch and text-field accessibility pages.
|
||||
(Data tables themselves have no M3 spec — RB §Data Tables confirms the component is absent from
|
||||
m3.material.io — so foundations govern.)
|
||||
- Library does: `resources/css/components/table.css:37-45` tightens `--cell-y` from `0.75rem` to
|
||||
`0.5rem` under `@media (pointer: fine)` with no opt-out, and to `0.25rem` at `data-size="xs"`. With
|
||||
`body-md`'s 20 px line box that is a 36 px row (28 px at `xs`). Even on a coarse pointer the default row
|
||||
is 12 + 20 + 12 = 44 px, under 48. Rows are clickable (`data-list-row`,
|
||||
`resources/js/list-rows.js`), so the row *is* a target.
|
||||
- Fix: `resources/css/components/table.css` — raise the coarse-pointer default to
|
||||
`--cell-y: 0.875rem` (14 px → 48 px rows) and turn the fine-pointer step into an opt-in
|
||||
(`[data-table][data-density="compact"]`) rather than a media query, exposed as a `density` prop on
|
||||
`resources/views/components/table.blade.php:10-12`.
|
||||
- Effort: M
|
||||
- Breaks API? yes — adds a prop and changes default row height; existing `size="xs"` keeps its meaning.
|
||||
|
||||
### IN-17 · sort-header · the sort button's hit target is about 20 px tall
|
||||
- Severity: should-fix
|
||||
- M3 says: 48 × 48 minimum target, no density by default (RF §Accessibility/Values and /Rules).
|
||||
- Library does: `resources/views/components/sort-header.blade.php:21-30` renders
|
||||
`class="group/sort focus-ring inline-flex cursor-pointer items-center gap-1 rounded-corner-xs"` with no
|
||||
padding of its own. Its box is the `title-sm` line box (20 px, `resources/css/tokens/type.css:45`) plus
|
||||
a 16 px icon — roughly 20 px tall. The surrounding `<th>` padding (`table.css:47-51`) belongs to the
|
||||
cell, not the button, so it is not part of the target.
|
||||
- Fix: `sort-header.blade.php:26` → add `-my-2 py-2 -mx-1 px-1` (or `min-h-12` with the `<th>` padding
|
||||
removed for sorting cells) so the button covers its cell.
|
||||
- Effort: S
|
||||
- Breaks API? no (the `class` prop still lands on the `<th>`)
|
||||
|
||||
### IN-18 · field · the error state has only one visual indicator
|
||||
- Severity: should-fix
|
||||
- M3 says: "**States have two visual indicators to ensure accessibility**" (RF §States/Principles), and
|
||||
for text fields specifically: error text "strongly recommended to pair with a **trailing error icon**
|
||||
for a second, non-color-dependent cue" (RB §Text Fields/Behaviour); a non-actionable error icon gets the
|
||||
accessibility label "Error." (RB §Text Fields/Accessibility).
|
||||
- Library does: `resources/views/components/field.blade.php:39-45` sets only `data-invalid`, and
|
||||
`resources/css/components/field.css:403-415,446-448` changes only colours — outline, label and support
|
||||
text all move to `error`. No icon is added. `<x-input>`'s `icon-right` slot is free, so the hook exists.
|
||||
- Fix: in `field.blade.php`, when `$messages !== []` and no `$trailing` is set, render
|
||||
`<x-icon name="error" class="field-trailing size-(--field-icon)" aria-label="{{ __('Error') }}" />` into
|
||||
the trailing position; suppress it at `data-size="xs"` where there is no room.
|
||||
- Effort: S
|
||||
- Breaks API? no (additive; callers passing `icon-right` keep theirs)
|
||||
|
||||
### IN-19 · field · a read-only field is drawn with a dashed outline
|
||||
- Severity: nice-to-have
|
||||
- M3 says: "**Read-only fields**: same visual style as an editable field, but clearly labeled read-only"
|
||||
— RB §Text Fields/Behaviour.
|
||||
- Library does: `resources/css/components/field.css:419-421` —
|
||||
`.field[data-readonly] .field-outline { border-style: dashed; }`, set from
|
||||
`input.blade.php:38`, `textarea.blade.php:27` and `datepicker.blade.php:161`. The comment at
|
||||
`field.css:417-418` explains the intent (a date picker makes its own input read-only). The native
|
||||
`readonly` attribute does carry the state to AT, so the accessibility half of M3's rule is met; only
|
||||
"same visual style" is not.
|
||||
- Fix: either drop the dashed rule, or keep it and record it in the header comment as a deliberate
|
||||
departure. If kept, note that the date picker's *docked* field is **not** read-only
|
||||
(`datepicker.blade.php:182-189` binds typing), so only `mode="modal"`/`"input"` and caller-set
|
||||
`readonly` fields are affected.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### IN-20 · search · the bar has no minimum or maximum width and does not widen on focus
|
||||
- Severity: nice-to-have
|
||||
- M3 says: search bar "Container | Width | Min: 360dp, max: 720dp"; docked container the same; and the
|
||||
Expressive update lists "Motion: the search bar grows wider when focused" — RB §Search/Specs and
|
||||
§Search/Status.
|
||||
- Library does: `resources/css/components/search.css:28-41` sizes `[data-search-bar]` only by height;
|
||||
width is the parent's. `[data-search-view]` (`search.css:130-147`) is `inset-inline: 0` with
|
||||
`max-height: min(40rem, 70dvh)` and no `min-height` (M3: docked height "Min: 240dp, max: 2/3 of screen
|
||||
height" — 70 dvh vs 66.7 % is close enough to ignore). No focus-widening transition.
|
||||
- Fix: `search.css:28` → add `max-width: 45rem;` (720 px) to `[data-search-bar]` and `[data-search-view]`,
|
||||
and `min-height: 15rem` (240 px) to the docked view. The focus-widen motion would need a
|
||||
`--search-width` custom property toggled by `[data-open]` and animated on the effects-default spring.
|
||||
- Effort: S (widths) / M (motion)
|
||||
- Breaks API? no
|
||||
|
||||
### IN-21 · search · the unfocused leading padding is 16 px where M3 gives 24 dp
|
||||
- Severity: nice-to-have
|
||||
- M3 says: unfocused search bar "Leading padding | Unfocused: 24dp, focused: 12dp" and "Trailing padding |
|
||||
Unfocused: 24dp, focused: 12dp"; contained focused style "Leading padding | 16dp | Trailing padding |
|
||||
16dp" — `docs/reference/m3/raw/components_search_specs.md:338-346,376-386`.
|
||||
- Library does: `resources/css/components/search.css:28-35` gives the bar `padding-inline: 0.25rem` and
|
||||
`resources/css/components/search.css:53-61` gives the leading/clear boxes `width: 3rem` — so the icon
|
||||
glyph starts 4 + 12 = 16 px from the container edge in every state.
|
||||
- Both readings: the site's measurement diagram is an image, so "leading padding" may mean edge-to-tap-
|
||||
target (24 + 12 = 36 px to the glyph, which looks wrong in a 56 dp bar) or edge-to-glyph (24 px). Under
|
||||
the second reading the focused/contained value (16 dp) is exactly what the library does and only the
|
||||
unfocused state is 8 px short.
|
||||
- Fix: if pursued, `search.css:28` → `padding-inline: 0.75rem` when `:not([data-open])` and `0.25rem`
|
||||
when `[data-open]`, transitioned on the effects-fast duration.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### IN-22 · datepicker · Shift+M and Shift+Y do not reach the month/year dropdowns
|
||||
- Severity: nice-to-have
|
||||
- M3 says: date-pickers/accessibility keyboard table — "Shift + M | Moves to the month list dropdown;
|
||||
Shift + Y | Moves to the year list dropdown" (RB §Date Pickers/Accessibility).
|
||||
- Library does: `resources/js/datepicker.js:578-600` handles Arrows, Home, End, PageUp/PageDown (with
|
||||
Shift for ±12 months), Space and Enter; there is no Shift+M / Shift+Y branch. The menu buttons exist
|
||||
and are focusable (`datepicker.blade.php:287-296,309-318,328-337`).
|
||||
- Fix: add two cases to the `gridKey` map in `resources/js/datepicker.js:585-592` that focus
|
||||
`[data-datepicker-menu-button]` (the month one in the docked layout, the combined one in the modal).
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### IN-23 · choices · the combobox has no Home/End keys
|
||||
- Severity: nice-to-have
|
||||
- M3 says: nothing directly — M3 has no combobox component. WAI-ARIA APG's combobox-with-listbox pattern,
|
||||
which the component already follows (`role="combobox"`, `aria-autocomplete`, `aria-activedescendant`),
|
||||
specifies Home/End within the popup.
|
||||
- Library does: `resources/views/components/choices.blade.php:140-145` binds ArrowDown, ArrowUp, Enter,
|
||||
Escape and Tab only.
|
||||
- Fix: add `x-on:keydown.home.prevent` / `.end.prevent` at `choices.blade.php:142` setting
|
||||
`active = 0` / `active = filtered.length - 1` and calling `reveal()`.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### IN-24 · slider · Space+Arrows (the large-interval step) is not implemented
|
||||
- Severity: nice-to-have
|
||||
- M3 says: sliders/accessibility keyboard table — "Space & Arrows | Increase/decrease the value by one
|
||||
**interval** or one stop indicator" (RB §Sliders/Accessibility), on top of Arrows (one value) and
|
||||
Home/End.
|
||||
- Library does: the native `<input type="range">` supplies Arrows and Home/End
|
||||
(`slider.blade.php:357-379`); the SKILL documents PageUp/PageDown as a tenth of the range
|
||||
(`SKILL.md:562`), which is an extension, not M3's Space+Arrows.
|
||||
- Fix: add a `keydown` handler in `resources/js/slider.js` that, while Space is held, multiplies the arrow
|
||||
step. Low value — PageUp/PageDown already covers the need and is the more common web idiom.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### IN-25 · chips · no dragged state
|
||||
- Severity: nice-to-have
|
||||
- M3 says: chips are the only component in this group whose state list includes **Dragged**
|
||||
(RB §Chips/Specs, "States (site, all 4 variants): Enabled, Disabled, Hovered, Focused, Pressed,
|
||||
**Dragged**"); dragged state layer opacity is 0.16 (RF §States/Values). Input chips also support
|
||||
"reordering, moving between fields ('To' → 'CC')" (RB §Chips/Behaviour).
|
||||
- Library does: `resources/views/components/chip.blade.php:158` and the `state-layer` utility
|
||||
(`resources/css/tokens/state.css:13-45`) cover hover 8 %, focus 10 % and active 10 % only. No chip is
|
||||
draggable.
|
||||
- Fix: only worth doing alongside a reordering feature — then add
|
||||
`[draggable]:active`/`[data-dragged]` at 16 %.
|
||||
- Effort: L
|
||||
- Breaks API? no (additive)
|
||||
|
||||
### IN-26 · timepicker · the AM/PM buttons are toggle buttons, not radios
|
||||
- Severity: nice-to-have
|
||||
- M3 says: time-pickers/accessibility labelling table — "AM/PM selection | AM or PM | **Radio button (in
|
||||
list)** (Wiz/Compose) | Checkbox (in list) (Android Views)" (RB §Time Pickers/Accessibility).
|
||||
- Library does: `resources/views/components/timepicker.blade.php:239-251` and `:333-345` render two
|
||||
`<button aria-pressed>` in a `role="group"`. A screen reader hears "AM, toggle button, pressed" rather
|
||||
than "AM, radio button, selected, 1 of 2"; both convey the state, and `aria-pressed` avoids needing
|
||||
arrow-key roving, so this is defensible. Noted because the M3 table is explicit.
|
||||
- Fix: if changed, `role="radiogroup"` on the wrapper and `role="radio" aria-checked` on the buttons,
|
||||
with Left/Right arrow handling.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### IN-27 · timepicker · the disabled period button uses a 10 % container instead of 12 %
|
||||
- Severity: nice-to-have
|
||||
- M3 says: disabled container opacity is **0.12**, disabled content 0.38 (RF §States/Values). M3 defines
|
||||
no disabled state for the period selector at all (its state list is Enabled, Hover, Focus, Pressed —
|
||||
RB §Time Pickers/Specs), so this is the library's own `min`/`max` extension and only has to stay inside
|
||||
M3's rules.
|
||||
- Library does: `resources/css/components/timepicker.css:172-174` —
|
||||
`background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 10%, transparent)` with the content
|
||||
at 38 % (correct).
|
||||
- Fix: `timepicker.css:173` → `12%`.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### IN-28 · radio · `inline` lays radios in a row, which M3 cautions against
|
||||
- Severity: nice-to-have
|
||||
- M3 says: radio buttons are "Always vertically stacked (never horizontal — flagged as a 'Caution', not
|
||||
an outright 'Don't')" (RB §Radio Button/Behaviour); also "Recommended for five or fewer options" and
|
||||
"One option should always be pre-selected in a radio group".
|
||||
- Library does: `resources/views/components/radio.blade.php:18,39` offers
|
||||
`inline` → `sm:flex sm:flex-wrap sm:gap-x-6`, and `value` (the pre-selection) is optional
|
||||
(`radio.blade.php:19`). Neither is wrong — M3's is a caution — but the SKILL entry
|
||||
(`SKILL.md:552`) presents `inline` without the caveat.
|
||||
- Fix: add one sentence to `SKILL.md:552` and to the `radio.blade.php` header noting M3 prefers a vertical
|
||||
stack, and that a group should normally ship a pre-selected `value`.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### IN-29 · password · the reveal button carries both a changing label and `aria-pressed`
|
||||
- Severity: nice-to-have
|
||||
- M3 says: "Interactive trailing icons need a functional label (e.g., 'Show password' / 'Hide password')"
|
||||
— RB §Text Fields/Accessibility. It says nothing about a pressed state.
|
||||
- Library does: `resources/views/components/password.blade.php:41-44` binds *both*
|
||||
`aria-label` (flipping between "Show password" and "Hide password") **and** `aria-pressed`. Screen
|
||||
readers then announce e.g. "Hide password, toggle button, pressed" — the state twice, once inverted
|
||||
relative to the label's promise. M3 names only the label pattern; the repo's own recent commit
|
||||
"Keep aria-pressed off a selected link button" shows the same concern elsewhere.
|
||||
- Fix: `password.blade.php:42,44` → drop `aria-pressed` and keep the flipping label (the M3-documented
|
||||
pattern), or keep `aria-pressed` with a fixed "Show password" label. Not both.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### IN-30 · table · the row divider is a colour outside the role set
|
||||
- Severity: nice-to-have
|
||||
- M3 says: colour is applied through the roles; RF §Design tokens frames system tokens as the sanctioned
|
||||
vocabulary and the styles pages give no "60 % of outline variant" value. Opacity modifiers in M3 are
|
||||
reserved for state layers (8/10/10/16 %) and disabled (12/38 %).
|
||||
- Library does: `resources/css/components/table.css:66-68` —
|
||||
`border-bottom: 1px solid color-mix(in srgb, var(--md-sys-color-outline-variant) 60%, transparent)`.
|
||||
(The header rule at `table.css:58` correctly uses full `outline-variant`.) Data tables have no M3 spec,
|
||||
so this is a foundations-level nit, not a spec breach.
|
||||
- Fix: `table.css:67` → `var(--md-sys-color-outline-variant)`, and let the heavier header rule read as
|
||||
heavier by keeping the header's `title-sm` weight contrast, or use `surface-container-highest` if a
|
||||
lighter rule is wanted.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
## Deliberate deviations
|
||||
|
||||
The code explains these; the verdict column is whether the stated reason survives Google's text.
|
||||
|
||||
| Where | Deviation and its stated reason | Holds up? |
|
||||
|---|---|---|
|
||||
| `field.css:8-12` | The outlined field's notch is MUI's `<fieldset>`/`<legend>` technique so the gap is right on any background | Yes. M3 does not prescribe a mechanism; the rendered result (4 dp corners, 1 dp / 2 dp focus outline, `body-small` floated label) matches `OutlinedTextFieldTokens` exactly. |
|
||||
| `field.blade.php:9-13` | `size="sm"` (40 px) / `xs` (32 px) for unlabelled toolbar controls | Yes — M3's rule is "don't apply density **by default**; offer an explicit opt-in" (RF §Accessibility). These are opt-in and documented as unlabelled-control sizes. |
|
||||
| `field.css:417-421` | Read-only fields get a dashed outline "marked on the field, not read off the control" | Partly — see IN-19; M3 says a read-only field keeps the same visual style. |
|
||||
| `select.blade.php:1-8` | The native `<select>` is kept "on purpose", with the M3 menu only where `appearance: base-select` exists | Yes. M3's exposed-dropdown guidance is about the rendered result, and the native control gives the keyboard, type-ahead and AT semantics for free. |
|
||||
| `file.blade.php:1-13` | "M3 has no file field" — the native input is kept and only `::file-selector-button` is restyled as a tonal pill | Yes. RB confirms no file-input component exists; the pill uses `secondary-container` / `on-secondary-container` with 8 % hover and 12 %/38 % disabled — inside M3's rules. |
|
||||
| `chip.blade.php:29-30` | "input chips are flat only, as in M3" | Partly. RB §Chips/Variants says "**All** chips can be elevated (default elevation 0)"; Compose has no `ElevatedInputChip`, which is presumably the real source. The restriction is safe but the stated justification is the implementation, not the spec. |
|
||||
| `chip.blade.php:88-89` | Padding is 1 px short of the token because a CSS border takes room where Compose draws it inside | Yes — a correct compensation; the rendered box matches 16/8/12/4 dp. |
|
||||
| `timepicker.blade.php:37-39` | The period selector follows Compose's `isUpdatedTimepickerToggleEnabled` default — "two separate shape-morphing toggle buttons in primary-container, not the outlined pair its tokens still describe" | Partly — see IN-11. The shape morph is a plausible Expressive reading; the tertiary→primary colour swap is undocumented and the site still lists Tertiary container. |
|
||||
| `datepicker.js:18-19` | "The keyboard is WAI-ARIA's date picker dialog: … Home and End go to the start and end of the week" | Defensible. M3's own table says "Home/End | Move to the first day of the month" (RB §Date Pickers/Accessibility) — a row the reference itself flags as terse/ambiguous. The APG behaviour is the stronger web convention; worth a one-line note in the header that M3's table differs. |
|
||||
| `slider.blade.php:39-41` | The value indicator is "M3's as Flutter draws it (`RoundedRectSliderValueIndicatorShape`): a 32px inverse-surface pill" because "Compose leaves the value label to the app" | No — see IN-05. M3 Expressive's sliders/specs table gives an explicit 44 dp × 48 dp label container that neither source was consulted for. |
|
||||
| `search.css:83-87` | `[data-search-bar] button:focus-visible { outline-offset: -3px }` instead of the package's `+2px` | Yes, in context — the ring is drawn inside a 48 px box that is flush with the 56 px bar; a +2 px ring would clip. |
|
||||
| `table.css:7-10` | Density is keyed "on the pointer, not the width, so a touch tablet in landscape keeps rows a finger can hit" | Partly — the pointer key is a good idea, but the coarse-pointer default is still 44 px (IN-16). |
|
||||
| `chip-set.blade.php:12-14` | `scroll` keeps chips on one line that scrolls sideways "as M3 lays chips out on a narrow screen" | Yes. RB §Chips/Behaviour: "place inline as a single row, wrapping to new lines when needed, or **scroll horizontally** if the field is only one row tall". |
|
||||
|
||||
### Token-citation disagreements
|
||||
|
||||
Places where a header comment's androidx/MDC figure contradicts the M3 site's spec table:
|
||||
|
||||
| File:line | Comment claims | M3 site says | Effect in code |
|
||||
|---|---|---|---|
|
||||
| `slider.blade.php:44` | "handles of 44, 44, **44**, 68 and 108px" (md.comp.slider.* tokens) | 44 / 44 / **52** / 68 / 108 (sliders/specs) | Yes — `slider.blade.php:248` is 44 px (IN-04) |
|
||||
| `slider.blade.php:40-41` | value indicator "a **32px** inverse-surface pill … 4px above the handle" (Flutter) | Label container **44 dp** high, **48 dp** wide, all sizes; `ValueIndicatorActiveBottomSpace = 12dp` | Yes — `slider.blade.php:346,252` (IN-05) |
|
||||
| `timepicker.blade.php:33` | "96×80 time selector boxes" | 96 dp, **and 114 dp for the 24-hour vertical layout** | Yes — `timepicker.css:106` (IN-10) |
|
||||
| `timepicker.blade.php:37-39` | period selector "in **primary-container**, not the outlined pair its tokens still describe" | `PeriodSelectorSelectedContainerColor = TertiaryContainer`; site colour list includes Tertiary container / On tertiary container | Yes — `timepicker.css:165-167` (IN-11) |
|
||||
| `selection.css:1-3` | cites `CheckboxTokens` | `IconSize = 18.0.dp` | Yes — `checkbox.blade.php:41-42` uses `size-4` (16 px) (IN-12) |
|
||||
| `search.css:1-10` | cites `SearchBarTokens` / `SearchViewTokens`; "surface-container-high" for the view | Docked: Surface container high ✓; **Full-screen: Surface container low** | Yes — `search.css:140` (IN-08) |
|
||||
| `pagination/livewire/tailwind.blade.php:7` | "Page numbers are **40px icon-button targets**" | 40 dp is the state layer; the target is 48 dp (RF §States) | Yes — `size-10` (IN-15) |
|
||||
| `chip.blade.php:35-38` | 32 px, small corner, label-large, 18 px icons, 24 px avatar, 16/8/8 and 12/8/4 padding | identical (chips/specs, all four variants) | No — all correct |
|
||||
| `field.css:8-12` | `FilledTextFieldTokens` / `OutlinedTextFieldTokens` | 56 dp height, `CornerExtraSmall` = 4 dp, outline 1/2 dp, indicator 1/2 dp, disabled 12 %/38 %/4 %, `BodyLarge` input, `BodySmall` supporting | No — all correct |
|
||||
| `datepicker.blade.php:39-45` | 360 px, extra-large (modal) / large (docked) corner, elevation 3, surface-container-high, 40 px days in 48 px cells, today outlined primary, range middle secondary-container | `DatePickerModalTokens`: 360 × 568, `CornerExtraLarge`, Level3, Surface container high, cell 40 × 40, today outline 1 dp, range indicator 40 dp | No — all correct (the 48 px cell is the library's own target addition, which M3's 48 dp target rule requires) |
|
||||
| `selection.css:219-223` | switch handle centres at 16 px / 36 px, sizes 16 / 24 / 28 px "measured inside the track's 2px outline" | `SwitchTokens`: track 32 × 52, outline 2, handles 16 / 24 / 28 | Numbers correct; the phrase "inside the track's 2px outline" is wrong (the handle is positioned against the track's outer box, `selection.css:249-251`) — comment only |
|
||||
|
||||
## Aligned
|
||||
|
||||
Verified correct; no need to re-check.
|
||||
|
||||
- **Text field geometry** — 56 px container, 16 px side padding (12 px with icons via `--field-start`),
|
||||
24 px leading icon, 4 dp corner, 1 px → 2 px outline/indicator on focus, `body-large` input,
|
||||
`body-small` floated label and supporting text, 4 px supporting-text top padding
|
||||
(`field.css:27-73,295-344,439-444`).
|
||||
- **Text field colours and states** — outline `outline`, hover `on-surface`, focus `primary`, filled
|
||||
container `surface-container-highest`, filled resting indicator `on-surface-variant`, filled hover
|
||||
`on-surface` 8 %, disabled outline 12 % / content 38 % / filled container 4 % — all match
|
||||
`OutlinedTextFieldTokens` / `FilledTextFieldTokens` (`field.css:33-44,390-437,450-520`).
|
||||
- **Error replaces the hint, never stacks** (`field.blade.php:73-81`), with `role="alert"` on the error —
|
||||
exactly RB §Text Fields/Behaviour and /Accessibility.
|
||||
- **Required asterisk is in the label** (`field.css:346-349`, in both the label and the notch legend) —
|
||||
RB §Text Fields/Accessibility, "the label must include the asterisk".
|
||||
- **Date input is never masked while typing** — `datepicker.js:350-366` only commits a whole, valid date;
|
||||
`parse` (`datepicker.js:274-300`) accepts any separator and optional leading zeros. This is the exact
|
||||
rule in RB §Date Pickers/Accessibility.
|
||||
- **Switch** — every dimension matches `SwitchTokens`: 52 × 32 track, 2 px outline, 16 / 24 / 28 px handle,
|
||||
40 px state layer, 16 px icons, and the full colour map (selected track `primary`, handle `on-primary`,
|
||||
hover/focus/pressed handle `primary-container`, unselected track `surface-container-highest`, handle
|
||||
`outline`, disabled 12 %/38 %) — `selection.css:219-343`.
|
||||
- **Checkbox / radio boxes** — 18 px box with 2 px `on-surface-variant` outline and 2 dp corner, 20 px
|
||||
radio with a 2 px ring, both in 40 px state layers, `primary` when selected, error variants
|
||||
(`selection.css:103-217`). Indeterminate uses a dash icon and is kept in sync through the
|
||||
`indeterminate` DOM property (`field.js:32-34,49-80`) — the only correct way to do it in HTML.
|
||||
- **State-layer opacities everywhere in the group** — 8 % hover (always behind `@media (hover: hover)`),
|
||||
10 % focus, 10 % press, tinted with the content's "on" colour: `selection.css:73-101`,
|
||||
`field.css:268-282`, `datepicker.css:250-268`, `chip.blade.php:158,249`, `state.css:13-45`.
|
||||
- **Focus ring** — 3 px `secondary` at +2 px offset, consistently (`state.css:46-53`, `field.css:274-278`,
|
||||
`selection.css:98-101,241-244`, `datepicker.css:270-275`, `timepicker.css:207-208`,
|
||||
`slider.blade.php:335`).
|
||||
- **Chips** — 32 px height, 8 dp corner, `label-large`, 18 px icons, 24 px avatar, 16/8 dp label padding
|
||||
(1 px short for the CSS border), 8 dp between elements, 8 px chip-to-chip gap, a 48 px `::after` target
|
||||
strip, filter selected `secondary-container`/`on-secondary-container`, stroke `outline-variant`
|
||||
(the Aug-2024 update), elevated on `surface-container-low` at elevation 1 → 2 on hover, disabled
|
||||
12 %/38 % (`chip.blade.php:91-125,197-198`, `chip-set.blade.php:51`).
|
||||
- **Chip keyboard removal** — Backspace/Delete on a focused input chip removes it and moves focus to the
|
||||
previous/next chip, with a `wire:key`-aware re-focus across Livewire morphs (`chips.js:38-118`). Remove
|
||||
buttons are named "Remove {label}" (`chip.blade.php:242`). Exactly RB §Chips/Accessibility.
|
||||
- **Date picker anatomy and geometry** — 360 px container, 120 px header (128 px for range),
|
||||
`headline-large` headline (`title-large` for range), `surface-container-high` at elevation 3,
|
||||
`corner-xl` modal / `corner-lg` docked, 40 px day in a 48 px cell, `body-large` day and weekday labels,
|
||||
today outlined 1 px `primary`, selected `primary`/`on-primary`, range band
|
||||
`secondary-container`/`on-secondary-container` 40 px tall, 72 × 36 year cells, divider under the header
|
||||
(`datepicker.css:64-436`).
|
||||
- **Date picker semantics** — `<table role="grid">` with `<th scope="col">`, per-cell `aria-label` naming
|
||||
the full date, `aria-selected`, `aria-current="date"`, roving `tabindex`, `aria-live` month announcement,
|
||||
and both entry methods always available — matching M3's labelling table
|
||||
(`datepicker.blade.php:284,344-378`).
|
||||
- **Time picker geometry** — 24 dp container padding, 96 × 80 time selectors in `display-large` with a
|
||||
`corner-small` container, 52 × 80 (vertical) / 216 × 38 (horizontal) period selector, 256 px dial in
|
||||
`surface-container-highest` with `body-large` numbers, 48 px selector handle, 8 px centre, 2 px track,
|
||||
96 × 72 input fields in `display-medium` — every number in RB §Time Pickers/Specs
|
||||
(`timepicker.css:50-460`).
|
||||
- **Slider** — track heights 16/24/40/56/96, corners 8/8/12/16/28, 4 px handle narrowing to 2 px on
|
||||
press/focus, 6 px active-handle spacing, 4 px stop indicators, 24/24/32 px inset icons restricted to
|
||||
M/L/XL standard sliders only (never range or centered, exactly RB §Sliders/Variants), `primary` active
|
||||
track and handle, `secondary-container` inactive track, `inverse-surface`/`inverse-on-surface` value
|
||||
indicator in `label-large`, disabled 38 %/12 % (`slider.blade.php:132-252,283-354`).
|
||||
- **Slider accessibility** — real `<input type="range">`s, so Arrows/Home/End and AT come free; a range
|
||||
slider is `role="group"` with `aria-labelledby` and "Range start"/"Range end" handle labels; the
|
||||
drawing is `aria-hidden` (`slider.blade.php:265-379`).
|
||||
- **Reduced motion** — all six spring/easing duration tokens collapse to 0 ms under
|
||||
`prefers-reduced-motion: reduce` (`resources/css/tokens/motion.css:63-75`), and every component in this
|
||||
group animates through those tokens, so nothing needs a per-component opt-out.
|
||||
- **`<x-choices>` combobox semantics** — `role="combobox"`, `aria-autocomplete="list"`, `aria-controls`,
|
||||
`aria-expanded`, `aria-activedescendant`, `role="listbox"`/`role="option"`/`aria-selected`,
|
||||
`aria-disabled` (`choices.blade.php:121-179`). This is what `<x-search>` should look like (IN-01).
|
||||
- **Search geometry** — 56 px pill, `corner-full`, `surface-container-high`, 8 % hover, `body-large`
|
||||
input, 48 px leading/clear targets, docked view `corner-xl` at elevation 3, 72 px full-screen header
|
||||
with safe-area padding, `corner-none` full-screen — all matching `SearchBarTokens` / `SearchViewTokens`
|
||||
(`search.css:20-181`). Escape releases focus, the back arrow dismisses, and the full-screen view traps
|
||||
focus and locks scroll (`search.blade.php:29-32`, `search.js:39-97`).
|
||||
- **Pagination colour choice** — the current page is `secondary-container`/`on-secondary-container`, never
|
||||
the action colour, with `aria-current="page"` (`pagination/livewire/tailwind.blade.php:55`). This is the
|
||||
right reading of RF §Interaction/Selection ("active indicator" components).
|
||||
- **Table roles** — headers `title-small` `on-surface-variant` over an `outline-variant` rule, selected
|
||||
row `secondary-container`/`on-secondary-container` keyed on `aria-selected="true"`, `aria-sort` on the
|
||||
sorted `<th>` (`table.css:53-77`, `sort-header.blade.php:20`). RF §Interaction/Selection lists data
|
||||
tables among the components that inherit selection styling, and this is that styling.
|
||||
|
||||
## Missing
|
||||
|
||||
M3 components, variants or configurations in this group that the library does not implement.
|
||||
|
||||
- **Slider — vertical orientation.** RB §Sliders/Variants: the Expressive configuration table lists
|
||||
"Orientation | Horizontal (default) | Available | Available; Vertical | -- | Available". The library is
|
||||
horizontal only (`slider.blade.php` has no orientation prop). M3's caveat that range sliders must stay
|
||||
horizontal would apply.
|
||||
- **Date pickers — the full-screen range picker.** RB §Date Pickers/Anatomy describes a 14-element
|
||||
full-screen range-selection picker with a close (×) icon button and a **Save** confirmation, used at
|
||||
compact breakpoints. `<x-datepicker range>` always uses the 360 px modal dialog
|
||||
(`datepicker.css:64-129`), which on a phone is cramped for a two-month range scroll.
|
||||
- **Search — the other two entry points.** RB §Search/Behaviour names three: search bar (implemented),
|
||||
**search app bar** (a top app bar whose whole row is the search field, opening the Search view), and
|
||||
**search icon button** (search as a secondary action that expands). Only the first exists.
|
||||
- **Search — the Expressive focus-widen motion** ("the search bar grows wider when focused",
|
||||
RB §Search/Status). See IN-20.
|
||||
- **Text fields — the character counter.** RB §Text Fields/Variants lists "supporting text + character
|
||||
counter" as a configuration of both variants, with its own accessibility label ("Character count,
|
||||
5/20"). No `<x-input>`/`<x-textarea>` prop provides it; `maxlength` passes through to the control but
|
||||
nothing is rendered.
|
||||
- **Text fields — width bounds at medium/expanded.** RB §Text Fields/Behaviour: "compact breakpoints can
|
||||
let a text field span full width; medium/expanded should bound it with flexible margins/other
|
||||
containers — **never let it span the full width of a large screen**." `<x-form>`
|
||||
(`form.blade.php:14`) is an unbounded one-column grid; nothing in the package caps a field's width.
|
||||
Currently the caller's job, undocumented in `SKILL.md:521-535`.
|
||||
- **Checkbox — the ≥ 840 dp grouping rule.** RB §Checkbox/Behaviour: "in expanded breakpoints (≥840dp),
|
||||
group checkboxes inside a contained region (e.g., a side sheet)". No affordance or documentation.
|
||||
- **Chips — the horizontal-overflow affordances.** RB §Chips/Accessibility prescribes either a leading
|
||||
"Show all" filter chip that reflows the list downward, or a leading button opening a menu of all chip
|
||||
options. `<x-chip-set scroll>` fades the scrollable edge instead — a reasonable web idiom, but neither
|
||||
of M3's two named patterns is offered.
|
||||
- **Time picker — the 24-hour inner/outer ring is implemented** (`timepicker.blade.php:126-129`), so this
|
||||
is *not* missing; noted because it is easy to assume otherwise.
|
||||
|
||||
## Breakpoint map
|
||||
|
||||
| Component | Library breakpoint used | M3 window size class it stands in for | Gap |
|
||||
|---|---|---|---|
|
||||
| `<x-search>` full-screen ↔ docked | `(max-width: 39.99rem)` = < 640 px (`resources/js/search.js:11`) | Compact < 600 dp (full-screen), medium/expanded ≥ 600 dp (docked) | 40 px late — 600–639 px gets the full-screen view where M3 wants docked |
|
||||
| `<x-datepicker>` docked → modal dialog | `(max-width: 39.99rem)` = < 640 px (`resources/js/datepicker.js:23`) | Compact < 600 dp (M3: "Used at compact breakpoints as a full-screen dialog"; docked "Used at medium/expanded") | 40 px late; also M3 wants *full-screen* at compact, the library shows a 360 px centred dialog |
|
||||
| `<x-timepicker>` landscape layout | `(orientation: landscape) and (min-width: 37rem)` = ≥ 592 px (`timepicker.css:403`) | No width class — M3 keys this on device orientation and **viewport height** (RB §Time Pickers/Behaviour) | 8 px under 600; the height-based dial shrink at `max-height: 22.75rem` / `21.625rem` (`timepicker.css:449,455`) is Compose's `ClockFaceSizeModifier`, not an M3 window class — acceptable |
|
||||
| `<x-radio inline>` | `sm:` = ≥ 640 px (`radio.blade.php:39`) | Compact < 600 dp stacks; medium+ may lay out horizontally (M3 cautions against horizontal at any width) | 40 px late, and the direction of the rule is a caution either way (IN-28) |
|
||||
| Pagination numbers vs "Page 2 of 7" | `max-sm:` / `sm:` = 640 px (`pagination/livewire/tailwind.blade.php:31-32,48,53`) | Compact < 600 dp | 40 px late — 600–639 px loses the page numbers where M3's medium class would keep them |
|
||||
| `<x-table>` density | `@media (pointer: fine)` (`table.css:37`) | None — M3 has no pointer-keyed density rule; density is an explicit user opt-in at any size | Not a size-class substitution; the problem is that it is on by default (IN-16) |
|
||||
| `<x-field>` / `<x-form>` width | none | Medium/expanded should bound a text field's width (RB §Text Fields/Behaviour) | Not implemented at all — see **Missing** |
|
||||
| `<x-chip-set scroll>` | none (a prop, not a breakpoint) | M3 expects wrap-or-scroll chosen by available width | Caller decides; no adaptive switch |
|
||||
| `<x-chip type="filter">` trailing icon | none | M3: a filter chip's trailing icon is for medium/expanded; at compact the whole chip must be the tap target | Satisfied incidentally — the whole chip is always the target (`chip.blade.php:200`) |
|
||||
| `<x-search>` docked max height | `min(40rem, 70dvh)` (`search.css:136`) | Docked container height min 240 dp, max 2/3 of screen height | 70 dvh vs 66.7 % — negligible; the missing 240 dp minimum is the real gap (IN-20) |
|
||||
|
||||
Note on the Tailwind scale generally: this group consistently uses `sm` (640 px) where M3's compact
|
||||
boundary is 600 dp. The fix is one shared media query — a `--material-compact: (max-width: 37.49rem)`
|
||||
custom media (or a `xs`-ish Tailwind screen at 600 px) referenced by `search.js:11`, `datepicker.js:23`,
|
||||
`radio.blade.php:39` and the pagination views — rather than five separate edits.
|
||||
@@ -0,0 +1,637 @@
|
||||
# Audit: navigation
|
||||
|
||||
Scope: `app-bar`, `toolbar`, `tabs`/`tab`, `navigation-bar(-item)`, `navigation-rail(-item|-section)`,
|
||||
`app-shell`, `section-nav`, `account-menu`, `theme-toggle`, `scheme-picker`, their CSS/JS, the showcase
|
||||
usage and the matching SKILL.md sections.
|
||||
|
||||
Sources abbreviated below: **RCB** = `docs/reference/m3/reference-components-b.md`; **RF** =
|
||||
`docs/reference/m3/reference-foundations.md`; **raw/** = `docs/reference/m3/raw/<page>.md`; **tok/** =
|
||||
`docs/reference/m3/tokens/<File>.kt` (androidx-main). Anything fetched live is named with its URL.
|
||||
|
||||
## Summary
|
||||
|
||||
The component-level work is unusually faithful: every numeric token I checked for the app bar
|
||||
(64/112/136/120/152dp heights, title-large → headline-medium → display-small, 4dp edge spacing), the
|
||||
navigation bar (64dp, 56×32 indicator, 40dp horizontal pill, 16dp indicator spaces, and even Compose's
|
||||
`calculateCenteredContentHorizontalPadding` formula reproduced as `calc(10% * (count + 3))`), the rail
|
||||
(96dp collapsed, 220–360dp expanded, 44dp top space, 40dp header space, 56dp expanded item, modal =
|
||||
surface-container + elevation 2 + `CornerLarge` inner edge) and both toolbars (64dp, 16dp/4–32dp docked,
|
||||
8dp/4dp + `CornerFull` floating) matches the androidx token files exactly. The catalogue is also right
|
||||
about Expressive: the deprecated bottom app bar and navigation drawer are absent, the flexible app bars
|
||||
and the flexible navigation bar are what is implemented, and the expanded rail correctly plays the
|
||||
drawer's role. The real problems are three: **the adaptive breakpoint map is Tailwind's, not M3's** — `sm`
|
||||
640 stands in for 600 and, much worse, `lg` 1024 stands in for 840, so every window from 840 to 1023px is
|
||||
denied the standard expanded rail and gets a modal that re-closes on every navigation; **two hand-rolled
|
||||
buttons (`theme-toggle`, `account-menu`) are 40×40 with no 48px target**, below M3's stated minimum, while
|
||||
the library's own `<x-button>` and the rail's menu button both do it correctly; and **a few placement
|
||||
rules are simply not wired up** — a `place="bottom"` toolbar sits on top of the app shell's navigation
|
||||
bar, a rail-nested FAB keeps elevation 3 where M3 says level 0, and `theme-toggle mode="picker"`
|
||||
hand-rolls the Expressive-deprecated segmented button even though the library already ships its
|
||||
successor (`<x-group>`).
|
||||
|
||||
## Findings
|
||||
|
||||
### N-01 · theme-toggle, account-menu · 40×40 controls with no 48px interaction target
|
||||
- Severity: must-fix
|
||||
- M3 says: "Touch target minimum **48 × 48dp**"; "Web default target size: at least 48 × 48 **CSS
|
||||
pixels**"; and, precisely for this case, "The size of state layers is 40dp while the interactive target
|
||||
size is 48dp" (RF §Accessibility Values, §States/state-layer mechanics). RF §Layout/Density repeats:
|
||||
"Interaction targets must stay ≥48×48dp even when the visible element is scaled down."
|
||||
- Library does: `resources/views/components/theme-toggle.blade.php:65` renders `inline-flex size-10` (40px)
|
||||
with no target extension; `resources/views/components/account-menu.blade.php:41` renders
|
||||
`inline-flex size-10 … overflow-hidden` (40px), likewise none. The library already knows the fix: the
|
||||
icon button applies `after:absolute after:top-1/2 after:left-1/2 after:size-full after:min-h-12
|
||||
after:min-w-12 after:-translate-x-1/2 after:-translate-y-1/2` at `xs`/`sm`
|
||||
(`resources/views/components/button.blade.php:171-172`, comment "Below 48px the touch target reaches
|
||||
past the button, as M3 requires"), and the rail's menu button repeats it at
|
||||
`resources/views/components/navigation-rail.blade.php:108` (`after:size-12`).
|
||||
- Fix: add the same `after:` pseudo-target to both class lists. For `account-menu`, `overflow-hidden` on
|
||||
the button would clip the pseudo-element — move the clipping to the `<img>` (`class="size-full
|
||||
rounded-corner-full object-cover"`) and drop `overflow-hidden` from the button.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### N-02 · toolbar, app-shell · a `place="bottom"` toolbar sits on top of the navigation bar
|
||||
- Severity: must-fix
|
||||
- M3 says: "Docked toolbar and navigation bar occupy the same screen position — **never show both
|
||||
simultaneously**" (RCB §Toolbars/Behaviour; raw/components_toolbars_guidelines.md). For the floating
|
||||
case: "FAB, when present, floats above the nav bar, right-aligned — **must never overlap/cover it**"
|
||||
(RCB §Navigation Bar/Behaviour), and the toolbar occupies the same rail region as a FAB
|
||||
(raw/foundations_layout_scaffold_rails.md: "On mobile, a toolbar can float in the rail region" — the
|
||||
rail region sits *above* the navigation bar, item 1: "A toolbar sits above the navigation bar").
|
||||
- Library does: `resources/css/components/toolbar.css:75-81` pins the toolbar at
|
||||
`bottom: calc(1rem + var(--material-safe-bottom, env(safe-area-inset-bottom)))` — it ignores
|
||||
`--material-bottom-bar`, the variable the shell sets for exactly this purpose
|
||||
(`resources/views/components/app-shell.blade.php:88`). The library's FAB
|
||||
(`resources/views/components/button.blade.php:177`,
|
||||
`max-sm:bottom-[calc(var(--material-bottom-bar,0px)+1rem)]`) and the snackbar
|
||||
(`resources/views/components/toast.blade.php:36`) both do use it. A docked
|
||||
`variant="docked" place="bottom"` toolbar (`toolbar.css:91-96`, `bottom: 0`) lands squarely on the
|
||||
navigation bar with no warning anywhere in the code or in SKILL.md:754-764.
|
||||
- Fix: in `toolbar.css`, change `[data-toolbar-place="bottom"]`'s `bottom` to
|
||||
`calc(var(--material-bottom-bar, 0px) + 1rem + var(--material-safe-bottom, env(safe-area-inset-bottom)))`
|
||||
(mirroring the FAB/toast rule, which already folds the safe area into `--material-bottom-bar`), and for
|
||||
`[data-toolbar][data-variant="docked"][data-toolbar-place="bottom"]` `bottom: var(--material-bottom-bar, 0px)`.
|
||||
Add one line to SKILL.md's `<x-toolbar>` section: a docked toolbar and a navigation bar must never be on
|
||||
screen together — show the bar on primary pages, the toolbar on secondary/contextual ones.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### N-03 · navigation-rail · a FAB nested in the rail keeps elevation 3
|
||||
- Severity: must-fix
|
||||
- M3 says: "When nested within another component, such as the navigation rail, the FAB's resting
|
||||
elevation should be **level 0**" (raw/components_navigation-rail_guidelines.md:213; RCB §Navigation
|
||||
Rail/Behaviour: "the site explicitly states this contrasts with a standalone FAB's normal elevation").
|
||||
- Library does: `resources/views/components/fab.blade.php:49` always applies
|
||||
`shadow-elevation-3 hover:shadow-elevation-4`; `resources/css/components/navigation.css` contains no
|
||||
rule for a FAB inside a rail (grep for `fab` in it returns nothing). The documented pattern
|
||||
(`navigation-rail.blade.php:7-9`, SKILL.md:717-719, `showcase/shell.blade.php:48-49`) puts a FAB in the
|
||||
`header` slot, so every documented use is at elevation 3.
|
||||
- Fix: the FAB has no attribute hook today (`fab.blade.php:65` renders `<{{ $tag }} {{ $attributes }}>`
|
||||
with classes only), so add `data-fab` to its merged attributes, then add an **unlayered** rule to
|
||||
`navigation.css` — `shadow-elevation-3` is a Tailwind utility, so a `@layer components` rule would lose,
|
||||
exactly as `toolbar.css:10-12` already explains for its vibrant recolouring:
|
||||
`[data-navigation-rail-header] [data-fab], [data-navigation-rail-header] [data-fab]:hover { box-shadow: none; }`.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### N-04 · theme-toggle · `mode="picker"` renders the Expressive-deprecated segmented button
|
||||
- Severity: must-fix
|
||||
- M3 says: segmented buttons "are deprecated in the expressive update"; "connected button groups should
|
||||
replace the baseline segmented button" (`docs/reference/m3/reference-components-a.md:343-345, :224, :257`).
|
||||
- Library does: `resources/views/components/theme-toggle.blade.php:22-48` hand-rolls one — a 40px
|
||||
(`h-10`) outlined stadium (`rounded-corner-full border border-outline`) whose segments share dividing
|
||||
borders (`not-first:border-s`) and whose selected segment is `bg-secondary-container` with a check icon.
|
||||
That is the `OutlinedSegmentedButtonTokens` design (40dp, `CornerFull`, 1dp outline, label-large),
|
||||
point-for-point. Its own header comment says so: "picker: M3's segmented buttons for the three
|
||||
choices". Meanwhile the library ships the successor twice over:
|
||||
`resources/views/components/group.blade.php:1-2` ("An M3 Expressive connected button group — the
|
||||
successor of the segmented button", native radios, arrow-key movement, selected segment rounds fully)
|
||||
and `resources/views/components/button-group.blade.php` (`connected`). The showcase puts the deprecated
|
||||
control in its own chrome at `showcase/layout.blade.php:148`.
|
||||
- Fix: re-implement `mode="picker"` on top of `<x-group>` with
|
||||
`:options="[['id' => 'light', 'name' => __('Light'), 'icon' => 'light_mode'], …]"` and an `x-model`-ish
|
||||
binding onto `$store.theme.choice` (a small `x-data` wrapper writing `$store.theme.set()` on change). The
|
||||
selected-segment shape morph, the radiogroup semantics and the arrow keys then come for free — which
|
||||
also resolves N-17.
|
||||
- Effort: M
|
||||
- Breaks API? no (the `mode="picker"` prop name and the `data-theme-toggle="picker"` /
|
||||
`data-theme-option` hooks can be kept; the internal markup changes, so any app CSS targeting the inner
|
||||
buttons would move)
|
||||
|
||||
### N-05 · tabs · every panel is visible until Alpine initialises
|
||||
- Severity: must-fix
|
||||
- M3 says: tabs show one destination at a time; the active indicator plus a single visible panel is the
|
||||
whole mechanism (RCB §Tabs/Anatomy, /Behaviour). Exposing all panels also breaks the ARIA tabs
|
||||
contract — only the panel for the selected tab may be rendered/visible.
|
||||
- Library does: `resources/views/components/tab.blade.php:10` hides the panel with `x-show` only, and
|
||||
there is no `x-cloak` or server-side `hidden`/`style="display:none"`. `[x-cloak]{display:none!important}`
|
||||
*is* defined (`resources/css/tokens/state.css:68-70`) but unused here. Until Alpine boots, all panels
|
||||
stack vertically (the showcase's 4-tab example renders all four bodies), and a screen reader reaching
|
||||
the page early sees four `role="tabpanel"` regions at once. The same line binds `x-bind:id`, so
|
||||
`aria-controls="…-panel"` on every tab button (`tabs.blade.php:65`) dangles until Alpine runs.
|
||||
- Fix: in `tab.blade.php`, render the id server-side the way the tab buttons already do (pass the `$id`
|
||||
down, or reuse `tabsId` via a `@aware`/slot prop) and add `x-cloak` plus a server-rendered
|
||||
`@if ($name !== $initial) style="display: none" @endif` — or simplest: keep `x-show`, add `x-cloak`, and
|
||||
add a CSS rule that un-cloaks the initially selected panel. Any of these removes both the flash and the
|
||||
dangling `aria-controls`.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### N-06 · app-shell, navigation-rail · `lg` (1024px) stands in for M3's expanded breakpoint (840dp)
|
||||
- Severity: should-fix
|
||||
- M3 says: breakpoints are Compact <600, Medium 600–839, **Expanded 840–1199**, Large 1200–1599,
|
||||
Extra-large ≥1600 (RF §Layout/Breakpoints). For Expanded the recommended navigation is "**Modal or
|
||||
standard expanded navigation rail**", and the swap table gives Expanded a "Standard expanded navigation
|
||||
rail" (RF §Layout/Breakpoints tables). The rail page: "Expanded → extra-large → use a rail (never a
|
||||
bar); choose Standard vs. Modal based on space and destination count" (RCB §Navigation Rail/Behaviour).
|
||||
- Library does: the threshold is Tailwind's `lg` = 64rem = **1024px**, in three places that must agree —
|
||||
`resources/js/navigation.js:16` (`const WIDE = '(min-width: 64rem)'`), `resources/css/components/navigation.css:51`
|
||||
and `:57` (the `rail-collapsed` variant) and `:184-188` (the adaptive rail's 96px layout width). Below
|
||||
it, `materialNavigationRail.modal` is true (`navigation.js:116-118`) and `expanded` ignores
|
||||
`$store.rail.collapsed` (`:124-130`).
|
||||
**What a user sees at 840–1023px** (a landscape tablet, a half-screen desktop window, a 12.9" iPad):
|
||||
a 96px collapsed rail; pressing its menu button opens the expanded rail *as a modal over a scrim* with a
|
||||
focus trap; `document.addEventListener('livewire:navigating', … hide())` (`navigation.js:86`) closes it
|
||||
on every destination click, so the rail must be re-opened for each navigation; and the remembered
|
||||
`$store.rail` = expanded preference is silently ignored. M3 lists a standard (in-layout) expanded rail
|
||||
for this whole band.
|
||||
- Fix: change `WIDE` to `'(min-width: 52.5rem)'` (840px) and the three `64rem` media queries in
|
||||
`navigation.css` to `52.5rem`. If 840px feels too narrow for a 256px rail beside content, expose the
|
||||
threshold instead of hard-coding it: a `--material-rail-expanded-at` custom property or a `wide-at` prop
|
||||
on `<x-app-shell>`/`<x-navigation-rail mode="adaptive">`, defaulting to 840px. Update SKILL.md:665 and
|
||||
:735, which both say "to `lg`" / "from `lg`".
|
||||
- Effort: M
|
||||
- Breaks API? no (unless an app relies on the 1024px switch point visually)
|
||||
|
||||
### N-07 · app-shell, navigation-bar · `sm` (640px) stands in for M3's compact/medium boundary (600dp)
|
||||
- Severity: should-fix
|
||||
- M3 says: Compact is "Under 600dp"; Medium is "600–839dp" (RF §Layout/Breakpoints). The navigation bar
|
||||
is for "compact (<600dp) and medium (600–839dp)" (RCB §Navigation Bar/Behaviour); at Medium the swap
|
||||
table gives "Collapsed navigation rail" as the primary navigation (RF §Layout/Breakpoints).
|
||||
- Library does: two different numbers for the same boundary. The **bar's own item layout** switches at the
|
||||
right place — `@container (width >= 37.5rem)` = 600px (`navigation.css:123`, `:465`, `:519`, `:525`,
|
||||
`:555`, `:562`), correct and matching `ShortNavigationBarArrangement.Centered`. The **shell**, however,
|
||||
switches at `sm` = 40rem = 640px: `app-shell.blade.php:142` (`sm:hidden` on the bar wrapper), `:87`
|
||||
(`sm:flex`), `:88` and `:136` (the `--material-bottom-bar` padding), and `navigation.css:190-194` /
|
||||
`:235-279` (the adaptive rail is width 0 and `display:none` below 40rem).
|
||||
**What a user sees at 600–639px**: the navigation bar is still the only navigation, but it has already
|
||||
flipped to the medium horizontal (icon-beside-label, centred) configuration — so the Expressive medium
|
||||
bar exists in a 40px-wide window band and then disappears entirely at 640px, where the collapsed rail
|
||||
takes over. The two switch points should be the same number.
|
||||
- Fix: replace the `sm:`/`max-sm:` utilities in `app-shell.blade.php` (lines 87, 88, 136, 142) with
|
||||
arbitrary variants at 600px (`max-[37.5rem]:` / `min-[37.5rem]:`), or define a `compact`/`medium`
|
||||
custom variant in the theme so the whole package has one name for 600px; change the two `40rem` media
|
||||
queries in `navigation.css` to `37.5rem`. Also update the documented wrapper in
|
||||
`navigation-bar.blade.php:4` and SKILL.md:700-707, which teach `sm:hidden`.
|
||||
- Effort: M
|
||||
- Breaks API? no (the documented `sm:hidden` wrapper idiom changes in the docs)
|
||||
|
||||
### N-08 · navigation-bar-item · the horizontal (medium) item label is label-large, not label-medium
|
||||
- Severity: should-fix
|
||||
- M3 says: `NavigationBarTokens.LabelTextFont = TypographyKeyTokens.LabelMedium`
|
||||
(tok/NavigationBarTokens.kt). There is no per-layout label font: `NavigationBarHorizontalItemTokens`
|
||||
defines only `ActiveIndicatorHeight`/`LeadingSpace`/`TrailingSpace`/`IconSize`, and Compose's
|
||||
`ShortNavigationBarItem` passes `labelTextStyle = NavigationBarTokens.LabelTextFont.value` for *both*
|
||||
icon positions (androidx `ShortNavigationBar.kt:220`, fetched from
|
||||
https://raw.githubusercontent.com/androidx/androidx/androidx-main/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ShortNavigationBar.kt).
|
||||
- Library does: `resources/css/components/navigation.css:147-148` sets
|
||||
`font: var(--md-sys-typescale-label-lg)` inside `@container (width >= 37.5rem)`. (The rail's horizontal
|
||||
item *is* label-large — `NavigationRailHorizontalItemTokens.LabelTextFont = LabelLarge` — which is
|
||||
probably where this came from; the nav bar has no such token.)
|
||||
- Fix: `navigation.css:147-148` → keep `label-md` (delete the two lines; the base `[data-navigation-pill]`
|
||||
rule at `:105-106` already sets label-medium).
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### N-09 · app-bar · the search variant's container does not follow the 312dp / 50% growth rule
|
||||
- Severity: should-fix
|
||||
- M3 says: "The search container of the search app bar should fill 100% of the space between leading and
|
||||
trailing app bar elements **until it reaches 312dp. Then, it should only grow further to fill 50% of
|
||||
that space**" (raw/components_app-bars_guidelines.md:454; RCB §Top App Bar/Behaviour).
|
||||
- Library does: `resources/css/components/app-bar.css:133-137` — `[data-app-bar-search] { flex: 1 1 0%; }`,
|
||||
i.e. 100% of the available space at every width. On a 1600px window the search bar spans ~1500px. The
|
||||
showcase works around it by hand (`showcase/layout.blade.php:60`, `class="mx-auto w-full max-w-2xl"`),
|
||||
which is evidence the component's own behaviour is wrong, and `max-w-2xl` (672px) is not the M3 curve
|
||||
either.
|
||||
- Fix: in `app-bar.css`, give the search slot the M3 curve:
|
||||
`[data-app-bar-search] { flex: 1 1 0%; max-width: calc(19.5rem + (100% - 19.5rem) / 2); margin-inline: auto; }`
|
||||
(19.5rem = 312px; below 312px `max-width` never binds so it still fills 100%). Then drop the manual
|
||||
`max-w-2xl` in the showcase layout.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### N-10 · tabs · scrollable tabs have no 52dp leading offset
|
||||
- Severity: should-fix
|
||||
- M3 says: "When using scrollable tabs, the first visible tab should be offset by **52dp** from the left
|
||||
side of the device for both web and mobile"; "Do — Offset the first scrollable tab 52dp from the leading
|
||||
edge so it's clear that more content is available" (raw/components_tabs_guidelines.md:150, :157; RCB
|
||||
§Tabs/Variants).
|
||||
- Library does: `resources/css/components/tabs.css:50-52` only sets `flex: none` on the tabs when
|
||||
`[data-scrollable]` is present; there is no leading padding anywhere on `[data-tabs-bar]`. The showcase's
|
||||
scrollable example (`showcase/sections/bars.blade.php:101-105`) starts "January" flush against the
|
||||
container edge, so nothing signals that the set scrolls.
|
||||
- Fix: `tabs.css`, add
|
||||
`[data-tabs-bar][data-scrollable] { padding-inline-start: 3.25rem; }` (52px) — logical property, so RTL
|
||||
mirrors for free.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### N-11 · app-bar · the centred headline uses fixed 56px insets and collides with two trailing actions
|
||||
- Severity: should-fix
|
||||
- M3 says: the centred configuration is the Expressive replacement for the center-aligned bar ("Merged
|
||||
into **small**. Use centered-text configuration.", RCB §Top App Bar/Status), and trailing elements are
|
||||
"up to 2 icon buttons after headline" (RCB §Top App Bar/Behaviour). The headline must not be truncated
|
||||
(raw/components_app-bars_guidelines.md:337).
|
||||
- Library does: `resources/css/components/app-bar.css:122-130` absolutely positions the centred headline
|
||||
with `inset-inline: 3.5rem` — a hard-coded 56px on each side, enough for exactly one 48px icon button.
|
||||
With the M3-sanctioned two trailing buttons (≈96px plus the 4px row padding), the headline box extends
|
||||
~40px underneath them; a long title ellipsises *behind* the buttons instead of before them. Nothing in
|
||||
the code adapts the inset to what the `navigation`/`actions` slots actually contain.
|
||||
- Fix: replace the absolute positioning with a three-column grid on the row for the centred variant —
|
||||
`[data-app-bar][data-variant="center"] [data-app-bar-row] { display: grid; grid-template-columns: 1fr auto 1fr; }`
|
||||
with the leading slot in column 1, the headline (centred, `justify-self: center`, `min-width: 0`) in
|
||||
column 2 and the trailing slot in column 3 (`justify-self: end`) — the headline then shrinks against
|
||||
whatever is actually beside it. Failing that, measure the two slots in `app-bar.js` (which already runs
|
||||
a `ResizeObserver`) and publish `--app-bar-lead`/`--app-bar-trail`.
|
||||
- Effort: M
|
||||
- Breaks API? no
|
||||
|
||||
### N-12 · toolbar · a vertical floating toolbar placed at `end` uses a 16px margin, M3 wants ≥24dp
|
||||
- Severity: should-fix
|
||||
- M3 says: "Horizontal toolbars should have a minimum 16dp margin from the edge of the window";
|
||||
"**Vertical toolbars should have a minimum 24dp margin**" / "Maintain at least a 24dp margin for
|
||||
vertical toolbars" (raw/components_toolbars_guidelines.md:334, :346, :350; RCB §Toolbars/Specs).
|
||||
- Library does: `resources/css/components/toolbar.css:83-89` —
|
||||
`[data-toolbar-place="end"] { inset-inline-end: calc(1rem + var(--material-safe-right, …)); }`, 16px for
|
||||
every toolbar placed at the end edge, and `place="end"` is documented as the placement "for a vertical
|
||||
one" (`toolbar.blade.php:6`).
|
||||
- Fix: `toolbar.css`, split the rule:
|
||||
`[data-toolbar][data-vertical][data-toolbar-place="end"], [data-toolbar-group][data-vertical][data-toolbar-place="end"] { inset-inline-end: calc(1.5rem + var(--material-safe-right, env(safe-area-inset-right))); }`
|
||||
(24px) while horizontal placements keep 16px.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### N-13 · toolbar · standard (non-filled, non-tonal) buttons inside a standard toolbar are on-surface-variant, M3 says Primary
|
||||
- Severity: should-fix (with a caveat — see both readings)
|
||||
- M3 says: standard toolbar colour list, verbatim: "1. Surface container / 2. Filled button (Primary, On
|
||||
primary) / 3. Toggle tonal button (Secondary container, On secondary container) / **4. Standard button
|
||||
(Primary)**" (raw/components_toolbars_specs.md:228-234; RCB §Toolbars/Specs). The vibrant list gives
|
||||
"Standard button (On primary container)" (:253) — which the library *does* honour.
|
||||
- Library does: `resources/css/components/toolbar.css:49-61` recolours buttons only in the vibrant case.
|
||||
In a standard toolbar the icon buttons keep the library's default icon-button ink,
|
||||
`text-on-surface-variant` (`resources/views/components/button.blade.php:116` `$quietInk`, :123 for
|
||||
icon-only text buttons) — the container's own `color: var(--md-sys-color-on-surface-variant)`
|
||||
(`toolbar.css:28`, `:37`) cannot reach them because they carry a utility.
|
||||
**The other reading**: M3's own icon-button spec gives an unselected standard icon button
|
||||
`on-surface-variant`, and the toolbar colour list is annotated from an image showing text/standard
|
||||
buttons rather than icon buttons. If the library treats the toolbar list as applying to *label* buttons
|
||||
only, its current choice is defensible. I lean to the finding because the same list's vibrant row *is*
|
||||
implemented, so the two rows are being read inconsistently.
|
||||
- Fix: if adopting the spec, add next to the vibrant rules (same unlayered block):
|
||||
`[data-toolbar]:not([data-vibrant]) [data-icon-button]:not([aria-pressed="true"]) { color: var(--md-sys-color-primary); }`.
|
||||
If keeping the current behaviour, say so in `toolbar.css`'s header comment with the reason, so the next
|
||||
reader does not re-litigate it.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### N-14 · app-shell · the app bar sits inside the content pane, beside the rail, not across the window
|
||||
- Severity: should-fix (structural; see both readings)
|
||||
- M3 says: the scaffold is bars → rails → panes, in that nesting order. "Bars can frame the page";
|
||||
"**Rails create the perimeter space surrounding panes**"
|
||||
(raw/foundations_layout_scaffold_overview.md:54-58); "Rails are the next level in layout **after bars**,
|
||||
filling the perimeter space surrounding panes"; "Rails occupy the spaces immediately adjacent to bars"
|
||||
(raw/foundations_layout_scaffold_rails.md:56, :64). And for the bar itself: "Container always spans 100%
|
||||
of window width at its default height" (RCB §Top App Bar/Behaviour).
|
||||
- Library does: `resources/views/components/app-shell.blade.php:97-139` puts `<x-navigation-rail>` as a
|
||||
flex sibling *before* a column that contains `{{ $top }}` and `<main>`. From `sm` the app bar therefore
|
||||
starts 96px (or 256px) in from the window's leading edge and is a pane-level bar, not a page-level one.
|
||||
There is no slot that renders above the rail, so an application cannot opt into the M3 arrangement
|
||||
without abandoning `<x-app-shell>`.
|
||||
**The other reading**: `NavigationSuiteScaffold` in Compose nests the whole content — top bar
|
||||
included — beside the rail, and M3's canonical-layout imagery shows both arrangements. If the library
|
||||
regards the app bar as a pane bar, the current structure is consistent; it is still worth a documented
|
||||
decision rather than an accident of markup order.
|
||||
- Fix: add a `window-top` (or `banner`) slot to `<x-app-shell>` rendered as the first child of
|
||||
`[data-app-shell]` with the shell root becoming `flex flex-col` and the rail+content row nested inside,
|
||||
so an application can choose either. Document which slot means which in SKILL.md:663-695.
|
||||
- Effort: M
|
||||
- Breaks API? no (additive slot)
|
||||
|
||||
### N-15 · account-menu · the avatar trigger has no state layer (no hover or pressed state)
|
||||
- Severity: should-fix
|
||||
- M3 says: the six states are Enabled, Disabled, Hover, Focused, Pressed, Dragged, and "States have two
|
||||
visual indicators to ensure accessibility"; the state layer's colour equals the content's "on" colour,
|
||||
at 8% hover / 10% focus / 10% pressed (RF §Interaction/States).
|
||||
- Library does: `resources/views/components/account-menu.blade.php:41` —
|
||||
`focus-ring inline-flex size-10 … rounded-corner-full bg-primary-container … text-on-primary-container`.
|
||||
It has the focus ring but no `state-layer`, so there is no hover and no pressed feedback at all. Every
|
||||
comparable trigger in the library (the rail menu button at `navigation-rail.blade.php:108`, the theme
|
||||
toggle at `theme-toggle.blade.php:65`, every `<x-button>`) carries `state-layer`.
|
||||
- Fix: add `state-layer` to the class list. Note the ordering constraint from
|
||||
`resources/css/tokens/state.css:13-27` (the utility sets `position: relative; isolation: isolate` and
|
||||
paints a `::before` at `z-index: -1`) — with `overflow-hidden` removed per N-01 this composes cleanly.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### N-16 · section-nav · secondary tabs wrapped onto a 3/4-column grid break the tab bar's divider and indicator
|
||||
- Severity: should-fix
|
||||
- M3 says: "Container always spans full width, divided into equal sections; **bottom-edge divider
|
||||
separates it from content below**" (RCB §Tabs/Behaviour). M3's stated answer to "too many tabs" is
|
||||
scrollable tabs — "scrollable allows longer labels and more tabs" (RCB §Tabs/Variants) — and its
|
||||
accessibility page explicitly blesses them: "Horizontal scrolling tabs meet accessibility requirements
|
||||
because they need to increase in width to respond to label text without affecting the layout"
|
||||
(raw/components_tabs_accessibility.md:95).
|
||||
- Library does: `resources/views/components/section-nav.blade.php:31-35, :60` reuses
|
||||
`[data-tabs-bar][data-variant="secondary"]` but applies `sm:grid sm:grid-cols-3` / `sm:grid-cols-4`, so
|
||||
from 5 items the "tab bar" becomes two or more rows. `tabs.css:19` puts the 1px outline-variant divider
|
||||
on the *bar*, so only the last row gets one and the upper rows' 2px active indicators float in mid-air
|
||||
against nothing. `[data-tab] { min-width: 5.625rem }` (`tabs.css:36`) also fights `grid-cols-4` in a
|
||||
narrow column.
|
||||
- Fix: either keep `sm:flex` with `[data-scrollable]` semantics (`flex: none` + the 52dp offset from
|
||||
N-10), which is what M3 prescribes and what the library already implements for `<x-tabs scrollable>`;
|
||||
or, if the grid is kept deliberately, move the divider to each row
|
||||
(`[data-section-nav] [data-tabs-bar] > li { border-bottom: 1px solid var(--md-sys-color-outline-variant); }`
|
||||
with the bar's own border removed) and drop the `min-width` inside the grid. See also "Deliberate
|
||||
deviations" — the stated reason for the grid does not survive M3's text.
|
||||
- Effort: M
|
||||
- Breaks API? no
|
||||
|
||||
### N-17 · theme-toggle · the picker's radiogroup keyboard handling is incomplete
|
||||
- Severity: should-fix
|
||||
- M3 says: a single-select segmented control's role is "Radiogroup"
|
||||
(`docs/reference/m3/reference-components-a.md:392`); RF §Interaction/Inputs and the per-component
|
||||
accessibility pages defer to the APG pattern, in which a radio group handles Up/Down as well as
|
||||
Left/Right and wraps from last to first.
|
||||
- Library does: `resources/views/components/theme-toggle.blade.php:38-39` handles only
|
||||
`keydown.arrow-right` / `keydown.arrow-left`, via `$el.nextElementSibling` / `previousElementSibling`,
|
||||
so there is **no wrap** (Right on "System" and Left on "Light" do nothing), **no Up/Down**, and **no
|
||||
Home/End**. `aria-checked="false"` is also rendered on all three server-side (`:33`), so before Alpine
|
||||
boots the group reports nothing checked.
|
||||
- Fix: fixed by N-04 — `<x-group>` uses native radios in a fieldset, where the browser supplies wrap,
|
||||
both axes and the roving tab stop. If N-04 is not taken, add `arrow-down`/`arrow-up`, wrap with
|
||||
`nextElementSibling ?? firstElementChild`, and render the initial `aria-checked` from the server
|
||||
default.
|
||||
- Effort: S (as part of N-04)
|
||||
- Breaks API? no
|
||||
|
||||
### N-18 · tabs · the primary active indicator is missing its 2dp side inset
|
||||
- Severity: should-fix
|
||||
- M3 says: "Primary tab active indicators are additionally **inset 2dp on each side**" (RCB §Tabs/Specs,
|
||||
from tabs/specs page text); "Active indicator minimum length 24dp".
|
||||
- Library does: `resources/css/components/tabs.css:119-129` — `inset-inline: 0` on
|
||||
`[data-tab-indicator]`, which for a primary tab is scoped to `[data-tab-content]`
|
||||
(`min-width: 1.5rem` = the 24dp minimum ✓, `tabs.css:108`). The 3px height, the `3px 3px 0 0` corners
|
||||
and the 2px/square secondary variant are all correct; only the inset is absent.
|
||||
- Fix: `tabs.css`, in the primary case: `[data-tabs-bar]:not([data-variant="secondary"]) [data-tab-indicator] { inset-inline: 2px; }`.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### N-19 · navigation-bar-item · the state layer uses on-surface where the rail uses on-secondary-container
|
||||
- Severity: should-fix
|
||||
- M3 says: "The state layer's color equals the content's **'on' color**" (RF §Interaction/States,
|
||||
state-layer mechanics). For navigation items androidx is explicit and uses one colour for active *and*
|
||||
inactive: `ItemActiveHoveredStateLayer = ItemActiveFocusedStateLayer = ItemActivePressedStateLayer =
|
||||
ItemInactiveHoveredStateLayer = ItemInactiveFocusedStateLayer = ItemInactivePressedStateLayer =
|
||||
OnSecondaryContainer` (tok/NavigationRailColorTokens.kt). `NavigationBarTokens.kt` ships no state-layer
|
||||
tokens, so the rail's are the only stated ones for a navigation item.
|
||||
- Library does: two different answers for the same kind of item. The rail is right —
|
||||
`navigation.css:544-552` paints both `::before` layers with
|
||||
`var(--md-sys-color-on-secondary-container)`. The navigation bar is not: `navigation.css:504-517` leaves
|
||||
`background-color: var(--md-sys-color-on-surface)` for `[data-navigation-bar-item]`'s indicator and pill
|
||||
layers. On an active bar item the pill is secondary-container, so the hover layer is an on-surface wash
|
||||
over secondary-container rather than the on-secondary-container the content uses.
|
||||
- Fix: `navigation.css`, add `[data-navigation-bar-item] :is([data-navigation-indicator],
|
||||
[data-navigation-pill])::before { background-color: var(--md-sys-color-on-secondary-container); }` after
|
||||
the shared rule, matching the rail's treatment two blocks below.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### N-20 · tabs · the focus ring is drawn inside the tab (`outline-offset: -3px`) instead of M3's 2px out
|
||||
- Severity: nice-to-have
|
||||
- M3 says: Foundations calls the indicator only "ring-like" and gives no number (RF §Interaction/States:
|
||||
"No thickness, offset, or color token is specified anywhere in Foundations"), but the library has picked
|
||||
one convention for the whole package — `focus-ring` = 3px secondary at `outline-offset: 2px`
|
||||
(`resources/css/tokens/state.css:46-53`), and the navigation bar/rail items follow it
|
||||
(`navigation.css:554-583`).
|
||||
- Library does: `resources/css/components/tabs.css:81-85` uses `outline-offset: -3px`, drawing the ring
|
||||
inside the tab's own box. (The likely reason is `overflow-x: auto` on the bar clipping an outward ring;
|
||||
the file says nothing about it.)
|
||||
- Fix: either keep it and record the reason in `tabs.css`'s header comment, or give the bar
|
||||
`padding-block: 3px` / `scroll-padding-inline: 3px` room and switch to `outline-offset: 2px` so tabs
|
||||
match every other focusable thing in the library.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### N-21 · tabs · `aria-current="page"` gets the indicator but not the active label colour in the primary variant
|
||||
- Severity: nice-to-have
|
||||
- M3 says: primary tabs' active label is Primary; secondary tabs' is On surface (RCB §Tabs/Specs).
|
||||
- Library does: `resources/css/components/tabs.css:87-93` — the active colour rule for the primary variant
|
||||
matches only `[aria-selected="true"]`, while the secondary rule matches
|
||||
`:is([aria-selected="true"], [aria-current="page"])` and the indicator rule (`:141`) matches both. So a
|
||||
link-based tab bar (the pattern `section-nav` uses, `section-nav.blade.php:65-70`) built with
|
||||
`variant="primary"` would show the indicator but keep the label on-surface-variant.
|
||||
- Fix: `tabs.css:87` → `[data-tab]:is([aria-selected="true"], [aria-current="page"])`.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
### N-22 · navigation-rail · no vertical divider and no way to turn the container fill off
|
||||
- Severity: nice-to-have
|
||||
- M3 says: "Optional vertical divider separates rail from content, placed on the content-adjacent edge";
|
||||
"container fill can be turned off (transparent) as long as items keep ≥3:1 contrast"; "If a layout
|
||||
scrolls horizontally, the rail can scroll off-screen or remain fixed. To distinguish that content is
|
||||
scrolling underneath the rail, use a divider or add elevation to the rail"
|
||||
(RCB §Navigation Rail/Behaviour; raw/components_navigation-rail_guidelines.md:456).
|
||||
- Library does: `navigation.css:196-215` always paints `background-color: var(--md-sys-color-surface)` and
|
||||
draws no divider; there is no prop for either (`navigation-rail.blade.php:60-65` has only `mode`,
|
||||
`label`, `width`, `menu`).
|
||||
- Fix: add a `divider` boolean prop rendering `border-inline-end: 1px solid
|
||||
var(--md-sys-color-outline-variant)` on `[data-navigation-rail-panel]`, and a `fill` prop (default
|
||||
`true`) that sets `background-color: transparent` when false.
|
||||
- Effort: S
|
||||
- Breaks API? no (additive props)
|
||||
|
||||
### N-23 · navigation-rail · the FAB → extended FAB transition is a display swap, not a morph
|
||||
- Severity: nice-to-have
|
||||
- M3 says: "Rail transitions from collapsed↔expanded should animate the FAB into/out of an Extended FAB"
|
||||
(RCB §Navigation Rail/Behaviour).
|
||||
- Library does: the documented pattern is two FABs swapped by `display`
|
||||
(`navigation-rail.blade.php:42-43`, SKILL.md:737, `showcase/shell.blade.php:48-49`) — the collapsed one
|
||||
appears at once while the rail's width is still springing. The rail's own width *is* animated on the
|
||||
spatial spring (`navigation.css:171`), so the mismatch is visible.
|
||||
- Fix: a single `<x-fab>` whose label is width-animated (`grid-template-columns: 0fr → 1fr` on the label
|
||||
span, `ease-spatial-default`) inside the `rail-collapsed` variant would morph instead of swap; or at
|
||||
minimum mention the limitation in SKILL.md.
|
||||
- Effort: M
|
||||
- Breaks API? no
|
||||
|
||||
### N-24 · navigation-rail · `mode="collapsible"` has no width floor of its own
|
||||
- Severity: nice-to-have
|
||||
- M3 says: "Compact → don't use a standard rail, use a navigation bar" (RCB §Navigation Rail/Behaviour).
|
||||
- Library does: only `mode="adaptive"` is width-aware (`navigation.css:184-194`). A `collapsible` or
|
||||
`expanded` rail keeps `clamp(13.75rem, 16rem, 22.5rem)` at 360px too — two-thirds of a phone screen.
|
||||
The docs point applications at `<x-app-shell>` for adaptivity, and the showcase only uses `collapsible`
|
||||
inside a bounded demo box, so nothing in-repo hits it; an application wiring a bare
|
||||
`<x-navigation-rail mode="collapsible">` into a page layout will.
|
||||
- Fix: a one-line guard in `navigation.css` —
|
||||
`@media (width < 37.5rem) { [data-navigation-rail='collapsible'], [data-navigation-rail='expanded'] { width: 6rem; } }`
|
||||
plus the matching `rail-collapsed` variant arm — or an explicit sentence in SKILL.md:735 that these two
|
||||
modes are fixed-width by design and belong behind a `min-[37.5rem]:` wrapper.
|
||||
- Effort: S
|
||||
- Breaks API? no
|
||||
|
||||
## Deliberate deviations
|
||||
|
||||
- **Expanded rail items are full-width pills, not label-hugging** — `navigation-rail-item.blade.php:18-19`:
|
||||
"Compose's expanded item hugs its label; the package draws the full-width pill, which leaves room for
|
||||
the count at the end." **Holds up.** M3 says exactly this is allowed: the indicator "hugs the label text
|
||||
by default (**can be overridden to fill the full container width**, resembling the old navigation drawer
|
||||
style); target area always spans the full rail width" (RCB §Navigation Rail/Behaviour).
|
||||
- **No drop shadow on a scrolled app bar, colour change only** — `app-bar.css:12`, `:59-61`. **Holds up,
|
||||
and is the right reading.** androidx has `AppBarTokens.OnScrollContainerElevation = Level2`, but the
|
||||
site is explicit: "On scroll: **No drop shadow**, instead a color fill creates separation from content"
|
||||
(raw/components_app-bars_overview.md:137).
|
||||
- **Medium/large flexible height keyed on whether a subtitle is present** — `app-bar.css:33-47` maps
|
||||
medium 112/136 and large 120/152 to without/with subtitle. **Holds up**: those are exactly
|
||||
`ContainerHeight`/`LargeContainerHeight` in tok/AppBarMediumFlexibleTokens.kt and
|
||||
tok/AppBarLargeFlexibleTokens.kt, and Compose's flexible app bars pick the taller one when a subtitle is
|
||||
supplied.
|
||||
- **Rail container is `surface`, modal is `surface-container`** — `navigation.css:210`, `:229`. **Holds
|
||||
up**: tok/NavigationRailCollapsedTokens.kt `ContainerColor = Surface`;
|
||||
tok/NavigationRailExpandedTokens.kt `ModalContainerColor = SurfaceContainer`, `ModalContainerElevation =
|
||||
Level2`, `ModalContainerShape = CornerLarge` — all three reproduced exactly. (The site's colour list
|
||||
says "Surface container (optional container fill)"; androidx is the more specific source and the library
|
||||
follows it.)
|
||||
- **Every toolbar control stays in the Tab order rather than a roving tabindex** — `toolbar.js:4-5`: "Every
|
||||
control stays in the Tab order, so a control added by a Livewire render is reachable without any
|
||||
bookkeeping." **Holds up against M3** (though not against APG's toolbar pattern): M3's own accessibility
|
||||
page says "Initial focus lands on the first interactive element; **Tab (or Arrows) moves between all
|
||||
others**" (RCB §Toolbars/Accessibility).
|
||||
- **Section nav wraps onto a grid instead of scrolling** — `section-nav.blade.php:15-16`: "tabs that scroll
|
||||
hid the last sections on a tablet." **Does not hold up.** M3 prescribes scrollable tabs for exactly this
|
||||
case and its accessibility page defends them explicitly ("Horizontal scrolling tabs meet accessibility
|
||||
requirements…", raw/components_tabs_accessibility.md:95), while the grid breaks the tab bar's divider
|
||||
and strands the indicators (N-16). If the grid is kept for product reasons, it should stop presenting
|
||||
itself as `[data-tabs-bar]`.
|
||||
- **Small app-bar titles truncate with an ellipsis** — `app-bar.css:104-110`. M3 says "Don't truncate the
|
||||
headline text" *and* "Don't wrap text in a small app bar"
|
||||
(raw/components_app-bars_guidelines.md:337, :347); its actual answer is "If headline text is long, use a
|
||||
medium flexible or large flexible app bar" (:342). Truncation is the only remaining web-sane behaviour,
|
||||
so this is a reasonable forced choice — but it is undocumented. Worth one line in the header comment
|
||||
pointing authors at `variant="medium"`.
|
||||
|
||||
## Aligned
|
||||
|
||||
- **App bar**: 64 / 112 / 136 / 120 / 152px heights; title-large + label-medium (small), headline-medium +
|
||||
label-large (medium flexible), display-small + title-medium (large flexible); 4px leading/trailing space;
|
||||
0 gap between icon buttons; 16px title inset without a leading button; surface → surface-container on
|
||||
scroll; on-surface title / on-surface-variant subtitle and trailing icons / on-surface leading icon;
|
||||
square container. The pure-CSS collapse (negative sticky `top` + sticky inner row) is elegant and
|
||||
produces no layout shift; the flexible variants' row headline is `aria-hidden` so the real `<h1>` is
|
||||
never announced twice.
|
||||
- **Navigation bar**: 64px, surface-container, square, items 0 apart, 56×32 `CornerFull` indicator, 4px
|
||||
icon–label space, 6px item vertical padding, 40px horizontal indicator with 16px leading/trailing,
|
||||
24px icons, on-secondary-container active icon, secondary-container indicator, secondary active label
|
||||
(vertical) / on-secondary-container (horizontal), on-surface-variant inactive — all correct. The medium
|
||||
arrangement reproduces `calculateCenteredContentHorizontalPadding` (items occupy `10 × (count + 3)`% of
|
||||
the bar) exactly, and it switches on a **container** query at 600px, so a bar in a narrow column keeps
|
||||
the compact layout.
|
||||
- **Navigation rail**: 96px collapsed, `clamp(220px, width, 360px)` expanded, 44px top space, 40px header
|
||||
space (32 + 8), 4px between collapsed items, 64px collapsed item, 56px expanded item, 8px/4px icon–label
|
||||
spaces, 56×32 collapsed indicator, `CornerFull`. Modal = fixed, surface-container, elevation 2,
|
||||
`CornerLarge` on the inner edge, 32% scrim, `x-trap.inert.noscroll`, Escape, scrim click, focus returned
|
||||
to the menu button, closed on `livewire:navigating`. Badges move from the icon (collapsed) to the label
|
||||
end (expanded), which is M3's stated rule. State layers use on-secondary-container per
|
||||
NavigationRailColorTokens.
|
||||
- **Catalogue vs. Expressive**: no bottom app bar, no navigation drawer, no baseline medium/large app bar,
|
||||
no baseline navigation bar — all four correctly skipped, and the expanded rail (standard + modal) is
|
||||
what stands in for the drawer, exactly as M3 Expressive directs.
|
||||
- **Tabs**: 48px (64px stacked), 24px icons, 1px outline-variant divider, 3px primary indicator with
|
||||
`3px 3px 0 0` corners, 2px square secondary indicator, 24px minimum indicator length, 8px icon–label gap,
|
||||
title-small labels, primary/on-surface active colours, 8%/10% state layers, 38% disabled. Real tablist
|
||||
semantics, arrow keys + Home/End, disabled tabs skipped, `aria-selected`/`tabindex` rendered on the
|
||||
server, and the indicator moves in a view transition on the spatial spring with a reduced-motion escape.
|
||||
- **Toolbars**: docked 64px / square / surface-container / 16px ends / 4–32px between items (as a
|
||||
`clamp`); floating 64px / `CornerFull` / surface-container / 8px ends / 4px between; vibrant =
|
||||
primary-container with on-primary-container ink and a surface-container + on-surface selected button —
|
||||
all matching tok/DockedToolbarTokens.kt and tok/FloatingToolbarTokens.kt. `role="toolbar"` +
|
||||
`aria-orientation` + RTL-aware arrow keys.
|
||||
- **Motion**: all durations collapse to 0 under `prefers-reduced-motion`
|
||||
(`resources/css/tokens/motion.css:63-75`); spatial springs for size/position, effects easing for colour
|
||||
and opacity, throughout this group.
|
||||
- **Safe areas**: the app bar pads the top inset, the navigation bar and rail the bottom inset, the docked
|
||||
toolbar the bottom inset — matching M3's safety-region rule.
|
||||
- **Links vs. buttons**: navigation items are real `<a>`s with `aria-current="page"` (plus the filled icon
|
||||
and the indicator — the two-indicator rule) and degrade to `<button>` without a `link`; the section nav
|
||||
uses links with `aria-current`, not fake tabs.
|
||||
|
||||
## Missing
|
||||
|
||||
M3 variants/configurations in this group with no implementation at all:
|
||||
|
||||
- **Navigation bar, tall container (80dp)** — `NavigationBarTokens.TallContainerHeight = 80.0.dp`
|
||||
(tok/NavigationBarTokens.kt). Only the 64px short bar exists (`navigation.css:76`); no prop selects the
|
||||
tall one.
|
||||
- **Navigation bar, hide-on-scroll** — "Scrolling: hides on scroll-down, reappears on scroll-up — but never
|
||||
hide it while a screen reader is active" (RCB §Navigation Bar/Behaviour). Not implemented and not
|
||||
mentioned.
|
||||
- **Navigation rail, narrow collapsed width (80dp)** — `NavigationRailCollapsedTokens.NarrowContainerWidth
|
||||
= 80.0.dp`. Only 96px (`navigation.css:174`).
|
||||
- **Navigation rail, centre alignment** — "Alignment: top or center; on tablets prefer center alignment for
|
||||
reach. Menu icon and FAB are always top-aligned" (RCB §Navigation Rail/Behaviour). The rail is
|
||||
top-aligned only.
|
||||
- **Navigation rail, "hide when collapsed" as an explicit configuration** — it is in M3's configuration
|
||||
table ("Expanded behavior | Hide when collapsed | — | Available"). The library gets it implicitly below
|
||||
`sm` in `mode="adaptive"`, but there is no mode that hides a rail in an immersive wide layout.
|
||||
- **Docked toolbar with a FAB** — M3's configuration table lists "Other elements | With FAB" for both
|
||||
toolbars; `toolbar.blade.php:26` gates the `fab` slot on `$variant === 'floating'`.
|
||||
- **App bar, trailing-action overflow at small widths** — "trailing actions collapse into an overflow menu
|
||||
at smaller breakpoints and reappear at larger ones" (RCB §Top App Bar/Behaviour). Not implemented (and
|
||||
M3 also tells you not to put an overflow menu in the app bar in the first place, so this is genuinely
|
||||
optional).
|
||||
- **Toolbar, large-screen treatments** — "On web/large screens it can gain rounded corners and use dividers
|
||||
to organize many actions"; "medium+ breakpoints allow centering all elements or centering one key
|
||||
action while pinning others to the edges" (RCB §Toolbars/Behaviour). The toolbar has no breakpoint
|
||||
behaviour at all.
|
||||
|
||||
Correctly absent (deprecated in M3 Expressive, do **not** add): bottom app bar, navigation drawer,
|
||||
baseline medium/large top app bar, baseline navigation bar. `theme-toggle mode="picker"` is the one place
|
||||
a deprecated design slipped back in — see N-04.
|
||||
|
||||
## Breakpoint map
|
||||
|
||||
M3's classes: Compact <600, Medium 600–839, Expanded 840–1199, Large 1200–1599, Extra-large ≥1600
|
||||
(RF §Layout/Breakpoints). Tailwind's: `sm` 640, `md` 768, `lg` 1024, `xl` 1280, `2xl` 1536.
|
||||
|
||||
| Component / behaviour | Library breakpoint used | M3 class it stands in for | Gap |
|
||||
| --- | --- | --- | --- |
|
||||
| `app-shell` — bottom navigation bar shown; rail hidden, modal-only | `max-sm` < 640px (`app-shell.blade.php:142`, `:87-88`, `:136`; `navigation.css:190-194`, `:235-279`) | Compact (<600dp) | **+40px.** 600–639px is M3 Medium but is driven as Compact |
|
||||
| `navigation-bar` — item layout vertical → horizontal | container 600px (`navigation.css:123`, `:465`, `:519`, `:525`, `:555`, `:562`) | Medium (600dp) | **exact** — and a container query, so it also works in a narrow column |
|
||||
| `app-shell` — collapsed rail appears (96px), bar disappears | `sm` ≥ 640px | Medium (600–839dp) | **+40px late**; disagrees with the line above, so the horizontal bar layout only exists in a 40px band |
|
||||
| `app-shell` / `navigation-rail mode="adaptive"` — rail becomes a standard, in-layout collapsible rail; menu button toggles instead of opening a modal | `lg` ≥ 1024px (`js/navigation.js:16`; `navigation.css:51`, `:57`, `:184`) | Expanded (≥840dp) | **+184px.** 840–1023px gets a collapsed rail whose expansion is modal, scrimmed, focus-trapped and re-closed by every `wire:navigate`; the remembered "expanded" preference is ignored |
|
||||
| `app-shell` — content region `max-lg:overflow-x-clip` | `lg` 1024px (`app-shell.blade.php:136`) | Expanded (840dp) | +184px (a clipping backstop, no M3 rule attached) |
|
||||
| `section-nav` — menu picker → secondary-tab row | `sm` 640px (`section-nav.blade.php:40`, `:59`) | Compact/Medium boundary (600dp) | +40px |
|
||||
| `section-nav` — 3/4-column grid → single row | `xl` 1280px (`section-nav.blade.php:33-34`) | Large (1200dp) | +80px |
|
||||
| showcase chrome — theme picker → cycle button | `md` 768px (`showcase/layout.blade.php:148-149`) | none (no M3 rule) | n/a — showcase only |
|
||||
| `app-bar`, `toolbar`, `tabs` | none | Medium+ adaptive guidance (toolbar centring/rounded corners, app-bar action overflow) | not implemented (see Missing) |
|
||||
|
||||
**What a user actually sees in the three bands the brief asks about**
|
||||
|
||||
- **600–639px** (M3 Medium; e.g. a small tablet in portrait, a resized desktop window): no navigation rail
|
||||
at all — it is `width: 0; display: none`. The bottom navigation bar is still pinned, but it has already
|
||||
flipped to the Expressive *medium* configuration (icon beside label in a 40px secondary-container pill,
|
||||
items grouped in the middle at `10 × (count + 3)`% of the width, label-large instead of label-medium per
|
||||
N-08). Any destination marked `'bar' => false` is reachable only by an application-supplied menu button
|
||||
calling `$store.rail.show()`. At 640px the bar vanishes outright and a 96px collapsed rail appears — a
|
||||
jump, at the wrong number.
|
||||
- **840–1023px** (M3 Expanded; a landscape tablet, a half-screen desktop browser): a 96px collapsed rail
|
||||
with icon-over-label items. The menu button does **not** expand it in place — `materialNavigationRail.modal`
|
||||
is still true, so it opens the 256px rail *over* the page on a 32% scrim with a focus trap, and
|
||||
`livewire:navigating` closes it again, so choosing a destination means re-opening the rail every time.
|
||||
`$store.rail.collapsed === false` (the visitor's remembered choice, applied to `<html data-rail>` before
|
||||
first paint) has no effect here. M3 lists a standard expanded rail for this whole band.
|
||||
- **1200–1279px** (M3 Large): **no mismatch** — `lg` (1024px) is long past, so the rail is already the
|
||||
standard collapsible one, expanded by default and remembered in localStorage, which is what M3 wants at
|
||||
Large. The only thing that changes at 1280px is `section-nav`'s grid → row switch (N-07's `xl` row
|
||||
above), which is 80px later than M3's 1200dp Large boundary. Nothing in the group does anything
|
||||
different at 1600px (Extra-large), where M3 suggests considering a third pane.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Material Design 3 reference (extracted 2026-09-13)
|
||||
|
||||
What Google's documentation says, condensed for auditing this library against it. Every file
|
||||
keeps the wording of the site where a rule is stated, the tables verbatim, and a source URL per
|
||||
section. Numbers the site only draws in pictures were taken from androidx Compose Material 3's
|
||||
token files (`compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/`)
|
||||
and the Material Components Android component docs, and are marked with their source.
|
||||
|
||||
| File | Covers |
|
||||
|---|---|
|
||||
| `foundations.md`, `foundations-supplement.md` | accessibility, content design, customisation, design tokens, interaction (gestures, inputs, selection, states), layout (window size classes, scaffold, grids and spacing, canonical layouts, RTL), usability, glossary |
|
||||
| `styles.md`, `styles-supplement.md` | colour (roles, system, dynamic, custom colours, contrast), elevation, icons, motion (Expressive springs, easing and duration, transitions), shape (corner scale, shapes, morph), spacing, typography |
|
||||
| `components-actions-communication-containment.md` | buttons, icon buttons, button groups, split button, segmented buttons, FAB, extended FAB, FAB menu, badges, progress indicators, loading indicator, snackbar, tooltips, bottom sheets, cards, carousel, dialogs, divider, lists, side sheets, menus |
|
||||
| `components-navigation-selection-inputs.md` | app bars, navigation bar, navigation rail, navigation drawer, tabs, toolbars, search, checkbox, chips, radio button, sliders, switch, text fields, date pickers, time pickers, data tables |
|
||||
| `pages.txt` | the 238 pages under `/foundations`, `/styles` and `/components` in the site's sitemap (watches and XR left out) — the coverage checklist; every one was fetched |
|
||||
|
||||
## Refreshing
|
||||
|
||||
m3.material.io renders only in a browser. Read it through a reader proxy, one page at a time
|
||||
(it rate-limits with HTTP 429; retry after a few seconds; add `-H "x-no-cache: true"` for a
|
||||
stale snapshot):
|
||||
|
||||
```bash
|
||||
curl -sL "https://m3.material.io/sitemap.xml" | grep -o '<loc>[^<]*' | sed 's/<loc>//' # the page list
|
||||
curl -sL "https://r.jina.ai/https://m3.material.io/components/buttons/specs" # one page
|
||||
```
|
||||
|
||||
The audits that used these files are in `docs/audits/m3-alignment/`.
|
||||
|
||||
## Attribution
|
||||
|
||||
The text is Google's, from https://m3.material.io, reproduced here so the library can be
|
||||
checked against it; the token values are from the Android Open Source Project (Apache-2.0, see
|
||||
`NOTICE`). Google publishes its design documentation under the Creative Commons Attribution 4.0
|
||||
licence "except as otherwise noted"; confirm the statement in the site's footer before
|
||||
reproducing these files anywhere else. `docs/` is export-ignored, so they are not part of the
|
||||
Composer distribution.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,355 @@
|
||||
# Material Design 3 — Foundations reference (supplement)
|
||||
|
||||
Covers 27 sitemap URLs under `foundations/*` that `reference-foundations.md` did not fetch under their exact
|
||||
path (per-tab sub-pages the earlier pass reached only via a parent tab, plus a few pages the earlier pass
|
||||
never visited at all). Only content **not already in `reference-foundations.md`** is written out in full;
|
||||
pages that turned out to duplicate material already in the reference say so in one line instead of repeating
|
||||
it. Same structure as the reference: Principles / Rules / Values tables / M3 vs M3 Expressive /
|
||||
Accessibility / Source URLs, grouped by topic.
|
||||
|
||||
---
|
||||
|
||||
## Accessibility — assistive technology, designing, writing
|
||||
|
||||
### Duplicates (no new content)
|
||||
- `foundations/designing/overview` — restates the reference's "Do use native elements / Don't use
|
||||
non-standard elements" dialog example verbatim. One new framing sentence: the designing tabs
|
||||
(Accessibility markup, Implementing accessibility, ...) exist to **"translate a visual UI into a
|
||||
text-based, linear user experience that maps to code."** The page only fully rendered two of its stage
|
||||
tabs (Accessibility markup, Implementing accessibility); a third stage implied by "three stages" was not
|
||||
captured in this fetch.
|
||||
|
||||
### New — Assistive technology (foundations/overview/assistive-technology)
|
||||
Three assistive-technology categories, quoted:
|
||||
- **Keyboard**: "Hardware or software directional controllers such as keyboards, a D-pad, or trackball allow
|
||||
users to jump from selection to selection in a linear fashion."
|
||||
- **Screen readers**: "A screen reader is a software program that uses either a braille display or reads
|
||||
text aloud, such as Google's screen reader, TalkBack." Verbalizes visible content (paragraph/button text)
|
||||
**and** hidden content (alt text for icons, headings). Used by people with vision impairments, reading
|
||||
difficulty, or temporary reading limitations.
|
||||
- **Switch input**: "Switches scan the items on your screen, highlighting each item in turn, until you make
|
||||
a selection. Switch Access lets you interact with your Android device using one or more switches instead
|
||||
of the touchscreen."
|
||||
|
||||
### New — Writing best practices, beyond alt text (foundations/writing/best-practices)
|
||||
The reference already covers alt text length/rules; this page adds three more image-text mechanisms:
|
||||
- **Adjacent text**: text in/around an image should present key information about the image (distinct from
|
||||
alt text and captions).
|
||||
- **Captions**: text that appears below an image, explaining how it relates to the content; benefits both
|
||||
sighted and screen-reader users (screen readers **do** read captions, unlike embedded image text).
|
||||
- **Embedded text in images**: "Screen readers are unable to read text that is embedded in imagery." Any
|
||||
essential information embedded as image text must be duplicated into the alt text instead. Caution flagged
|
||||
explicitly for this case.
|
||||
- Alt text is also framed as an SEO benefit ("Keywords can also improve search engine optimization").
|
||||
|
||||
### New — Text resizing (foundations/writing/text-resizing)
|
||||
**Requirement**: UIs must support a minimum text increase of **200%**.
|
||||
|
||||
**Scaling rules** — most components:
|
||||
- Text and line height scale up proportionally, multiplied by the scale value.
|
||||
- Padding remains constant at **1x** the default size (e.g., a button keeps 8dp top/bottom and 24dp
|
||||
left/right padding at 1x, 1.3x, and 2x text scale).
|
||||
- Spacing between elements in a component remains constant at 1x.
|
||||
- Components with no text (progress indicators, checkboxes, radio buttons) are **not** affected by text
|
||||
resizing — don't resize them.
|
||||
|
||||
**Multiplier calculation** (when the OS doesn't drive resizing): offer multipliers (e.g. 1.5x, 2x);
|
||||
`font size = default font size (density 0) × scale value`. Example: 14pt at 1x → 28pt at 2x scale.
|
||||
|
||||
**Methods to handle large type** (avoids clipping, overlap, unwanted truncation):
|
||||
| Method | Rule |
|
||||
| --- | --- |
|
||||
| Increase container size | Let containers grow so text doesn't overlap/clip/truncate; design for line-to-line eye tracking |
|
||||
| Reflow the layout | Stack components (e.g. buttons) that were side-by-side, rather than keeping them fixed-width |
|
||||
| Enable content to scroll | Prefer vertical scrolling only — never ask users to scroll both vertically and horizontally |
|
||||
| Touch & hold tooltips | For space-constrained components — **top app bar, navigation bar, navigation rail, and fixed (non-scrolling) tabs** — keep the component's own text at 1x and show the full scaled-up label in a tooltip on touch & hold |
|
||||
|
||||
### New — Text truncation (foundations/writing/text-truncation)
|
||||
**Requirement**: "Content, understandability, and functionality must not be lost when users modify their
|
||||
type settings." (Exceptions may apply for non-Latin-alphabet languages.)
|
||||
|
||||
- **Text wrapping**: wrap text when it's critical, needed for understandability, or space allows. **Don't**
|
||||
cut off wrapped text without a way to see the rest (e.g., an expand affordance).
|
||||
- **Height/width of components**: prefer flexible containers that grow to fit content over fixed text-size
|
||||
limits that don't fit the available space.
|
||||
- **Ellipsis + hover/link**: truncating to an ellipsis is acceptable **only** if the full text is reachable
|
||||
via a tooltip or a link that reveals what was truncated. An ellipsis with no reveal mechanism "is not
|
||||
accessible." Note: this pattern still adds difficulty for some users, so use it deliberately.
|
||||
|
||||
### Source URLs
|
||||
https://m3.material.io/foundations/overview/assistive-technology
|
||||
https://m3.material.io/foundations/designing/overview
|
||||
https://m3.material.io/foundations/writing/best-practices
|
||||
https://m3.material.io/foundations/writing/text-resizing
|
||||
https://m3.material.io/foundations/writing/text-truncation
|
||||
|
||||
---
|
||||
|
||||
## Content design — global writing, style guide
|
||||
|
||||
### Duplicates (no new content)
|
||||
- `foundations/content-design/global-writing/overview` — same definition of global writing already quoted
|
||||
in the reference. One new line: **"This guidance is for all American English that is localized."**
|
||||
- `foundations/content-design/style-guide/ux-writing-best-practices` — this is the exact content the earlier
|
||||
pass already captured at `foundations/content-design/style-guide` (explain consequences, scannable
|
||||
words/formats, sentence case, abbreviations sparingly) — fully identical, word for word.
|
||||
|
||||
### New — Grammar and punctuation (foundations/content-design/style-guide/grammar-and-punctuation)
|
||||
Not covered anywhere in the reference. Rules, quoted/paraphrased:
|
||||
- **Skip periods on single sentences** — omit in labels, tooltip text, bulleted lists, dialog body text,
|
||||
hyperlinked text. Use periods for multiple sentences, or long/complex sentences.
|
||||
- **Use contractions** — easier to scan; exception: spelled-out "do not" can read as more emphatic than
|
||||
"don't" when caution is needed.
|
||||
- **Serial (Oxford) comma** — always use it, except before an ampersand. Commas always go inside quotation
|
||||
marks.
|
||||
- **Commas in numbers**: use commas for numbers **1,000–1,000,000**. Don't use commas when identifying
|
||||
something (street address, radio frequency, year). Above 1 million, comma use depends on context;
|
||||
"million" can be abbreviated "M" with rounding when giving a sense of volume rather than an exact count
|
||||
(e.g. "23M views").
|
||||
- **Skip colons in headings** — no colon on a heading introducing a list; do use a colon for a list
|
||||
introduced within body text.
|
||||
- **Exclamation points sparingly** — fine for greetings/congratulatory moments; avoid for empty states and
|
||||
routine tasks ("save it for bigger accomplishments").
|
||||
- **Ellipses sparingly** — indicate an in-progress action or incomplete text (check with engineering, since
|
||||
truncation ellipses are often automatic). No space before an ellipsis. Omit ellipses from menu items or
|
||||
buttons that open a dialog or start a process.
|
||||
- **Parentheses** — only to define acronyms/jargon or cite a source; never for an aside/afterthought (fold
|
||||
the info into the sentence instead).
|
||||
- **Ampersands**: "&" is acceptable in headlines, column headers, table headers, navigation labels, and
|
||||
buttons (space-constrained contexts) — but spelling out "and" improves readability where there's room.
|
||||
Always spell out "and" in sentences/paragraphs, before the final item in a 3+ item list, and in email
|
||||
subject lines.
|
||||
- **Dashes** — three kinds: em dash (—), en dash (–), hyphen (-). Avoid em dashes in UX writing (replace with
|
||||
a comma, period, or new sentence). Use an en dash with no surrounding spaces for a range (e.g. "9 AM–Noon").
|
||||
- **Hyphens** — bind closely related words; also represent negative numbers (e.g. "-100"); never surrounded
|
||||
by spaces. Defer to AP style when unsure whether a phrase needs one. Hyphenation table:
|
||||
| Rule | Examples | Why |
|
||||
| --- | --- | --- |
|
||||
| Hyphenate adjective phrases | case-by-case basis, best-in-class performance, once-in-a-lifetime opportunity | Multiple words used together as one adjective are hyphenated — but not proper nouns or common phrases ("cell phone number", "chocolate chip cookie") |
|
||||
| Hyphenate noun phrases | sign-off, drive-through, go-ahead (verb+adverb); higher-up, most-read (adjective-as-noun); jack-of-all-trades, stick-in-the-mud (fixed idioms) | Certain multi-word nouns are always hyphenated |
|
||||
| Don't hyphenate verb phrases | "look out for falling rocks," "drop off your car," "check in to the room" | A verb+adverb/preposition acting as a verb isn't hyphenated (contrast the noun "check-in"); note "check into" ≠ "check in to" |
|
||||
- **Italics sparingly** — hard to read; use bold for emphasis instead. Italics are acceptable for a single
|
||||
word/phrase (a name or title), never a full sentence.
|
||||
- **No caps blocks** — "Avoid using caps blocks altogether; they're not accessible." Use sentence case for
|
||||
all product text.
|
||||
|
||||
### New — Word choice: pronouns (foundations/content-design/style-guide/word-choice)
|
||||
Note: this is a **different** page from the already-covered `global-writing/word-choice` (which is about
|
||||
localization word choice). This one is about person/pronoun choice:
|
||||
- **Use second person ("you"/"your")** — makes the UI feel like it's speaking to the user about their own
|
||||
actions; avoid impersonal/robotic phrasing.
|
||||
- **Don't mix first and second person** in the same context (e.g. "My photos" title next to "Your photos"
|
||||
description) — pick one address form, or drop the pronoun.
|
||||
- **Use caution with "I"/"we"** — for a large company like Google, first person can read as robotic or
|
||||
disconcerting; prefer the user's point of view or remove the pronoun. **Exception**: legal text may use
|
||||
"I"/"my" to emphasize ownership in an agreement (e.g. "I agree to the terms of service").
|
||||
|
||||
### Source URLs
|
||||
https://m3.material.io/foundations/content-design/global-writing/overview
|
||||
https://m3.material.io/foundations/content-design/style-guide/grammar-and-punctuation
|
||||
https://m3.material.io/foundations/content-design/style-guide/ux-writing-best-practices
|
||||
https://m3.material.io/foundations/content-design/style-guide/word-choice
|
||||
|
||||
---
|
||||
|
||||
## Design tokens — usage workflow
|
||||
|
||||
### Duplicates (no new content)
|
||||
- `foundations/design-tokens/overview` — restates the reference's token definition, naming scheme,
|
||||
three-class model (ref/sys/comp), and contexts verbatim (this is the site's single `design-tokens` page,
|
||||
now also reachable at `/overview`).
|
||||
|
||||
### New — Reading a component's token spec table (foundations/design-tokens/overview)
|
||||
Not in the reference: a documented procedure for finding a token in a component's Specs tab. Tokens there
|
||||
are grouped first by **state** (enabled, disabled, hover, etc.), then by **element** (the part the token
|
||||
applies to, e.g. container or label text). Table columns:
|
||||
| Column | Meaning |
|
||||
| --- | --- |
|
||||
| Name | The style aspect the token applies to (e.g. color, font) |
|
||||
| Token ID | The token defining that style aspect |
|
||||
| Description | Optional extra info |
|
||||
| Context/value | The value stored for a given context |
|
||||
|
||||
Worked example given on the page: to verify a filled button's label-text color, go to *Common buttons* →
|
||||
*Specs*, find the filled-button token module, locate the "label text" row under elements, and copy the color
|
||||
token into code (or compare it to the Figma color role).
|
||||
|
||||
### New — How to use tokens, a Figma workflow (foundations/design-tokens/how-to-use-tokens)
|
||||
Entirely new — a step-by-step production workflow, not covered anywhere in the reference:
|
||||
1. **Install** the Material Theme Builder Figma plugin.
|
||||
2. **Generate tokens**: Plugins → Material Theme Builder → Open Plugin → Get started. Creates a
|
||||
`material-theme` artboard with baseline tonal palettes (light/dark) and a default type scale, populated
|
||||
as Figma styles.
|
||||
3. **Update token values** — two ways:
|
||||
- Via the plugin (color only): re-open the plugin, choose new colors; color/text styles update in place.
|
||||
- Via Figma styles directly: right-click a style → "Go to style definition," hover the style → the
|
||||
adjust icon (or right-click → Edit style) to change name/description/properties.
|
||||
4. **Use tokens in mockups**: apply the generated Figma styles to layout elements instead of hand-setting
|
||||
color/typography, so engineers can trust the design intent transfers.
|
||||
5. **Use tokens with the Material Design Kit**: duplicate the kit, open the plugin with kit components
|
||||
selected, and choose **Swap** to replace baseline token values with the custom generated ones.
|
||||
6. **Export tokens**: plugin's Export tab → choose a target format (Android, Jetpack Compose) → save as a
|
||||
named `.zip`.
|
||||
|
||||
### Source URLs
|
||||
https://m3.material.io/foundations/design-tokens/overview
|
||||
https://m3.material.io/foundations/design-tokens/how-to-use-tokens
|
||||
|
||||
---
|
||||
|
||||
## Interaction: states (per-state one-line definitions)
|
||||
|
||||
### Duplicate, with one addition
|
||||
`foundations/interaction/states/overview` restates the "two visual indicators," "states can combine," and
|
||||
six-state list already in the reference. New here: the site's own one-line definition for each state
|
||||
(not quoted in the reference), verbatim:
|
||||
| State | Definition |
|
||||
| --- | --- |
|
||||
| Enabled | "communicates an interactive component or element" |
|
||||
| Disabled | "communicates an inoperable component or element" |
|
||||
| Hover | "communicates when a user has placed a cursor above an interactive element" |
|
||||
| Focused | "communicates when a user has highlighted an element, using an input method such as a keyboard or voice" |
|
||||
| Pressed | "communicates a user tap" |
|
||||
| Dragged | "communicates when a user presses and moves an element" |
|
||||
|
||||
### Source URL
|
||||
https://m3.material.io/foundations/interaction/states/overview
|
||||
|
||||
---
|
||||
|
||||
## Layout — adaptive design, breakpoints per class, canonical layouts
|
||||
|
||||
### Duplicates (no new content)
|
||||
- `foundations/layout/layout-overview/overview` — same 7-parts-of-layout figure and Layout terms glossary
|
||||
already quoted in the reference (this is the site's `layout-overview` tab, now also at `/overview`).
|
||||
- `foundations/layout/scaffold/overview` — same Bars/Rails/Panes summary already in the reference.
|
||||
- `foundations/layout/grids-spacing/overview` — same "grids adapt across breakpoints / spacing groups &
|
||||
directs attention / density for data-heavy views" summary already in the reference.
|
||||
- `foundations/layout/breakpoints/overview` — same breakpoint width table, pane/navigation/communication/
|
||||
action table, five adaptation questions, and common-swappable-components table already fully quoted in
|
||||
the reference.
|
||||
- `foundations/usability/overview` and `foundations/usability/applying-m3-expressive` — **both render the
|
||||
same content** as the reference's Usability section (five NN/g usability aspects, usability-vs-
|
||||
accessibility distinction, the M3-Expressive design-tactics list — color/contrast, containment, motion,
|
||||
shape & shape morph, size, typography — and "design based on primary goals"). No new facts on either page.
|
||||
|
||||
### New — Adaptive design, defined (foundations/layout/layout-overview/adaptive-design)
|
||||
Not covered in the reference at all — this is the site's actual definition of the "adaptive design" term
|
||||
the reference only flagged as a May-2026 rename target:
|
||||
- **Adaptive vs. responsive**: "While responsive design scales a single layout to fit any screen, adaptive
|
||||
design customizes a product to optimize the experience on each device." Adapts to **People** (preferences/
|
||||
settings), **Devices** (watch, phone, foldable, tablet, desktop, XR), and **Usage** (window resize,
|
||||
orientation, device switch).
|
||||
- **Three primary experience types** — design starts at mobile and scales up to spatial; all three should
|
||||
account for touch, pointer, and physical-keyboard input regardless of the device's default input:
|
||||
| Experience | Notes |
|
||||
| --- | --- |
|
||||
| Mobile | Phones, foldables, tablets. Window modes: **full-screen** (default), **split-screen** (2+ apps sharing the screen), **bubbles** (floating multitask windows) |
|
||||
| Desktop | Free-form windows adapting across breakpoints; split-screen/floating/free-form multitasking. A tablet becomes a desktop experience once a keyboard+mouse connect; an Android phone can become desktop-like via an external monitor |
|
||||
| Spatial (XR) | Multiple free-form windows in a "virtually limitless" screen. Android XR's immersive **"full space"** mode positions components freely in 3D. A navigation rail can become an **Orbiter**, floating beside the main pane (ties to the glossary's XR "Orbiter" term already in the reference, now with a concrete use case) |
|
||||
- **Adaptive strategies** (named mechanism, not in the reference): as a pane/window resizes or someone
|
||||
navigates, panes may change size, enter/exit, or reorganize via three strategies: **show and hide**,
|
||||
**levitate**, and **reflow**. Panes are "the primary segments of a layout" and can change based on context.
|
||||
- **Pane display styles**, with sharper definitions/examples than the reference's terse list:
|
||||
- **Co-planar**: panes displayed side by side.
|
||||
- **Floating**: a pane displayed above other panes/content, "like a dialog."
|
||||
- **Docked**: a pane displayed above other content where one edge extends beyond a screen edge, "like a
|
||||
bottom sheet."
|
||||
- Compose's **Navigation 3** library lets multiple destinations show on screen simultaneously and adapts
|
||||
layouts across window sizes/screens automatically.
|
||||
- **Adapting components** — three component-level adaptation strategies:
|
||||
| Strategy | Example given |
|
||||
| --- | --- |
|
||||
| Resizing | A button may scale with its parent container, or "hug" its content and stay left/right-aligned |
|
||||
| Showing & hiding | A list item can reveal a description/extra info as its container scales up |
|
||||
| Presentation changes | A FAB can become an extended FAB; a navigation rail can auto-expand as the window grows |
|
||||
|
||||
### New — Breakpoints, per window-size class (foundations/layout/breakpoints/{compact,medium,expanded,large-extra-large})
|
||||
The reference only has the aggregate breakpoint table; these per-class pages add concrete margins, pane
|
||||
widths, and navigation placement the reference doesn't state:
|
||||
|
||||
| Breakpoint | Width | Margins/spacer | Panes | Navigation | Notes |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| Compact | <600dp | **16dp** margins | 1 (always) | Navigation bar or modal expanded navigation rail, placed close to the screen edge for reach | Must transition dynamically on: foldable unfold, portrait→landscape rotation, exiting split-screen, multi-window resize larger, free-form resize |
|
||||
| Medium | 600–839dp | **24dp** margins and spacer | 1 recommended; 2 only for low-density content (e.g. settings) — each pane defaults to 50% width, don't set custom widths; a drag handle can expand either pane to 100% | Single-pane: navigation rail (can hide on secondary destinations if a back button still reaches the primary destination); two-pane: navigation bar (to keep panes at full width) | Reachability: top 25% of screen is hard to reach on horizontal tablets/unfolded foldables — limit interactions there; page defines 3 ergonomic regions (inconvenient reach / comfortable / challenging near bottom edge). Transition triggers: foldable fold, landscape→portrait rotation, full-screen→split-screen, multi-window start, free-form resize |
|
||||
| Expanded | 840–1199dp | **24dp** margins and spacer | 1 (only for dense content like video) or 2 (recommended); fixed-and-flexible layout's fixed pane defaults to **360dp**; a split-pane layout's two flexible panes visually center the spacer, with nav rail + first pane totaling 50% | Navigation rail (collapsed or expanded); hideable on secondary destinations with a back button; use tabs/other in-pane components for secondary/sort/filter nav | Transition triggers: foldable fold, landscape→portrait rotation, full-screen→split-screen, multi-window start, free-form resize |
|
||||
| Large (1200–1599dp) & Extra-large (1600dp+) | — | **24dp** margins and spacer | 2 recommended (1 only for dense/media content); fixed-and-flexible fixed pane defaults to **412dp**; extra-large only can add a **standard side sheet as a 3rd pane** (side sheet max width 400dp even though 412dp is the general fixed-pane recommendation) — never exceed 3 panes | Navigation rail, collapsed or expanded depending on content volume; expanded rail suits extra-large best, collapse when space is needed or deep in the hierarchy | Large/extra-large are "most useful for...laptop and desktop devices"; some products may skip them entirely. Watch line length/typography at this size. Transition triggers: full-screen→split-screen, multi-window start, free-form resize |
|
||||
|
||||
### New — Canonical layout examples, in detail (foundations/layout/canonical-examples/{overview,feed,list-detail,supporting-pane})
|
||||
The reference only had a stub (three names + "not fetched, outside foundations/*"); these are the real
|
||||
`foundations/layout/canonical-examples/*` pages (distinct from the `m3/pages/canonical-examples/*` URLs the
|
||||
reference declined to follow), each with compact/medium/expanded configurations:
|
||||
|
||||
**Overview**
|
||||
- Three canonical layouts exist: **feed, list-detail, supporting pane** — each with compact/medium/expanded
|
||||
configurations, meant as a starting point rather than a fixed template.
|
||||
- Supporting-pane split defined precisely: primary/focus area "typically about two-thirds" of the window;
|
||||
secondary/supporting area takes the remaining third.
|
||||
- **Advanced custom layouts**: build on a canonical example or layer scaffold elements directly. The
|
||||
**"layering"** technique uses the **levitate** adaptive strategy — a pane floats above other content for a
|
||||
focused, task-oriented moment (examples given: reviewing a shopping basket, responding to comments,
|
||||
creating a calendar event).
|
||||
- Availability: Figma M3 Design Kit, Jetpack Compose canonical-layouts sample, Android Views (MDC-Android)
|
||||
canonical-layouts sample.
|
||||
|
||||
**Feed**
|
||||
- Use case: grid of cards/lists for quick browsing/discovery of large content volumes (news, photos, social
|
||||
media); grids adapt from single- to multi-column at any size.
|
||||
- Dividing space: flexible enough to mix proportions/sizes (e.g. small + large cards together) to signal
|
||||
relationships between items; items reflow on rotation/unfold/multi-window, and item **order is determined
|
||||
by position**.
|
||||
- Across breakpoints: **Compact** — stack vertically, one card per row, full pane width. **Medium** — split
|
||||
into multiple equal-width columns (example shown: 4 columns). **Expanded/large/extra-large** — more
|
||||
columns than medium, and column width should usually increase as the breakpoint grows.
|
||||
|
||||
**List-detail**
|
||||
- Use case: parent-child content pairings — text message + conversation, file browser + open folder, artist
|
||||
+ album, settings + category detail, inbox + selected email.
|
||||
- Visible-panes table (breakpoint dp ranges as stated on this page):
|
||||
| Breakpoint (dp) | Visible panes |
|
||||
| --- | --- |
|
||||
| Compact (0–599) | 1 pane |
|
||||
| Medium (600–839) | 1 (recommended) or 2 |
|
||||
| Expanded (840+) | 2 |
|
||||
| Large (1200–1599) | 2 |
|
||||
| Extra-large (1600+) | 2 |
|
||||
- **Compact**: single pane only, either list or detail view at a time.
|
||||
- **Medium**: single pane for information-dense content/deep focus, **or** two-pane to browse and switch
|
||||
quickly — use a bottom navigation bar or modal navigation rail in two-pane mode to maximize horizontal
|
||||
space.
|
||||
- **Expanded/large/extra-large**: two-pane.
|
||||
- **Behavior**: a **Back button appears in the detail view only for single-pane layouts**; a **selected
|
||||
state appears in the list view only for two-pane layouts**; use explicit/implicit grouping to direct
|
||||
visual focus in two-pane layouts. Layouts transition dynamically as available space changes (e.g. rotating
|
||||
from expanded to medium collapses two panes back to one).
|
||||
|
||||
**Supporting pane**
|
||||
- Use case: secondary content that is "only meaningful in relation to" the primary content (contrast with
|
||||
list-detail's parent-child relationship — use list-detail instead if that's the actual relationship).
|
||||
Examples: productivity, document editing + commenting, content/media browsing.
|
||||
- Placement/width table:
|
||||
| Supporting-pane placement | Pane width | Breakpoint |
|
||||
| --- | --- | --- |
|
||||
| Below the focus pane | Flexible | Compact or Medium |
|
||||
| Leading or trailing side of the focus pane | Fixed, **360dp** | Expanded |
|
||||
- **Compact**: supporting pane appears below the focus pane; a bottom sheet is a good way to keep focus on
|
||||
the primary pane while still giving access to supporting info.
|
||||
- **Medium**: supporting pane appears below the focus pane (same as compact).
|
||||
- **Expanded**: supporting pane moves to the leading or trailing side of the focus pane.
|
||||
|
||||
### Source URLs
|
||||
https://m3.material.io/foundations/layout/layout-overview/overview
|
||||
https://m3.material.io/foundations/layout/layout-overview/adaptive-design
|
||||
https://m3.material.io/foundations/layout/scaffold/overview
|
||||
https://m3.material.io/foundations/layout/grids-spacing/overview
|
||||
https://m3.material.io/foundations/layout/breakpoints/overview
|
||||
https://m3.material.io/foundations/layout/breakpoints/compact
|
||||
https://m3.material.io/foundations/layout/breakpoints/medium
|
||||
https://m3.material.io/foundations/layout/breakpoints/expanded
|
||||
https://m3.material.io/foundations/layout/breakpoints/large-extra-large
|
||||
https://m3.material.io/foundations/layout/canonical-examples/overview
|
||||
https://m3.material.io/foundations/layout/canonical-examples/feed
|
||||
https://m3.material.io/foundations/layout/canonical-examples/list-detail
|
||||
https://m3.material.io/foundations/layout/canonical-examples/supporting-pane
|
||||
https://m3.material.io/foundations/usability/overview
|
||||
https://m3.material.io/foundations/usability/applying-m3-expressive
|
||||
@@ -0,0 +1,748 @@
|
||||
# Material Design 3 — Foundations reference
|
||||
|
||||
Extracted verbatim (with light reformatting) from m3.material.io/foundations for auditing an M3 Expressive
|
||||
component library. Facts not present on the fetched pages are marked "(not on the site; from ...)".
|
||||
Where the site's own text is ambiguous or inconsistent, both readings are quoted rather than resolved.
|
||||
|
||||
---
|
||||
|
||||
## Accessibility (foundations/overview, /overview/principles, /designing*, /writing, /building-for-all*)
|
||||
|
||||
### Principles
|
||||
- "Accessibility by default is a core design value for Material." Accessibility requirements and goals are
|
||||
documented across component pages and guidelines.
|
||||
- Three principles for accessible design:
|
||||
- **Honor individuals** — universal defaults rarely meet everyone's needs; build in customizable features
|
||||
since a person's needs change over time.
|
||||
- **Learn before, not after** — invest in research across a wide range of abilities before defining
|
||||
solutions, rather than reacting to problems after launch.
|
||||
- **Requirements as a starting point** — treat WCAG minimums as a floor for creative solutions, not a
|
||||
ceiling (dark mode, text-to-speech, speech-to-text all originated as accommodations).
|
||||
- "Usability focuses on making products intuitive and easy to understand for everyone" while "Accessibility
|
||||
focuses on making products accessible for people with disabilities. Accessible experiences are
|
||||
perceivable, operable, understandable and robust, and support people who use assistive technology."
|
||||
(from foundations/usability)
|
||||
|
||||
### Rules (do / don't)
|
||||
- **Do** use native platform elements/semantic HTML so components automatically carry the right
|
||||
accessibility markup (e.g., a native `dialog`).
|
||||
- **Don't** use non-standard elements to fake a standard pattern (e.g., a custom div styled as a dialog) —
|
||||
it requires extra testing to work with assistive tech.
|
||||
- Disabled states **do not** need to meet contrast requirements.
|
||||
- **Landmarks (web only)** — classify page regions with ARIA landmark roles so visual structure is also
|
||||
conveyed in code. The eight landmark roles: **navigation, search, main, banner, complementary,
|
||||
contentinfo, region, form**.
|
||||
- Only one **main**, one **banner**, one **contentinfo** per page.
|
||||
- Label any landmark role that repeats (e.g., multiple `navigation` regions) with a unique, specific
|
||||
label; **all regions** should be labeled.
|
||||
- **Don't repeat the landmark role inside its own label** (e.g., a nav labeled "Navigation").
|
||||
- **Headings (web only)**:
|
||||
- Identify headings by content hierarchy, not visual styling.
|
||||
- "Headings should not skip a level, for example, don't go from H2 to H4 without using an H3."
|
||||
- Map content to H1–H6 in sequential order.
|
||||
- "A single H1 for the page title is recommended."
|
||||
- Heading level and visual prominence don't have to match.
|
||||
- **Focus order / keyboard**:
|
||||
- Default tab order should follow the DOM and "generally flows from left to right; top to bottom." Use
|
||||
this default unless a custom pattern requires otherwise.
|
||||
- Define **initial focus** for a screen and for multi-element components (dialogs, complex cards).
|
||||
- When a dialog opens, focus moves into it (to a specific interactive element); on close/cancel, focus
|
||||
returns to the element that triggered it.
|
||||
- Tab / Shift+Tab move between interactive elements (Shift+Tab reverses); arrow keys navigate within a
|
||||
component (menu items, grid cells); Enter activates a link/button or submits a focused form.
|
||||
- Grouped sub-elements can be one tab stop, navigated internally with arrow keys.
|
||||
- **Keyboard shortcuts**: use two-or-more-key combinations by default. Document all shortcuts (help page
|
||||
or tutorial). If a shortcut uses a single key, provide at least one of, in preference order:
|
||||
1. (most preferred) remap to include a non-printable key,
|
||||
2. (preferred) only activate when the relevant component is focused,
|
||||
3. (temporary only) allow turning the shortcut off.
|
||||
- **Labeling elements**:
|
||||
- Needs a label: interactive icons/buttons with no/insufficient visible text, interactive images,
|
||||
meaningful visual cues (progress bars, error states), meaningful icons/images, generic links
|
||||
("Learn more"), buttons with ambiguous/duplicate text ("Save").
|
||||
- Doesn't need a label: non-interactive UI text (read automatically), buttons with sufficient text
|
||||
("Download image").
|
||||
- **Don't include the element's role (button, menu, etc.) in the label text** — the role is added
|
||||
automatically when the proper role is assigned.
|
||||
- Decorative icons/images that add nothing for a screen-reader user should be marked decorative/hidden
|
||||
(e.g., `alt=""`).
|
||||
|
||||
### Values
|
||||
| Item | Value | Source |
|
||||
| --- | --- | --- |
|
||||
| Large text/graphics contrast (≥14pt bold / ≥18pt regular) | ≥ 3:1 against background | designing/color-contrast, writing |
|
||||
| Small text contrast | ≥ 4.5:1 against background | designing/color-contrast, writing |
|
||||
| Clustered non-text elements (e.g. a group of buttons) vs. background | ≥ 3:1 recommended | designing/color-contrast |
|
||||
| Standalone elements (e.g. a single FAB) vs. background | not required to meet 3:1 (prominence already differentiates them) | designing/color-contrast |
|
||||
| Disabled-state contrast | not required to meet contrast requirements | designing/color-contrast, applying-states |
|
||||
| Touch target minimum | **48 × 48dp** (≈9mm physical; recommended range 7–10mm) | designing/structure |
|
||||
| iOS touch target note | iOS recommends 44 × 44dp | designing/structure |
|
||||
| Pointer (mouse/stylus) target minimum | **44 × 44dp** | designing/structure |
|
||||
| Target spacing | ≥ 8dp between adjacent targets | designing/structure |
|
||||
| Web default target size | at least 48 × 48 **CSS pixels** | grids-spacing/density |
|
||||
| Alt text length | "up to 125 characters" (foundations/writing) **vs.** "recommended length ... is 140 characters" (content-design/alt-text) — site is internally inconsistent; both figures are quoted verbatim | writing, content-design/alt-text |
|
||||
|
||||
### Alt text rules (content-design/alt-text, foundations/writing)
|
||||
- Alt text is read in place of an image by screen readers and shown if an image fails to load.
|
||||
- **Do** write alt text that conveys context/meaning; **don't** leave the auto-generated filename as alt
|
||||
text.
|
||||
- Mark an image `alt=""` (decorative) if removing it loses no information.
|
||||
- Focus on the meaning/purpose of the image, not exhaustive visual detail.
|
||||
- **Don't start alt text with "image of"** — screen readers already announce "image".
|
||||
- Essential vs. non-essential image content: essential info must meet the 3:1 (large) / 4.5:1 (small) contrast
|
||||
above; decorative/non-essential elements are exempt.
|
||||
|
||||
### M3 vs M3 Expressive differences
|
||||
- No explicit M3-vs-Expressive distinction is drawn on the Accessibility pages themselves; the WCAG-based
|
||||
contrast and target-size rules are presented as constant across both.
|
||||
|
||||
### Accessibility requirements (recap)
|
||||
- Two-indicator principle for states is asserted only as a bullet: **"States have two visual indicators to
|
||||
ensure accessibility"** (foundations/interaction/states) — the page does not elaborate further on this
|
||||
Foundations tier (see Interaction States section below for the gap this leaves).
|
||||
|
||||
### Source URLs
|
||||
https://m3.material.io/foundations/overview
|
||||
https://m3.material.io/foundations/overview/principles
|
||||
https://m3.material.io/foundations/designing (tabs: Overview, Color contrast, Structure, Flow, Elements)
|
||||
https://m3.material.io/foundations/designing/color-contrast
|
||||
https://m3.material.io/foundations/designing/structure
|
||||
https://m3.material.io/foundations/designing/flow
|
||||
https://m3.material.io/foundations/designing/elements
|
||||
https://m3.material.io/foundations/writing
|
||||
https://m3.material.io/foundations/building-for-all
|
||||
https://m3.material.io/foundations/building-for-all/user-needs (identical rendered content to /building-for-all)
|
||||
https://m3.material.io/foundations/building-for-all/co-design
|
||||
|
||||
---
|
||||
|
||||
## Content design (foundations/content-design/*)
|
||||
|
||||
### Principles
|
||||
- "UI text should be clear to anyone." Follow **Associated Press (AP) Style** unless Material notes
|
||||
otherwise.
|
||||
|
||||
### Rules (do / don't) — Style guide (content-design/style-guide)
|
||||
- **Explain consequences** in neutral, direct language; avoid alarming/condescending warnings. Do tell
|
||||
users what will happen and how to undo it; don't misrepresent consequences to influence a decision.
|
||||
- **Use scannable words and formats** — specific titles/headings people can skim.
|
||||
- **Use sentence case.** "Unless otherwise specified, use sentence-style capitalization... All text,
|
||||
including titles, headings, labels, menu items, navigation components, app bars, and buttons should use
|
||||
sentence-style capitalization." Products/branded terms may still be capitalized. **Don't use title case.**
|
||||
- **Use abbreviations sparingly.** Spell out words where possible; avoid Latin abbreviations ("e.g.",
|
||||
"etc.") — use "for example," "and more" instead.
|
||||
|
||||
### Rules — Alt text (see also Accessibility above)
|
||||
- Recommended alt text length: 140 characters (content-design/alt-text); don't start with "image of".
|
||||
|
||||
### Rules — Global writing (content-design/global-writing, /global-writing/word-choice)
|
||||
- Global writing = writing inclusively for many cultures and optimized for localization/translation.
|
||||
- **Use global examples; explain local references** — call out (in a message description for translators)
|
||||
locations, names/nicknames, currencies, temperatures, date formats, and providers (internet/cable) that
|
||||
don't translate globally.
|
||||
- Use short, simple sentences; break into bullets/sections with headings.
|
||||
- Avoid abbreviations; clarify ambiguous pronouns and "this"/"that" references.
|
||||
- Avoid idiomatic, colloquial, or overly polite expressions (translation risk).
|
||||
- Reduce technical jargon; clarify ambiguities.
|
||||
|
||||
### Rules — Notifications (content-design/notifications)
|
||||
- Notifications should: be about the user (not the product); be precise, timely, actionable, contextual,
|
||||
relevant; give easy opt-out controls; never be unsolicited ads.
|
||||
- Put the most important info first (people skim in an F-shape).
|
||||
- CTAs should be concise, specific, actionable.
|
||||
- Make notifications relevant/personal — don't broadcast generically.
|
||||
- **Avoid dynamic text**, especially in headlines (breaks character limits when translated, doesn't
|
||||
expand if truncated). If used, pair with no more than one additional word, and prepare a fallback
|
||||
notification that fits the character budget.
|
||||
- **Don't use relative day words** ("today"/"tomorrow"/"tonight") — about 20% of users don't see a
|
||||
notification the day it's sent; use the specific day of the week instead (exception: auto-dismissing
|
||||
notifications tied to a timestamp).
|
||||
- Use emoji sparingly; never to accentuate bad news (negative emoji tested poorly); don't replace words
|
||||
with emoji.
|
||||
- Don't repeat the app name/logo (already shown by the OS).
|
||||
- Don't interrupt onboarding with unrelated notifications/emails.
|
||||
- Give users in-context, clear opt-in/opt-out controls.
|
||||
|
||||
### Values
|
||||
| Element | Character limit |
|
||||
| --- | --- |
|
||||
| Notification title | < 29 characters |
|
||||
| Notification collapsed body | < 40 characters |
|
||||
| Notification expanded body | < 80 characters (start with the collapsed body and extend it) |
|
||||
| Notification buttons | 1–2 buttons, 1–2 words each |
|
||||
| SMS, Latin-script languages | < 160 characters |
|
||||
| SMS, non-Latin-script languages | < 134 characters |
|
||||
| Alt text | 140 characters recommended (content-design/alt-text) / "up to 125 characters" (foundations/writing) |
|
||||
|
||||
### M3 vs M3 Expressive differences
|
||||
- Not called out on these pages; content-design guidance is presented as version-agnostic.
|
||||
|
||||
### Accessibility requirements
|
||||
- Alt text and captions are the accessibility mechanism this section owns (see Accessibility section for
|
||||
full alt-text rules); captions benefit both sighted and screen-reader users.
|
||||
|
||||
### Source URLs
|
||||
https://m3.material.io/foundations/content-design/overview
|
||||
https://m3.material.io/foundations/content-design/alt-text
|
||||
https://m3.material.io/foundations/content-design/global-writing
|
||||
https://m3.material.io/foundations/content-design/global-writing/word-choice
|
||||
https://m3.material.io/foundations/content-design/notifications
|
||||
https://m3.material.io/foundations/content-design/style-guide
|
||||
|
||||
---
|
||||
|
||||
## Customization (foundations/customization)
|
||||
|
||||
### Principles
|
||||
- "M3 opens up new possibilities for both brand colors and individual color preferences to converge in
|
||||
one-of-a-kind experiences."
|
||||
- Dynamic color lets an app retain brand-defining colors while also honoring a user's device/personal color
|
||||
preference; works for custom (non-Material) components too.
|
||||
|
||||
### Rules
|
||||
- Build a **custom color scheme** with the M3 color system so the app can respect device/app-level dynamic
|
||||
color settings; always keep a fallback (non-dynamic) color scheme for users who don't enable dynamic
|
||||
color.
|
||||
- Apply dynamic color selectively if desired (e.g., only on a profile screen) rather than app-wide.
|
||||
|
||||
### Values
|
||||
- **Five essential color groups / roles**: Primary, Secondary, Tertiary, Neutral, Neutral Variant. An input
|
||||
(source) color generates a tonal palette that fills roles such as primary / on-primary / primary
|
||||
container.
|
||||
- The Material Theme Builder (Figma plugin) generates **color and type tokens**, exportable to multiple
|
||||
code formats, including Design System Package (DSP) format.
|
||||
|
||||
### M3 vs M3 Expressive differences
|
||||
- Not explicitly distinguished on this page; dynamic color and the 5-role model are presented as the
|
||||
current (M3) system without an Expressive-specific callout.
|
||||
|
||||
### Accessibility requirements
|
||||
- "The color system automatically handles critical adjustments that provide accessible color contrast,
|
||||
legibility, interaction states, and component structure."
|
||||
|
||||
### Source URLs
|
||||
https://m3.material.io/foundations/customization
|
||||
|
||||
---
|
||||
|
||||
## Design tokens (foundations/design-tokens)
|
||||
|
||||
Note: the task brief anticipated separate `/overview`, `/how-to-read-tokens`, `/how-to-use-tokens`
|
||||
sub-pages; the live site has a **single page** at `foundations/design-tokens` covering all of this content
|
||||
(confirmed via the site's own top-nav, which links only to `foundations/design-tokens`).
|
||||
|
||||
### Principles
|
||||
- "Design tokens are small, reusable design decisions that make up a design system's visual style. Tokens
|
||||
replace static values with self-explanatory names."
|
||||
- A token = a code-like name (e.g. `md.ref.palette.secondary90`) + an associated value (e.g. `#E8DEF8`);
|
||||
the value can be a color, typeface, measurement, or another token.
|
||||
- Tokens let design and code stay in sync: "if a designer's mock-ups and an engineer's implementation both
|
||||
reference the same token... they can be confident that the same [value] is being used in both places,"
|
||||
even after the underlying value changes.
|
||||
- Tokens should be used instead of hardcoded values whenever a design choice recurs across components.
|
||||
|
||||
### Rules
|
||||
- **Do** use design tokens instead of hardcoded values.
|
||||
- Name each token for how/where it's used (e.g. `md.comp.fab.primary.container.color` sets an FAB's
|
||||
container color).
|
||||
- **System tokens should point to reference tokens**, not static values, whenever possible.
|
||||
- **Component tokens should point to a system or reference token**, not hardcoded values, whenever
|
||||
possible.
|
||||
- Tokens are recommended when: building a design system from scratch or updating one broadly, applying a
|
||||
system across a suite of products, wanting to maintain/update styles easily, or wanting dynamic color.
|
||||
Tokens are **less** helpful for an existing app with values unlikely to change and no design system.
|
||||
|
||||
### Values — naming scheme
|
||||
| Part | Meaning | Example |
|
||||
| --- | --- | --- |
|
||||
| System prefix | Always starts with the system name | `md` (Material Design) |
|
||||
| Class | `ref` = reference, `sys` = system, `comp` = component | `md.ref…`, `md.sys…`, `md.comp…` |
|
||||
| Role/descriptor | Descriptive words for the token's purpose | `…palette.secondary90`, `…color.secondary-container`, `…fab.primary.container.color` |
|
||||
|
||||
### Values — the three token classes
|
||||
| Class | Prefix | Definition | Example |
|
||||
| --- | --- | --- | --- |
|
||||
| Reference tokens | `ref` | "All available tokens with associated values." Point to static values (or other reference tokens); don't change based on context. | `md.ref.palette.secondary90` → `#E8DEF8`; `md.ref.typeface.plain-medium` → Roboto Medium |
|
||||
| System tokens | `sys` | "Decisions and roles that give the design system its character" — theming happens here; can point to different ref tokens by context (e.g. light/dark theme). | `md.sys.color.secondary-container` → `md.ref.palette.secondary90`; `md.sys.typescale.label-medium.font` |
|
||||
| Component tokens | `comp` | "(in development)" — the design properties assigned to a specific component's elements (container, label, icon, state, size, shape, color, elevation). | Extended FAB container color / label text tokens |
|
||||
|
||||
- **Contexts**: conditions under which a token points to a non-default value — "device form factors, dark
|
||||
theme, dense layouts, and right-to-left writing systems" are the examples given. A context acts like a
|
||||
tag overriding the default token value.
|
||||
- Glossary cross-references (foundations/glossary): **Design tokens: Role** = "short nickname describing
|
||||
the purpose of a design token... Also known as slots" (e.g. "On surface", "Body 1"); **Design tokens:
|
||||
Value** = "the information defining a design attribute, either stored in a token or hard-coded."
|
||||
|
||||
### M3 vs M3 Expressive differences
|
||||
- Component tokens are explicitly flagged as **"(in development)"** — i.e., not every component's tokens
|
||||
are finalized/complete as of this fetch.
|
||||
|
||||
### Accessibility requirements
|
||||
- Not directly addressed on this page; contexts (e.g. dark theme, dense layout) are the token mechanism
|
||||
used elsewhere to satisfy accessibility/adaptation requirements.
|
||||
|
||||
### Source URLs
|
||||
https://m3.material.io/foundations/design-tokens
|
||||
|
||||
---
|
||||
|
||||
## Interaction: gestures, inputs, selection, states
|
||||
|
||||
### Gestures (foundations/interaction/gestures)
|
||||
|
||||
**Principles**
|
||||
- "Gestures help people navigate, take action, or transform content." UI elements should respond to
|
||||
gestures in real time. Tap, scroll, and swipe are the most common.
|
||||
|
||||
**Types (do/don't-style catalogue, no numeric specs given)**
|
||||
- Tap (navigate/interact), Double tap (zoom in/out), Long press (extra functionality / selection), Scroll
|
||||
and pan, Swipe (switch peer views like tabs, complete actions, or reveal actions on a list item), Drag,
|
||||
Pick up and move (long-press + drag to reorder), Pinch (scale), Compound gestures (e.g. pan + pinch on a
|
||||
map).
|
||||
- **Predictive back** (Android): swipe left/right to preview going back before committing. Compatible
|
||||
components: **bottom sheet, navigation bar, navigation rail, search bar, side sheet**.
|
||||
|
||||
**Source**: https://m3.material.io/foundations/interaction/gestures
|
||||
|
||||
### Inputs (foundations/interaction/inputs)
|
||||
|
||||
**Principles**
|
||||
- "Design for touch, keyboard, and mouse interactions." Embrace multiple input methods within one app.
|
||||
|
||||
**Rules / values**
|
||||
- External input devices: mouse (left/right click, wheel, extra buttons), trackpad (click, gestures,
|
||||
haptics), physical keyboard (replaces virtual keyboard; media keys; modifier keys), stylus.
|
||||
- **Hover**: rest a cursor on an interactive element to trigger its hover state (see States below) and, if
|
||||
applicable, a tooltip.
|
||||
- **Cursor shapes**: Pointer (default), Hand (links/linked images), Resize arrows (resizable element
|
||||
edges), I-beam (editable/selectable text). Text-selection click semantics: single click places cursor /
|
||||
deselects & repositions; double click selects a word; triple click selects a paragraph.
|
||||
- **Physical keyboard**: show/hide the on-screen (virtual) keyboard based on whether a physical keyboard is
|
||||
attached (do hide it when a physical keyboard connects; do show it again when disconnected). Enter
|
||||
typically sends/submits; Spacebar (or media keys) typically play/pause media.
|
||||
|
||||
**Source**: https://m3.material.io/foundations/interaction/inputs
|
||||
|
||||
### Selection (foundations/interaction/selection)
|
||||
|
||||
**Principles**
|
||||
- Selection can be shown via a check icon, a checkbox, or a surface-color change (or a combination).
|
||||
- An entire component, or only certain parts of it, can be selected. Selection can be performed by tap,
|
||||
cursor, keyboard, or voice.
|
||||
|
||||
**Values**
|
||||
- Components that **inherit selection styling**: cards, checkboxes, chips, data tables, icon buttons, list
|
||||
items, menu items, pickers, radio buttons, segmented buttons, sliders, switch.
|
||||
- Components that use an **active indicator** instead (only one selected at a time): navigation bar,
|
||||
navigation drawer, navigation rail, tabs.
|
||||
|
||||
**Rules**
|
||||
- Touch: long-press or two-finger touch (or a shortcut, e.g. tapping an avatar) enters selection mode; tap
|
||||
additional items to add them; tap each selected item (or a toolbar action) to exit selection mode.
|
||||
- Long-press + drag can batch-select — **don't** reuse this gesture combo for selection if it's already
|
||||
used to pick up/move items (e.g. reordering cards).
|
||||
- Desktop/click: when selection is the primary activity, checkboxes stay visible; when secondary, show a
|
||||
checkbox on hover for a single item, and for all items once one is selected.
|
||||
|
||||
**Source**: https://m3.material.io/foundations/interaction/selection
|
||||
|
||||
### States (foundations/interaction/states, /states/state-layers, /states/applying-states)
|
||||
|
||||
**Principles**
|
||||
- "States are visual indicators used to communicate the status of a component or interactive element."
|
||||
- **"States have two visual indicators to ensure accessibility"** — stated as a bullet on the States
|
||||
overview page; the site does **not** elaborate on this rule anywhere in Foundations (no page defines
|
||||
what the two indicators must be, e.g. color + shape/outline). This is a documentation gap to flag when
|
||||
auditing — Foundations only asserts the principle, not the mechanism.
|
||||
- States can combine (e.g. selected + hover); apply states consistently across components.
|
||||
- Six states: **Enabled, Disabled, Hover, Focused, Pressed, Dragged.**
|
||||
|
||||
**State layer mechanics (states/state-layers)**
|
||||
- "A state layer is a semi-transparent covering on an element that indicates its state," providing "a
|
||||
systematic approach to visualizing states by using opacity." Only one state layer can be applied at a
|
||||
given time; it can cover the whole element or a circular sub-region.
|
||||
- The state layer's color equals the **content's "on" color** (e.g. if the container uses **secondary
|
||||
container** and content uses **on secondary container**, the state layer uses **on secondary
|
||||
container**).
|
||||
- **"The size of state layers is 40dp while the interactive target size is 48dp."**
|
||||
|
||||
**Values — state layer / disabled opacities** (from the states/state-layers interactive module, exact
|
||||
figures)
|
||||
| State | Opacity |
|
||||
| --- | --- |
|
||||
| Hover | 0.08 (+8%) |
|
||||
| Focus | 0.10 (+10%) |
|
||||
| Pressed | 0.10 (+10%) |
|
||||
| Dragged | 0.16 (+16%) |
|
||||
| Disabled | 0.38 |
|
||||
|
||||
**Rules — per-state inheritance (applying-states)**
|
||||
| State | Inherited by | NOT inherited by |
|
||||
| --- | --- | --- |
|
||||
| Disabled | Buttons, Cards, Checkboxes, Chips, List items, Radio buttons, Switches, Text fields | App bars, Badges, Dialogs, FABs, Menus, Navigation bar/drawer/rail, Sheets, Tabs, Tooltips |
|
||||
| Hover | Buttons, Cards, Checkbox, Chips, Date/time pickers, List items, Slider, Switch, Text fields | App bars, Badges, Dialogs, Menus, Navigation bar/drawer/rail, Sheets, Tabs |
|
||||
| Focused | Buttons, Cards, Checkbox, Chips, Date/time pickers, List items, Selection controls, Text fields | App bars, Badges, Banner, Card, Dialogs, Navigation bar/drawer/rail, Sheets |
|
||||
| Pressed | Buttons, Cards, Checkbox, Chips, List items, Text fields | App bars, Badges, Bottom navigation, Dialogs, Menus, Sheets, Tabs |
|
||||
| Dragged | Cards, Chips, List items, Sliders | App bars, Badges, Buttons, Dialogs, Menus, Navigation bar/drawer/rail |
|
||||
|
||||
- **Disabled**: communicated via color change + reduced elevation; not focusable, draggable, or pressable;
|
||||
no hover/other state layer; exempt from contrast requirements; "if the action... is unavailable, the FAB
|
||||
shouldn't appear" (don't just disable it) — a Don't example specifically about FABs.
|
||||
- **Hover**: lower-emphasis overlay; appears/disappears with a "low-emphasis animated fade"; only one hover
|
||||
state active in a layout at a time; can combine with focused/activated/selected/pressed.
|
||||
- **Focused / keyboard focus indicator**: "When an element is tabbed to, it appears in its focused state
|
||||
with a ring-like keyboard focus indicator." **No thickness, offset, or color token is specified anywhere
|
||||
in Foundations** — this page only calls it "ring-like." (Exact focus-ring specs, if they exist, live in
|
||||
per-component spec pages outside the Foundations section — flag as out-of-scope-for-this-file / a gap
|
||||
for the audit.) Only one focus state active at a time; initiated by Tab (or equivalent).
|
||||
- **Pressed**: higher-emphasis than hover; a **ripple overlay** signifies press (can also/alternatively use
|
||||
an elevation change); only one pressed state active at a time; initiated by tap/click/keyboard/voice
|
||||
activation.
|
||||
- **Dragged**: lower-emphasis overlay than pressed, "to avoid distracting users from their task"; can also
|
||||
use elevation; only one dragged state active at a time; initiated by touch-and-hold + move.
|
||||
|
||||
### M3 vs M3 Expressive differences
|
||||
- Not explicitly called out for interaction/states; the state-layer opacity table and inheritance lists are
|
||||
presented as the current, single spec (this appears to be the M3 Expressive-era rewrite of the classic M2
|
||||
state-layer table, but the site does not itself label old vs. new values here).
|
||||
|
||||
### Accessibility requirements
|
||||
- The "two visual indicators" principle (see above) is the accessibility hook for state changes but is left
|
||||
unelaborated in Foundations.
|
||||
- Disabled components are fully exempted from contrast requirements.
|
||||
- Keyboard operability requirements live in Accessibility → Flow (focus order, keyboard shortcuts) above,
|
||||
and cross-reference into States for the focus indicator.
|
||||
|
||||
### Source URLs
|
||||
https://m3.material.io/foundations/interaction/gestures
|
||||
https://m3.material.io/foundations/interaction/inputs
|
||||
https://m3.material.io/foundations/interaction/selection
|
||||
https://m3.material.io/foundations/interaction/states/overview
|
||||
https://m3.material.io/foundations/interaction/states/state-layers
|
||||
https://m3.material.io/foundations/interaction/states/applying-states
|
||||
|
||||
---
|
||||
|
||||
## Layout (foundations/layout/*)
|
||||
|
||||
Note on the task brief's guessed slugs: the live IA does **not** use `/understanding-layout/*` or
|
||||
`/applying-layout/*`. Real structure discovered via the site's own nav/tab links:
|
||||
- `layout/layout-overview` (tabs: Overview, **Overview: Parts of layout** → `/layout-overview/parts-of-layout`)
|
||||
- `layout/scaffold` (tabs: Overview, Bars, Rails, Panes)
|
||||
- `layout/grids-spacing` (tabs: Overview, Grids, Spacing, Density)
|
||||
- `layout/breakpoints`
|
||||
- `layout/bidirectionality-rtl`
|
||||
- `layout/canonical-examples`
|
||||
|
||||
### Principles
|
||||
- "Layout is the visual and strategic arrangement of elements on a screen," used to organize elements,
|
||||
signal hierarchy, and draw attention to key actions.
|
||||
- Adapt layouts to the five breakpoints; build from an established canonical layout example; design for
|
||||
bidirectionality (LTR + RTL); apply consistent arrangement/sizing/spacing.
|
||||
- Material's components are built with **"leading"/"trailing"** (not left/right) naming so they support RTL
|
||||
by construction — "extra configuration may be needed to achieve specific RTL situations," though.
|
||||
- **May-2026 renames** (explicitly called out on the site as new terminology): **"Window size classes
|
||||
renamed to breakpoints"**; **"Responsive layout renamed to adaptive design."**
|
||||
|
||||
### Values — Layout terms glossary (layout-overview)
|
||||
- **Adaptive design**: techniques letting an interface dynamically respond to user preference, device
|
||||
type, state, and breakpoints.
|
||||
- **Bars**: frame the page (app bar, bottom navigation bar).
|
||||
- **Bidirectionality**: a writing system displaying content RTL.
|
||||
- **Breakpoints**: "opinionated window sizes where a layout changes to match available space, device
|
||||
conventions, and ergonomics (previously window size classes)."
|
||||
- **Column**: one or more vertical blocks of content within a pane.
|
||||
- **Drag handle**: resizes panes.
|
||||
- **Fold**: the flexible/hinge area separating two displays on a foldable.
|
||||
- **Gap**: space between components/elements within a container.
|
||||
- **Margin**: space between the screen edge and elements inside it.
|
||||
- **Multi-window mode**: multiple apps sharing one screen simultaneously.
|
||||
- **Pane**: "a layout container that houses other components and elements within a single app... fixed,
|
||||
flexible, floating, or semi-permanent."
|
||||
- **Rails**: perimeter space around panes holding navigation rails, toolbars, pane controls.
|
||||
- **RTL language**: e.g. Arabic, Hebrew, Farsi — used by "over 2 billion people."
|
||||
- **Rulers**: opinionated global alignment lines.
|
||||
- **Safety region**: zones reserved for system UI (status bar, gesture bar) — "shouldn't contain primary
|
||||
content."
|
||||
- **Scaffold**: "a fundamental UI design structure that provides a standard platform for assembling key
|
||||
screen components."
|
||||
- **Spacer**: space between two panes on a foldable.
|
||||
- **7 parts of layout** (numbered figure caption): Column, Fold, Margin, Bar, Drag handle, Pane, Rail.
|
||||
|
||||
### Values — Breakpoints (layout/breakpoints)
|
||||
| Breakpoint | Width (dp) | Common devices |
|
||||
| --- | --- | --- |
|
||||
| Compact | Under 600dp | Phone in portrait |
|
||||
| Medium | 600–839dp | Tablet in portrait; foldable in portrait (unfolded) |
|
||||
| Expanded | 840–1199dp | Phone/tablet in landscape; foldable in landscape (unfolded); desktop |
|
||||
| Large | 1200–1599dp | Desktop |
|
||||
| Extra-large | 1600dp+ | Desktop; ultra-wide monitors |
|
||||
|
||||
- Height breakpoints (compact/medium/expanded) also exist on Android but are rarely needed since most
|
||||
layouts scroll vertically.
|
||||
|
||||
**Breakpoint → recommended panes / navigation / communication / action** (layout/breakpoints)
|
||||
| Breakpoint | Panes | Navigation | Communication | Action |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Compact | 1 | Navigation bar, modal expanded navigation rail | Simple dialog, full-screen dialog | Bottom sheet |
|
||||
| Medium | 1 (recommended) or 2 | Navigation bar, modal expanded navigation rail | Simple dialog | Menu |
|
||||
| Expanded | 1 or 2 (recommended) | Modal or standard expanded navigation rail | Simple dialog | Menu |
|
||||
| Large | 1 or 2 (recommended) | Modal or standard expanded navigation rail | Simple dialog | Menu |
|
||||
| Extra-large | 1 to 3 (recommended) | Modal or standard expanded navigation rail | Simple dialog | Menu |
|
||||
|
||||
**Common swappable components by breakpoint** (layout/breakpoints)
|
||||
| Component type | Compact | Medium | Expanded |
|
||||
| --- | --- | --- | --- |
|
||||
| Navigation | Navigation bar | Collapsed navigation rail | Collapsed navigation rail |
|
||||
| Navigation (secondary row) | Modal expanded navigation rail | Modal expanded navigation rail | Standard expanded navigation rail |
|
||||
| Communication | Basic or full-screen dialog | Basic dialog | Basic dialog |
|
||||
| Supplemental selection | Bottom sheet | Menu | Menu |
|
||||
|
||||
- Pane-count guidance restated: **compact/medium → 1 pane; expanded/large → 2 panes recommended;
|
||||
extra-large → consider 3 panes.**
|
||||
- Five questions to ask moving up a breakpoint: what should be **revealed, divided (paned), resized,
|
||||
repositioned, swapped**. Across breakpoints, keep text to **40–60 characters per line**.
|
||||
- Component-swap example: compact navigation bar → medium/expanded navigation rail; collapsed rail
|
||||
(medium/expanded) → expanded rail (large/extra-large). **Don't** swap functionally non-equivalent
|
||||
components (e.g. a button for a menu).
|
||||
|
||||
### Values — Scaffold (layout/scaffold, /bars, /rails, /panes)
|
||||
- Scaffold = **Bars + Rails + Panes**, arranged around a **safety region** that must stay clear of primary
|
||||
content.
|
||||
- **Bars**: frame the page; contain app bar (top) or navigation bar (bottom). App bars: title + 1–2
|
||||
essential actions (search, back). "**Navigation bars let people switch between 3–5 primary UI views**"
|
||||
at compact/medium breakpoints.
|
||||
- **Rails**: the perimeter layer after bars; hold navigation rails, toolbars, chat inputs, FABs, other
|
||||
primary controls; on desktop, a leading/trailing rail; on mobile, a toolbar can float in the rail region.
|
||||
- **Panes**: all content must live in a pane; a layout has **1–3 panes**, adapting to breakpoint and
|
||||
language direction (RTL flips navigation to the right).
|
||||
- **Single-pane** layouts: any breakpoint, recommended at compact/medium.
|
||||
- **Two-pane** layouts: common at expanded/large/extra-large; fixed-and-flexible panes can appear in
|
||||
either order.
|
||||
- **Three-pane** layouts: "Fixed panes at this breakpoint are recommended to be **412dp**, but side
|
||||
sheets have a default maximum width of **400dp**."
|
||||
- **Snap widths** when resizing a two-pane layout at expanded/large/extra-large: **360dp**, **412dp**, or
|
||||
a centered split-pane with spacer.
|
||||
- **Persistent resizing** (remembers width across sessions/breakpoint changes) is the default
|
||||
recommendation for most resizable layouts; **temporary resizing** (reverts to default on
|
||||
close/reopen) is for supporting-pane layouts where resizing is uncommon.
|
||||
- Multi-pane display styles: **co-planar** (side by side — used for persistent utilities so they stay
|
||||
accessible), **floating**, **docked**.
|
||||
- **Accessibility**: coplanar-pane focus order must match visual arrangement; modal floating panes trap
|
||||
focus (move focus in on open, return it to the trigger on close) and close on outside interaction;
|
||||
non-modal floating panes let focus move freely in/out and must sit in logical reading order; docked
|
||||
panes follow the same focus rules as floating panes, plus the coplanar focus-order requirement.
|
||||
|
||||
### Values — Grids & spacing (layout/grids-spacing, /grids, /spacing, /density)
|
||||
- Grids: column count/width/spacing scale with breakpoint (compact fewer columns → expanded/foldable more
|
||||
columns); **no explicit numeric column-count or gutter table is given on the current site** (this is a
|
||||
gap versus the classic M2 12-column grid spec — flag if the component library assumes fixed column
|
||||
counts).
|
||||
- **Rulers**: Bar & safety rulers (reserve system-UI space), Title ruler (aligns app-bar title), Content
|
||||
rulers (first ruler = major blocks like hero/headline; secondary rulers = supplementary text/actions).
|
||||
Margin rulers can be widened/narrowed for expressive effect (e.g., full-bleed photo grid vs. a
|
||||
wide-margin search bar).
|
||||
- Spacing is used to **group** (explicit grouping = outlines/dividers/shadows; implicit grouping =
|
||||
proximity/whitespace only), to **direct attention** (rhythm, similarity, proximity, continuity), and as
|
||||
**expression** (generous spacing + bright surfaces on the most important content; negative space for
|
||||
emphasis). No fixed numeric spacing scale (e.g. 4/8/16dp increments) is stated on this page beyond the
|
||||
8dp target-spacing rule already listed under Accessibility.
|
||||
- **Density** (component scaling):
|
||||
- Density scale is numbered starting at **0** (default) and moves negative (**-1, -2, -3**) as space
|
||||
decreases.
|
||||
- "Higher density is typically applied by decreasing the top and bottom padding or overall height by
|
||||
**4dp**" per step.
|
||||
- Text size should not change as a container scales with density.
|
||||
- Example figures from the page: 20dp gap between a text field's label and input; a "don't" example shows
|
||||
a dropdown item shrunk to 38dp of selectable height (too dense — reduces usability).
|
||||
- **Density must be opt-in, never default**: "People should be able to **opt in** to dense layouts and
|
||||
components." Settings interactions themselves must keep default target sizes (**48×48 CSS pixels**).
|
||||
"Don't scale layouts below 48×48dp by default."
|
||||
- **Interaction targets must stay ≥48×48dp even when the visible element is scaled down** — e.g. a
|
||||
settings icon can be 24×24dp visually but must keep a 48×48dp target; a 36dp-tall button can keep a
|
||||
48dp target.
|
||||
- Don't increase density in focused/high-stakes UI (menus, snackbars, dialogs).
|
||||
- Pixel density formula: `pixel density = screen width (or height) in px / screen width (or height) in
|
||||
inches`.
|
||||
- **Density-independent pixel (dp)**: "A dp is equal to one physical pixel on a screen with a density of
|
||||
160." Formula: `dp = (width in pixels × 160) / screen density`.
|
||||
|
||||
### Values — Bidirectionality / RTL (layout/bidirectionality-rtl)
|
||||
- Over 2 billion people read/write RTL languages (Arabic, Hebrew, Farsi, Urdu).
|
||||
- **Mirroring**: LTR ⇄ RTL flips element alignment/order; reading starts top-right in RTL. **Exception**:
|
||||
graphs/charts stay LTR even in Persian and Urdu.
|
||||
- **Text rendering** has two parts: alignment (box placement) and directionality (text/element flow).
|
||||
Common RTL bugs: text entry, cursor position, punctuation, phone numbers, URLs. **Don't** reverse an
|
||||
email's username/domain order (domain always stays to the right of the username, even though the
|
||||
username itself can render RTL). **Don't** apply LTR directionality to RTL content (scrambles word
|
||||
order) — content needs both RTL alignment *and* RTL directionality.
|
||||
- **Icons**: directional icons (back/forward, send) mirror in RTL. **Exception**: in Hebrew, timelines and
|
||||
media controls keep LTR directionality; help icons mirror only in some RTL languages (e.g. Urdu,
|
||||
Persian).
|
||||
- **Time / progress**: linear progress indicators fill right-to-left in most RTL languages, **except
|
||||
Hebrew, which stays LTR**. Circular progress indicators always move clockwise regardless of direction.
|
||||
Media player controls (video/audio) are **always LTR**. Clocks always turn clockwise; on a 12-hour clock
|
||||
in RTL, the AM/PM symbol moves to the left; the 24-hour clock is common outside English-primary
|
||||
locales.
|
||||
- **Canonical layouts in RTL**: list-detail, feed, and supporting-pane layouts are all explicitly "mirrored
|
||||
in RTL."
|
||||
- **Component specifics**: badges reposition; toolbars mirror tool order; app bars mirror layout and flip
|
||||
directional icons; **navigation rail sits on the leading edge — right side in RTL, left in LTR**
|
||||
(expanded nav rail too); text field leading/trailing icons swap sides; a chip's trailing icon "is always
|
||||
aligned to the end side of the container" (right for LTR, left for RTL).
|
||||
- **Gestures**: swipe-to-reveal actions and Android predictive back must mirror their LTR counterparts
|
||||
(e.g. a delete action revealed by a right-swipe in LTR should be revealed by a left-swipe in RTL).
|
||||
|
||||
### Values — Canonical layout examples (layout/canonical-examples)
|
||||
- Named canonical layouts referenced: **feed**, **list-detail**, **supporting-pane** (each links out to a
|
||||
dedicated `m3.material.io/m3/pages/canonical-examples/...` page — a legacy/alternate URL namespace not
|
||||
fetched here since it falls outside `foundations/*`). An "Advanced custom layouts" section exists on the
|
||||
page but only points onward, with no further numeric content on this page itself.
|
||||
|
||||
### M3 vs M3 Expressive differences
|
||||
- The May-2026 rename ("window size class" → "breakpoint"; "responsive layout" → "adaptive design") is the
|
||||
only explicit terminology-version marker found in Layout.
|
||||
- Density (opt-in component scaling) and the layout scaffold (bars/rails/panes model) are presented as the
|
||||
current single system, introduced/updated as part of the same May-2026 refresh per layout-overview's
|
||||
"What's new" log ("Introduced layout scaffold... Updated canonical layout examples... Spacing system").
|
||||
|
||||
### Accessibility requirements
|
||||
- Safety regions must stay clear of primary content (keeps content from being obscured by system UI).
|
||||
- Pane focus-order/trap rules above (coplanar order must match visual order; modal panes trap and restore
|
||||
focus).
|
||||
- RTL is treated as an accessibility/global-reach requirement throughout, not an optional locale nicety.
|
||||
- Minimum 48×48dp interaction targets carry through from Accessibility into density/scaling rules here.
|
||||
|
||||
### Source URLs
|
||||
https://m3.material.io/foundations/layout/layout-overview
|
||||
https://m3.material.io/foundations/layout/layout-overview/parts-of-layout
|
||||
https://m3.material.io/foundations/layout/scaffold/overview
|
||||
https://m3.material.io/foundations/layout/scaffold/bars
|
||||
https://m3.material.io/foundations/layout/scaffold/rails
|
||||
https://m3.material.io/foundations/layout/scaffold/panes
|
||||
https://m3.material.io/foundations/layout/grids-spacing/overview
|
||||
https://m3.material.io/foundations/layout/grids-spacing/grids
|
||||
https://m3.material.io/foundations/layout/grids-spacing/spacing
|
||||
https://m3.material.io/foundations/layout/grids-spacing/density
|
||||
https://m3.material.io/foundations/layout/breakpoints
|
||||
https://m3.material.io/foundations/layout/bidirectionality-rtl
|
||||
https://m3.material.io/foundations/layout/canonical-examples
|
||||
|
||||
---
|
||||
|
||||
## Usability (foundations/usability)
|
||||
|
||||
### Principles
|
||||
- "Usability focuses on making products intuitive and easy to understand for everyone" — distinct from
|
||||
Accessibility (see Accessibility section for the exact wording of the distinction).
|
||||
- Key takeaways: emphasize key actions for visual hierarchy; leverage M3 Expressive design tactics; don't
|
||||
overwhelm users with visual information; test and iterate.
|
||||
|
||||
### Values — Nielsen Norman Group's five usability aspects (quoted directly)
|
||||
| Aspect | Definition |
|
||||
| --- | --- |
|
||||
| Efficiency | Users can efficiently complete tasks and goals |
|
||||
| Errors | Proper design reduces the likelihood of mistakes, and users can easily correct any errors that do occur |
|
||||
| Learnability | New users learn to use the product and complete tasks easily, even the first time |
|
||||
| Memorability | Returning users remember how to use the product |
|
||||
| Satisfaction | Users are satisfied with the designed experience |
|
||||
|
||||
### Rules — Usability design tactics (explicitly framed as **M3 Expressive** tactics)
|
||||
- **Color & contrast**: use eye-catching primary/secondary colors and contrasting pairs (e.g. purple +
|
||||
green) for hierarchy; always follow the accessibility contrast rules above.
|
||||
- **Containment & grouping**: group related elements in subtle containers; break content into sections with
|
||||
containment/spacing/headings.
|
||||
- **Motion**: use sparingly to emphasize key moments — "motion can be distracting" if overused.
|
||||
- **Shape & shape morph**: "The Material shape library has **35 shapes**." Shape adds emphasis/delight,
|
||||
guides focus, differentiates containers/buttons/animations, signals interaction, sets emotional tone.
|
||||
Every shape can morph into any other in the set; shape morph communicates interaction states (selected,
|
||||
tap, swipe, scroll, release, long press) and emphasizes in-progress actions.
|
||||
- **Size**: the most important action/CTA should be the largest element; larger key actions measurably
|
||||
improve efficiency, error rate, satisfaction, learnability (per the doc's own claim, no citation given).
|
||||
- **Typography**: use type scale/weight to separate information hierarchies (largest/most legible text =
|
||||
primary action; smaller text = secondary/tertiary); group similar content with the same font style.
|
||||
- Design around **primary / secondary / tertiary goals**: give primary goals the strongest emphasis;
|
||||
simplify to one primary task per page; make core actions large/reachable; don't stack too many expressive
|
||||
tactics at once (distracting).
|
||||
- **Iterate**: test and gather feedback early/often from a range of users/contexts.
|
||||
|
||||
### M3 vs M3 Expressive differences
|
||||
- This entire page's "design tactics" section is explicitly attributed to **M3 Expressive** ("Leverage
|
||||
[expressive design tactics]... containment, size, shape, color, and typography"), including the 35-shape
|
||||
library and shape-morph mechanic, which are Expressive-era additions.
|
||||
|
||||
### Accessibility requirements
|
||||
- "Accessibility focuses on making products accessible for people with disabilities... perceivable,
|
||||
operable, understandable and robust... support people who use assistive technology" (contrasted directly
|
||||
with usability's broader "intuitive for everyone" framing).
|
||||
|
||||
### Source URLs
|
||||
https://m3.material.io/foundations/usability
|
||||
|
||||
---
|
||||
|
||||
## Glossary terms that matter for a web component library (foundations/glossary)
|
||||
|
||||
Only terms relevant to auditing a component library are excerpted; the full glossary runs A–Z (~20KB) and
|
||||
also defines many component names already covered under Components (out of scope here). Quoted verbatim.
|
||||
|
||||
| Term | Definition (verbatim) |
|
||||
| --- | --- |
|
||||
| Adaptive design | "A design approach in which the interface changes based on known user, device, or environmental conditions. Adaptive design in Material includes layout and component adaptations." |
|
||||
| Condition | "A signal that determines when and how an adaptive layout or component should adapt." |
|
||||
| Contrast | "Difference between colors. For accessibility, contrast refers strictly to the difference in tone. **A difference of 40 in tone guarantees a WCAG contrast ratio ≥ 3.0; a difference of 50 in tone guarantees a contrast ratio ≥ 4.5.**" |
|
||||
| Customization | "A modification made to a UI reflecting an app, OEM, or individual user's visual preferences and brand." Can be per-element or global (a theme). |
|
||||
| Dark theme | "A dark theme is a low-light UI that displays mostly dark surfaces." |
|
||||
| Design tokens | "A design token represents a small, reusable design decision that's part of a design system's visual style. Tokens replace static values with self-explanatory names." |
|
||||
| Design tokens: Context | "The set of conditions in which tokens can point to non-default values (for example, dark theme, dense layout)." |
|
||||
| Design tokens: Role | "A shortened version of the system token name (for example, Secondary container color; Headline 1)." |
|
||||
| Design tokens: Types | Reference tokens (e.g. `md.ref.palette.secondary200`); Component tokens (define a component element's design attributes); System tokens (e.g. `md.sys.color.secondary-container`) |
|
||||
| Design tokens: Value | "The information defining a design attribute, either stored in a token or hard-coded." |
|
||||
| Element | (page continues past excerpt boundary — not fully captured; component name only) |
|
||||
| HCT | "HCT is an abbreviation of hue, chroma, tone. It's the name of the color space that enables dynamic color. HCT is based on CAM16 hue and chroma; the L* construct for luminance from L*a*b* (CIELAB, 1976) is denoted as T for tone." |
|
||||
| Mode | (component/theming term; not fully excerpted) |
|
||||
| Orbiter | "Floating UI elements that control the content within spatial panels." (XR-adjacent; likely out of scope) |
|
||||
| Pane | "The building blocks of a layout. Content and actions are grouped into panes, which adapt the content to best fit the screen." |
|
||||
| Role | "Short nickname describing the purpose of a design token within a design system. Also known as slots. Examples: On surface; Body 1." |
|
||||
| Spatial | (XR-adjacent term; out of scope per task instructions) |
|
||||
| Style | (design-spec term; not fully excerpted) |
|
||||
| Theme | (not fully excerpted — see "Note: A light scheme is not the same as a light theme" under Color: Scheme) |
|
||||
|
||||
### Color-system glossary terms (used throughout Customization/Design tokens)
|
||||
| Term | Definition (verbatim) |
|
||||
| --- | --- |
|
||||
| Color: Baseline scheme | "the group of selected tones that make up the default colors values used for light and dark themes." |
|
||||
| Color: Dynamic color | "A customization feature in which a user-generated color scheme is mapped to an app's color scheme." Not simply Theme-Builder output or an algorithmic scheme — it's the *presence of a changeable color role*. |
|
||||
| Color: Extended color | "A color specified (in addition to key colors) in order to fill color roles for custom schemes... brand expression or conventional (semantic) meanings." |
|
||||
| Color: Key color | "not an extracted color — it's a derivation of the source color... the basis for a tonal palette." |
|
||||
| Color: Scheme | "Any mapping of color roles to specific tones from a tonal palette." A **dark scheme** ≠ a **dark theme** (scheme vs. theme distinction explicitly noted). |
|
||||
| Color: Source color | "The single color that's extracted to define all five key colors." |
|
||||
| Color: Tonal palette | "a 13-tone range that serves as the basis for mapping tones to specific roles." |
|
||||
| Color: Tone | "colors with the same hue and chroma... degrees of lightness." Code: `Tone.of(hex).get(tone)` or `Tone.of(hue, chroma).get(tone)`. |
|
||||
| Color: User-generated schemes | colors derived from a user's wallpaper selection or Android preset colors. |
|
||||
|
||||
### Source URLs
|
||||
https://m3.material.io/foundations/glossary
|
||||
|
||||
---
|
||||
|
||||
## Cross-cutting gaps found (for the auditing agents to be aware of)
|
||||
|
||||
1. **Focus indicator specifics** (thickness / offset / color token) are **not defined anywhere in
|
||||
Foundations** — only "ring-like keyboard focus indicator" is stated (states/applying-states). If the
|
||||
component library asserts a specific focus-ring spec, that spec's source is not this section of the
|
||||
site.
|
||||
2. **The "two visual indicators" accessibility rule for states** is asserted once, as a bullet, with zero
|
||||
elaboration anywhere in Foundations on what the two indicators must be.
|
||||
3. **Alt text length is inconsistent across the site itself**: 125 characters (foundations/writing) vs. 140
|
||||
characters (foundations/content-design/alt-text).
|
||||
4. **No explicit numeric grid/column/gutter table** exists on the current (M3 Expressive-era) Layout pages,
|
||||
unlike the classic Material 2 12-column-grid spec; grids/spacing guidance here is qualitative except for
|
||||
the 8dp target-spacing and pane-width (360/400/412dp) numbers captured above.
|
||||
5. **Component tokens are explicitly flagged "(in development)"** on the design-tokens page — treat
|
||||
component-token completeness claims cautiously.
|
||||
6. Two task-provided sub-page slugs did not exist as separate URLs: `design-tokens/overview`,
|
||||
`/how-to-read-tokens`, `/how-to-use-tokens` all collapse into the single `foundations/design-tokens`
|
||||
page; `layout/understanding-layout/*` and `layout/applying-layout/*` do not exist — the real nesting is
|
||||
`layout/layout-overview/parts-of-layout`, `layout/scaffold/{bars,rails,panes}`, and
|
||||
`layout/grids-spacing/{grids,spacing,density}` (all fetched and covered above).
|
||||
@@ -0,0 +1,238 @@
|
||||
https://m3.material.io/components
|
||||
https://m3.material.io/components/all-buttons
|
||||
https://m3.material.io/components/app-bars/accessibility
|
||||
https://m3.material.io/components/app-bars/guidelines
|
||||
https://m3.material.io/components/app-bars/overview
|
||||
https://m3.material.io/components/app-bars/specs
|
||||
https://m3.material.io/components/badges/accessibility
|
||||
https://m3.material.io/components/badges/guidelines
|
||||
https://m3.material.io/components/badges/overview
|
||||
https://m3.material.io/components/badges/specs
|
||||
https://m3.material.io/components/bottom-sheets/accessibility
|
||||
https://m3.material.io/components/bottom-sheets/guidelines
|
||||
https://m3.material.io/components/bottom-sheets/overview
|
||||
https://m3.material.io/components/bottom-sheets/specs
|
||||
https://m3.material.io/components/button-groups/accessibility
|
||||
https://m3.material.io/components/button-groups/guidelines
|
||||
https://m3.material.io/components/button-groups/overview
|
||||
https://m3.material.io/components/button-groups/specs
|
||||
https://m3.material.io/components/buttons/accessibility
|
||||
https://m3.material.io/components/buttons/guidelines
|
||||
https://m3.material.io/components/buttons/overview
|
||||
https://m3.material.io/components/buttons/specs
|
||||
https://m3.material.io/components/cards/accessibility
|
||||
https://m3.material.io/components/cards/guidelines
|
||||
https://m3.material.io/components/cards/overview
|
||||
https://m3.material.io/components/cards/specs
|
||||
https://m3.material.io/components/carousel/accessibility
|
||||
https://m3.material.io/components/carousel/guidelines
|
||||
https://m3.material.io/components/carousel/overview
|
||||
https://m3.material.io/components/carousel/specs
|
||||
https://m3.material.io/components/checkbox/accessibility
|
||||
https://m3.material.io/components/checkbox/guidelines
|
||||
https://m3.material.io/components/checkbox/overview
|
||||
https://m3.material.io/components/checkbox/specs
|
||||
https://m3.material.io/components/chips/accessibility
|
||||
https://m3.material.io/components/chips/guidelines
|
||||
https://m3.material.io/components/chips/overview
|
||||
https://m3.material.io/components/chips/specs
|
||||
https://m3.material.io/components/date-pickers/accessibility
|
||||
https://m3.material.io/components/date-pickers/guidelines
|
||||
https://m3.material.io/components/date-pickers/overview
|
||||
https://m3.material.io/components/date-pickers/specs
|
||||
https://m3.material.io/components/dialogs/accessibility
|
||||
https://m3.material.io/components/dialogs/guidelines
|
||||
https://m3.material.io/components/dialogs/overview
|
||||
https://m3.material.io/components/dialogs/specs
|
||||
https://m3.material.io/components/divider/accessibility
|
||||
https://m3.material.io/components/divider/guidelines
|
||||
https://m3.material.io/components/divider/overview
|
||||
https://m3.material.io/components/divider/specs
|
||||
https://m3.material.io/components/extended-fab/accessibility
|
||||
https://m3.material.io/components/extended-fab/guidelines
|
||||
https://m3.material.io/components/extended-fab/overview
|
||||
https://m3.material.io/components/extended-fab/specs
|
||||
https://m3.material.io/components/fab-menu/accessibility
|
||||
https://m3.material.io/components/fab-menu/guidelines
|
||||
https://m3.material.io/components/fab-menu/overview
|
||||
https://m3.material.io/components/fab-menu/specs
|
||||
https://m3.material.io/components/floating-action-button/accessibility
|
||||
https://m3.material.io/components/floating-action-button/guidelines
|
||||
https://m3.material.io/components/floating-action-button/overview
|
||||
https://m3.material.io/components/floating-action-button/specs
|
||||
https://m3.material.io/components/icon-buttons/accessibility
|
||||
https://m3.material.io/components/icon-buttons/guidelines
|
||||
https://m3.material.io/components/icon-buttons/overview
|
||||
https://m3.material.io/components/icon-buttons/specs
|
||||
https://m3.material.io/components/lists/accessibility
|
||||
https://m3.material.io/components/lists/guidelines
|
||||
https://m3.material.io/components/lists/overview
|
||||
https://m3.material.io/components/lists/specs
|
||||
https://m3.material.io/components/loading-indicator/accessibility
|
||||
https://m3.material.io/components/loading-indicator/guidelines
|
||||
https://m3.material.io/components/loading-indicator/overview
|
||||
https://m3.material.io/components/loading-indicator/specs
|
||||
https://m3.material.io/components/menus/accessibility
|
||||
https://m3.material.io/components/menus/guidelines
|
||||
https://m3.material.io/components/menus/overview
|
||||
https://m3.material.io/components/menus/specs
|
||||
https://m3.material.io/components/navigation-bar/accessibility
|
||||
https://m3.material.io/components/navigation-bar/guidelines
|
||||
https://m3.material.io/components/navigation-bar/overview
|
||||
https://m3.material.io/components/navigation-bar/specs
|
||||
https://m3.material.io/components/navigation-drawer/accessibility
|
||||
https://m3.material.io/components/navigation-drawer/guidelines
|
||||
https://m3.material.io/components/navigation-drawer/overview
|
||||
https://m3.material.io/components/navigation-drawer/specs
|
||||
https://m3.material.io/components/navigation-rail/accessibility
|
||||
https://m3.material.io/components/navigation-rail/guidelines
|
||||
https://m3.material.io/components/navigation-rail/overview
|
||||
https://m3.material.io/components/navigation-rail/specs
|
||||
https://m3.material.io/components/progress-indicators/accessibility
|
||||
https://m3.material.io/components/progress-indicators/guidelines
|
||||
https://m3.material.io/components/progress-indicators/overview
|
||||
https://m3.material.io/components/progress-indicators/specs
|
||||
https://m3.material.io/components/radio-button/accessibility
|
||||
https://m3.material.io/components/radio-button/guidelines
|
||||
https://m3.material.io/components/radio-button/overview
|
||||
https://m3.material.io/components/radio-button/specs
|
||||
https://m3.material.io/components/search/accessibility
|
||||
https://m3.material.io/components/search/guidelines
|
||||
https://m3.material.io/components/search/overview
|
||||
https://m3.material.io/components/search/specs
|
||||
https://m3.material.io/components/segmented-buttons/accessibility
|
||||
https://m3.material.io/components/segmented-buttons/guidelines
|
||||
https://m3.material.io/components/segmented-buttons/overview
|
||||
https://m3.material.io/components/segmented-buttons/specs
|
||||
https://m3.material.io/components/side-sheets/accessibility
|
||||
https://m3.material.io/components/side-sheets/guidelines
|
||||
https://m3.material.io/components/side-sheets/overview
|
||||
https://m3.material.io/components/side-sheets/specs
|
||||
https://m3.material.io/components/sliders/accessibility
|
||||
https://m3.material.io/components/sliders/guidelines
|
||||
https://m3.material.io/components/sliders/overview
|
||||
https://m3.material.io/components/sliders/specs
|
||||
https://m3.material.io/components/snackbar/accessibility
|
||||
https://m3.material.io/components/snackbar/guidelines
|
||||
https://m3.material.io/components/snackbar/overview
|
||||
https://m3.material.io/components/snackbar/specs
|
||||
https://m3.material.io/components/split-button/accessibility
|
||||
https://m3.material.io/components/split-button/guidelines
|
||||
https://m3.material.io/components/split-button/overview
|
||||
https://m3.material.io/components/split-button/specs
|
||||
https://m3.material.io/components/switch/accessibility
|
||||
https://m3.material.io/components/switch/guidelines
|
||||
https://m3.material.io/components/switch/overview
|
||||
https://m3.material.io/components/switch/specs
|
||||
https://m3.material.io/components/tabs/accessibility
|
||||
https://m3.material.io/components/tabs/guidelines
|
||||
https://m3.material.io/components/tabs/overview
|
||||
https://m3.material.io/components/tabs/specs
|
||||
https://m3.material.io/components/text-fields/accessibility
|
||||
https://m3.material.io/components/text-fields/guidelines
|
||||
https://m3.material.io/components/text-fields/overview
|
||||
https://m3.material.io/components/text-fields/specs
|
||||
https://m3.material.io/components/time-pickers/accessibility
|
||||
https://m3.material.io/components/time-pickers/guidelines
|
||||
https://m3.material.io/components/time-pickers/overview
|
||||
https://m3.material.io/components/time-pickers/specs
|
||||
https://m3.material.io/components/toolbars/accessibility
|
||||
https://m3.material.io/components/toolbars/guidelines
|
||||
https://m3.material.io/components/toolbars/overview
|
||||
https://m3.material.io/components/toolbars/specs
|
||||
https://m3.material.io/components/tooltips/accessibility
|
||||
https://m3.material.io/components/tooltips/guidelines
|
||||
https://m3.material.io/components/tooltips/overview
|
||||
https://m3.material.io/components/tooltips/specs
|
||||
https://m3.material.io/foundations
|
||||
https://m3.material.io/foundations/building-for-all/co-design
|
||||
https://m3.material.io/foundations/building-for-all/user-needs
|
||||
https://m3.material.io/foundations/content-design/alt-text
|
||||
https://m3.material.io/foundations/content-design/global-writing/overview
|
||||
https://m3.material.io/foundations/content-design/global-writing/word-choice
|
||||
https://m3.material.io/foundations/content-design/notifications
|
||||
https://m3.material.io/foundations/content-design/overview
|
||||
https://m3.material.io/foundations/content-design/style-guide/grammar-and-punctuation
|
||||
https://m3.material.io/foundations/content-design/style-guide/ux-writing-best-practices
|
||||
https://m3.material.io/foundations/content-design/style-guide/word-choice
|
||||
https://m3.material.io/foundations/customization
|
||||
https://m3.material.io/foundations/design-tokens/how-to-use-tokens
|
||||
https://m3.material.io/foundations/design-tokens/overview
|
||||
https://m3.material.io/foundations/designing/color-contrast
|
||||
https://m3.material.io/foundations/designing/elements
|
||||
https://m3.material.io/foundations/designing/flow
|
||||
https://m3.material.io/foundations/designing/overview
|
||||
https://m3.material.io/foundations/designing/structure
|
||||
https://m3.material.io/foundations/glossary
|
||||
https://m3.material.io/foundations/interaction/gestures
|
||||
https://m3.material.io/foundations/interaction/inputs
|
||||
https://m3.material.io/foundations/interaction/selection
|
||||
https://m3.material.io/foundations/interaction/states/applying-states
|
||||
https://m3.material.io/foundations/interaction/states/overview
|
||||
https://m3.material.io/foundations/interaction/states/state-layers
|
||||
https://m3.material.io/foundations/layout/bidirectionality-rtl
|
||||
https://m3.material.io/foundations/layout/breakpoints/compact
|
||||
https://m3.material.io/foundations/layout/breakpoints/expanded
|
||||
https://m3.material.io/foundations/layout/breakpoints/large-extra-large
|
||||
https://m3.material.io/foundations/layout/breakpoints/medium
|
||||
https://m3.material.io/foundations/layout/breakpoints/overview
|
||||
https://m3.material.io/foundations/layout/canonical-examples/feed
|
||||
https://m3.material.io/foundations/layout/canonical-examples/list-detail
|
||||
https://m3.material.io/foundations/layout/canonical-examples/overview
|
||||
https://m3.material.io/foundations/layout/canonical-examples/supporting-pane
|
||||
https://m3.material.io/foundations/layout/grids-spacing/density
|
||||
https://m3.material.io/foundations/layout/grids-spacing/grids
|
||||
https://m3.material.io/foundations/layout/grids-spacing/overview
|
||||
https://m3.material.io/foundations/layout/grids-spacing/spacing
|
||||
https://m3.material.io/foundations/layout/layout-overview/adaptive-design
|
||||
https://m3.material.io/foundations/layout/layout-overview/overview
|
||||
https://m3.material.io/foundations/layout/layout-overview/parts-of-layout
|
||||
https://m3.material.io/foundations/layout/scaffold/bars
|
||||
https://m3.material.io/foundations/layout/scaffold/overview
|
||||
https://m3.material.io/foundations/layout/scaffold/panes
|
||||
https://m3.material.io/foundations/layout/scaffold/rails
|
||||
https://m3.material.io/foundations/overview/assistive-technology
|
||||
https://m3.material.io/foundations/overview/principles
|
||||
https://m3.material.io/foundations/usability/applying-m3-expressive
|
||||
https://m3.material.io/foundations/usability/overview
|
||||
https://m3.material.io/foundations/writing/best-practices
|
||||
https://m3.material.io/foundations/writing/text-resizing
|
||||
https://m3.material.io/foundations/writing/text-truncation
|
||||
https://m3.material.io/styles
|
||||
https://m3.material.io/styles/color/advanced/adjust-existing-colors
|
||||
https://m3.material.io/styles/color/advanced/apply-colors
|
||||
https://m3.material.io/styles/color/advanced/define-new-colors
|
||||
https://m3.material.io/styles/color/advanced/overview
|
||||
https://m3.material.io/styles/color/choosing-a-scheme
|
||||
https://m3.material.io/styles/color/dynamic/choosing-a-source
|
||||
https://m3.material.io/styles/color/dynamic/content-based-source
|
||||
https://m3.material.io/styles/color/dynamic/user-generated-source
|
||||
https://m3.material.io/styles/color/resources
|
||||
https://m3.material.io/styles/color/roles
|
||||
https://m3.material.io/styles/color/static/baseline
|
||||
https://m3.material.io/styles/color/static/custom-brand
|
||||
https://m3.material.io/styles/color/system/how-the-system-works
|
||||
https://m3.material.io/styles/color/system/overview
|
||||
https://m3.material.io/styles/elevation/applying-elevation
|
||||
https://m3.material.io/styles/elevation/overview
|
||||
https://m3.material.io/styles/elevation/tokens
|
||||
https://m3.material.io/styles/icons/applying-icons
|
||||
https://m3.material.io/styles/icons/designing-icons
|
||||
https://m3.material.io/styles/icons/overview
|
||||
https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration
|
||||
https://m3.material.io/styles/motion/easing-and-duration/tokens-specs
|
||||
https://m3.material.io/styles/motion/overview/how-it-works
|
||||
https://m3.material.io/styles/motion/overview/specs
|
||||
https://m3.material.io/styles/motion/transitions/applying-transitions
|
||||
https://m3.material.io/styles/motion/transitions/transition-patterns
|
||||
https://m3.material.io/styles/shape/corner-radius-scale
|
||||
https://m3.material.io/styles/shape/overview-principles
|
||||
https://m3.material.io/styles/shape/shape-morph
|
||||
https://m3.material.io/styles/spacing/applying-spacing
|
||||
https://m3.material.io/styles/spacing/overview
|
||||
https://m3.material.io/styles/spacing/tokens
|
||||
https://m3.material.io/styles/typography/applying-type
|
||||
https://m3.material.io/styles/typography/editorial-treatments
|
||||
https://m3.material.io/styles/typography/fonts
|
||||
https://m3.material.io/styles/typography/overview
|
||||
https://m3.material.io/styles/typography/type-scale-tokens
|
||||
@@ -0,0 +1,318 @@
|
||||
# Material Design 3 — Styles Supplement (Spacing; Color additions; Motion transition patterns)
|
||||
|
||||
Extraction from sitemap pages `reference-styles.md` did not cover (fetched directly, not via in-site
|
||||
navigation). Same structure as the reference. Only material **not** already in `reference-styles.md` is
|
||||
included; where a fetched page turned out to be a duplicate of an already-covered page, that is noted
|
||||
instead of repeating it.
|
||||
|
||||
**Duplicate pages found (no new content beyond what's already in the reference):**
|
||||
- `styles/color/system/overview` — byte-identical to `styles/color/system` (already in the reference's
|
||||
Color section / Source URLs), *except* it also carries a Resources table not previously captured — see
|
||||
Color § Values below.
|
||||
- `styles/color/advanced/overview` — byte-identical to `styles/color/advanced` (already in the reference's
|
||||
Color § Source URLs; its linked sub-pages apply-colors/define-new-colors/adjust-existing-colors are
|
||||
already the basis of the reference's "Advanced customization best practices" rules).
|
||||
- `styles/color/dynamic/choosing-a-source` — byte-identical to `styles/color/dynamic` (already in the
|
||||
reference's Color § Source URLs), *except* it's the canonical location for the source-selection
|
||||
decision criteria — see Color § Dynamic sources below (the reference didn't extract these criteria).
|
||||
- `styles/motion/easing-and-duration/applying-easing-and-duration` — its "Suggested easing and duration
|
||||
pairs" table, "Choosing an easing set", "Choosing an easing type" (by transition direction), and
|
||||
"Choosing a duration" (by size / enter vs. exit) sections are already reproduced verbatim in the
|
||||
reference's Motion § Rules and § Values. The only new fact is an M2-vs-M3 style comparison — see
|
||||
Motion § Principles below.
|
||||
|
||||
---
|
||||
|
||||
## Spacing
|
||||
|
||||
Absent from `reference-styles.md` entirely — no Spacing section existed. This is a full addition.
|
||||
|
||||
### Principles
|
||||
|
||||
- Spacing is applied to the margins, padding, and gaps of a component, UI element, or layout.
|
||||
- Spacing adapts to different values based on context, like mobile vs. desktop, or density settings.
|
||||
- The spacing system is measured on an **8dp scale**, where **space100 = 8dp**.
|
||||
- Spacing is applied to the flow of elements (horizontal, vertical) or in relation to elements (leading,
|
||||
trailing, top, bottom, gap).
|
||||
- Unlike the color system (which adjusts light/dark theme logic across all components at once), spacing
|
||||
logic is tailored and built within each component individually — there's no single global spacing
|
||||
transform.
|
||||
- Components have padding/margin/gap tokens that map to system spacing tokens, which resolve to final dp
|
||||
values (component token → system token → value).
|
||||
|
||||
### Rules (Google's wording)
|
||||
|
||||
- Spacing has three categories: **padding** (space inside an element), **gap** (space between elements in
|
||||
a grid or container), and **margin** (space outside an element). Position can be vertical, top, bottom,
|
||||
horizontal, leading, or trailing. **Leading**/**trailing** swap sides in RTL languages.
|
||||
- **Do** define padding and gaps on the parent container to organize all elements inside.
|
||||
- **Don't** define margins on child elements — they usually aren't uniform and require more tokens.
|
||||
- **Use padding & gaps before using margins**: Material rarely uses margins in components; padding/gaps
|
||||
apply spacing more uniformly. Only use margins for spacing beyond the parent container's padding, or in
|
||||
layouts.
|
||||
- Use a horizontal/vertical gap for simple components where gaps are always the same size; complex
|
||||
components with many different gaps should name them by the elements on each side (e.g. "icon-label
|
||||
gap") rather than a single generic gap token.
|
||||
- When text is scaled up to 200%, the same spacing should be preserved by default (spacing does not scale
|
||||
with text).
|
||||
- What to use, in priority order:
|
||||
1. **Pre-tokenized components** — some Material components map to spacing system tokens out of the box
|
||||
(work is ongoing to hook up all components; this mapping can be customized for form factor/density).
|
||||
2. **System tokens** — apply these to custom components/layouts, replacing hardcoded values.
|
||||
3. If the right system token doesn't exist, **customize the system** (see below) and add your own.
|
||||
- Customizing the system (three approaches, by scope):
|
||||
- **Customize Material's existing component spacing** — remap a component attribute to a different
|
||||
system token product-wide (e.g. change "button top padding" from **space125** to **space200** for a
|
||||
taller default button).
|
||||
- **Add custom system spacing & patterns** — when you need units beyond what Material provides, or have
|
||||
a recurring adaptive pattern. Follow the multiplier convention for new tokens (e.g. **space225 = 18dp**
|
||||
= 8dp × 2.25). Product-specific recurring patterns can get their own pattern token (e.g. if cards and
|
||||
sheets always adapt horizontal content padding the same way, create a **surface-content padding
|
||||
horizontal** token for that pattern).
|
||||
- **Add adaptive layout & density** — map the same component to different system tokens per device type
|
||||
(mobile vs. desktop), and/or adapt vertical padding to different values per density setting, while
|
||||
keeping the same core component.
|
||||
- **Note:** system spacing tokens are currently only used on **Jetpack Compose** (see Availability below)
|
||||
— this is a stronger platform restriction than color/elevation/shape, which have broader (if uneven)
|
||||
support.
|
||||
|
||||
### Values
|
||||
|
||||
#### Availability
|
||||
|
||||
| Type | Resource | Status |
|
||||
| --- | --- | --- |
|
||||
| Implementation | Android Views (MDC-Android) | Unavailable |
|
||||
| Implementation | Jetpack Compose | Available |
|
||||
| Implementation | Web | Unavailable |
|
||||
|
||||
#### Spacing scale
|
||||
|
||||
The site does not render the token→dp table as text (it's an image diagram); the following is
|
||||
reconstructed from the images' alt-text and from dp values cited inline elsewhere on the page, and should
|
||||
be treated as inferred, not a verbatim site table:
|
||||
|
||||
| Token | Multiplier | Value |
|
||||
| --- | --- | --- |
|
||||
| space25 | 0.25× | 2dp |
|
||||
| space50 | 0.5× | 4dp |
|
||||
| space75 | 0.75× | 6dp |
|
||||
| **space100** | **1×** | **8dp (base unit, `md.sys.measurement.space100`)** |
|
||||
| space125 | 1.25× | 10dp |
|
||||
| space200 | 2× | 16dp |
|
||||
| space300 | 3× | 24dp |
|
||||
| space400 | 4× | 32dp |
|
||||
| space500 | 5× | 40dp |
|
||||
| space600 | 6× | 48dp |
|
||||
| space700 | 7× | 56dp |
|
||||
| space800 | 8× | 64dp |
|
||||
| space900 | 9× | 72dp |
|
||||
|
||||
Basis for this reconstruction: the overview page's scale diagram alt-text reads "a spacing scale shows 2,
|
||||
4, 6, and 8 at the bottom range and 48, 56, 64, and 72 at the top of the range"; the tokens page states the
|
||||
main range "covers 0x to 9x" of the 8dp base unit and separately defines nested sub-1x units at "0.25x,
|
||||
0.5x, 0.75x, 1.25x"; the overview page separately lists the same nested dp values ("2dp, 4dp, 6dp, and
|
||||
10dp"). Values beyond space900 (e.g. the 18dp/space225 example) are explicitly presented as **custom**,
|
||||
not part of the default set. Material only defines the nested/nonstandard units it actively uses — the
|
||||
scale is not a complete arithmetic progression at every step.
|
||||
|
||||
#### Component spacing — naming convention
|
||||
|
||||
| | Going forward | Legacy |
|
||||
| --- | --- | --- |
|
||||
| Prefix | `padding`, `margin`, `gap` | `space` |
|
||||
| Positional modifiers | horizontal, vertical, leading, trailing, top, bottom | leading-space, trailing-space, top-space, bottom-space, between-space |
|
||||
| Example | "Medium button: leading padding" | "Medium button: leading space" |
|
||||
|
||||
#### Parts of spacing — example (Search)
|
||||
|
||||
The search container: 8dp vertical padding, 8dp horizontal gaps, 24dp horizontal margins by default
|
||||
(narrows to 12dp when focused, while padding/gaps stay the same). The 24dp default margin exists
|
||||
specifically "to ensure accurate placement from the screen edge."
|
||||
|
||||
#### Spacing concepts diagrammed on the site (numbered image captions — anatomy, not values)
|
||||
|
||||
- Component layout: (1) vertical padding top & bottom, (2) vertical gap, (3) horizontal padding leading &
|
||||
trailing, (4) horizontal gap.
|
||||
- Page layout: (1) margin, (2) top padding, (3) horizontal padding leading & trailing, (4) spacer/gap,
|
||||
(5) vertical gap. Layouts additionally use **panes** (layout containers, which can be fixed, flexible,
|
||||
floating, or semi-permanent) and **spacers** (space between two panes; contains a drag handle if the
|
||||
panes are resizable).
|
||||
|
||||
### M3 vs M3 Expressive differences
|
||||
|
||||
No Expressive-specific spacing changes are called out on these pages (unlike shape/motion/typography,
|
||||
spacing carries no "M3 Expressive" labelled update section). The system is presented as a single,
|
||||
version-agnostic linear scale.
|
||||
|
||||
### Accessibility
|
||||
|
||||
- Spacing must be preserved (not scaled) when text is scaled up to 200%, so layouts stay stable and
|
||||
legible as text grows — spacing is a support mechanism for text-resizing accessibility, not something
|
||||
that competes with it.
|
||||
|
||||
### Source URLs
|
||||
|
||||
- https://m3.material.io/styles/spacing/overview
|
||||
- https://m3.material.io/styles/spacing/applying-spacing
|
||||
- https://m3.material.io/styles/spacing/tokens
|
||||
|
||||
---
|
||||
|
||||
## Color — additions
|
||||
|
||||
### Static: Custom brand color scheme
|
||||
|
||||
Not previously fetched. Supplements the reference's Color section (which covers baseline/static generally
|
||||
but not the brand-specific workflow).
|
||||
|
||||
**Principles**
|
||||
- A **brand-based static scheme** is hand-picked by the product team to align with the product's brand
|
||||
color(s), as opposed to the **baseline** static scheme (Material's own default) or a **dynamic** scheme.
|
||||
It requires greater ongoing investment since the team creates and maintains it entirely.
|
||||
- End-users see: an accessible UI with static colors, and "a product that looks like its brand."
|
||||
|
||||
**Rules (Google's wording)**
|
||||
- Brand schemes are still generated through the same tonal-palette machinery as dynamic color: you can
|
||||
set a **custom source color independently for each of the 6 palette groups** — Primary, Secondary,
|
||||
Tertiary, Error, Neutral, and Neutral Variant — not just Primary. Setting only Primary and leaving the
|
||||
rest defaulted is also valid ("use the scheme as-is").
|
||||
- Workflow (Material Theme Builder Figma plugin): add a new theme, name it (the name becomes the color
|
||||
role prefix in Figma), set a custom Hex source color for Primary (and optionally Secondary/Tertiary/
|
||||
Error/Neutral/Neutral Variant) → generates a full custom color scheme with the same role set as baseline.
|
||||
- Enabling "Generate State Layers" in the plugin settings when creating the theme is called out as
|
||||
necessary groundwork for interaction-state design (cross-references state layers).
|
||||
- Point of escalation: "want to further adjust your brand color scheme?" and "need to make adjustments?"
|
||||
both route to **Advanced customizations** (apply/define/adjust colors) — i.e. custom-brand is the
|
||||
starting point, advanced customization is the refinement path.
|
||||
- Develop: export the branded scheme from Material Theme Builder for Jetpack Compose, Android Views,
|
||||
Flutter, Web, or as a JSON file.
|
||||
|
||||
**Source URLs**
|
||||
- https://m3.material.io/styles/color/static/custom-brand
|
||||
|
||||
### Dynamic: choosing and using a source
|
||||
|
||||
Two new sub-pages (content-based-source, user-generated-source) plus decision criteria from
|
||||
choosing-a-source that the reference's Color section states only as a single pipeline description, not as
|
||||
selection criteria.
|
||||
|
||||
**Principles**
|
||||
- Both user-generated and content-based color go through the same pipeline: the image is digitally
|
||||
analyzed through **quantization**, a single color is selected as the source color, and tones are chosen
|
||||
and assigned to each color role (this is the concrete mechanism behind the "dynamic color pipeline"
|
||||
already summarized in the reference).
|
||||
- **User-generated color**: sourced from the user's wallpaper.
|
||||
- **Content-based color**: sourced from in-app content — an album thumbnail, a logo, a video preview.
|
||||
|
||||
**Rules (Google's wording) — decision criteria (not previously captured)**
|
||||
|
||||
| Source type | Choose it if… |
|
||||
| --- | --- |
|
||||
| User-generated | Users would benefit from a personalized experience that's well-tested; you want to showcase the latest Material features |
|
||||
| Content-based | Content is front-and-center in the product; the team can do some advanced customization; it supports usability of content-centric features like media players; best applied to contained screen elements adjacent to the source image (the source image itself need not stay visible) |
|
||||
| Multiple sources | Product requirements meet more than one of the above criteria and the team doesn't mind extra customization work; get started with user-generated color first, then customize |
|
||||
|
||||
- Example of multiple sources coexisting: a photo app derives content-based color (green) for edit buttons
|
||||
from the in-app photo, while wallpaper-derived user-generated color (red) drives share buttons elsewhere
|
||||
in the same screen.
|
||||
- Content-based color guidance: apply it where it enhances brand identity and personalization (e.g. a
|
||||
music app deriving color from album artwork; a news app differentiating publications by their content
|
||||
color).
|
||||
- QA/testing step for user-generated color (Figma workflow): use the Material Theme Builder's "Dynamic"
|
||||
mode, add a real image or use the Shuffle icon for a random source color, swap it onto frames, and
|
||||
**repeat across a range of colors** to get a sense of how the product will appear across different
|
||||
users' devices/wallpapers — i.e. dynamic-color design review should be done across many source colors,
|
||||
not just one or two.
|
||||
- Android implementation reference specifically for content-based color: MDC-Android's
|
||||
"Content-based dynamic color" doc (separate from the general dynamic-color doc already cited).
|
||||
|
||||
**Source URLs**
|
||||
- https://m3.material.io/styles/color/dynamic/choosing-a-source
|
||||
- https://m3.material.io/styles/color/dynamic/content-based-source
|
||||
- https://m3.material.io/styles/color/dynamic/user-generated-source
|
||||
|
||||
---
|
||||
|
||||
## Motion — Transitions (supplement)
|
||||
|
||||
The reference's Motion section already reproduces the easing/duration Rules and Values from
|
||||
`applying-easing-and-duration` verbatim, and names the six transition patterns in one summary line without
|
||||
detail. This section supplies the missing detail: what each pattern is for, what it's commonly used with,
|
||||
platform (Android/iOS) differences, and the accessibility/consistency criteria for transitions overall —
|
||||
which also fills the gap the reference explicitly flagged ("No general reduced-motion policy was found
|
||||
stated on `styles/motion` itself").
|
||||
|
||||
**Naming note:** the current M3 site does **not** use the older M2 terms "shared axis / fade through /
|
||||
fade." The current (2026) taxonomy is: **Container transform**, **Forward and backward** (≈ old shared
|
||||
axis), **Lateral**, **Top level** (≈ old fade through), **Enter and exit** (subsumes old "fade" cases), and
|
||||
**Skeleton loaders** (new, not an M2 concept). Anyone looking for "shared axis"/"fade through"/"fade" by
|
||||
name should map to this table.
|
||||
|
||||
### Principles — what makes a good transition
|
||||
|
||||
- **Follows accessibility settings**: when a platform's reduced-motion setting is on, transitions should
|
||||
use subtle fades instead of intense sliding/scaling, and disable decorative effects like parallax or
|
||||
shape morphing. *(This is the missing general reduced-motion policy for `styles/motion` — previously
|
||||
only found at the component level, e.g. carousel/menus.)*
|
||||
- **Consistent**: applying the same transition type consistently for the same kind of navigation makes
|
||||
apps feel cohesive and predictable (e.g. multiple Android apps sharing one forward/backward transition).
|
||||
- **Stable layouts**: use skeleton loaders (subtle pulsing) so content doesn't shift position or pop in
|
||||
abruptly as it loads.
|
||||
- **No jarring jump cuts**: avoid instant, uncued transitions by default — they're disorienting. Exception:
|
||||
a jump cut may be preferred when pure efficiency is the top priority (e.g. opening a menu in a
|
||||
productivity app).
|
||||
- **Coherent spatial model**: transitions should help users understand an app's physical/spatial layout
|
||||
(e.g. keep a consistent axis when moving between collapsed/expanded carousel views; don't flip between
|
||||
horizontal and vertical layouts).
|
||||
- **Unified direction**: elements are grouped and move along one primary axis rather than independently;
|
||||
only genuinely persistent elements (e.g. a hero image) should remain visible throughout — animating many
|
||||
persistent elements independently is distracting.
|
||||
- **Clean fades**: fully fade out old content before fading in new content; avoid overlapping
|
||||
partially-transparent frames. If a cross-fade is unavoidable, keep it quick and hide it during the
|
||||
fastest part of the transition (e.g. a dialog entering mid-screen should not slowly fade on top of
|
||||
content — use a short fade to hide the overlap).
|
||||
- **Simple style**: transitions are frequent and utility-driven, not a showcase for stylized motion — avoid
|
||||
overt effects like bouncy springs on common transitions.
|
||||
|
||||
### Rules — per-pattern guidance (Google's wording)
|
||||
|
||||
| Pattern | Use for | Commonly used with | Do / Don't / Caution |
|
||||
| --- | --- | --- | --- |
|
||||
| **Container transform** | Seamlessly transforming an element to show more detail (e.g. a card expanding into a details page); hero moments that should be expressive; shallow hierarchies (expand for detail, then collapse); creating a seamless connection between elements. Most dramatic/expressive pattern — reserve for the right context. | Cards, lists, image galleries, search boxes, sheets, FABs, chips | **Do** use it for hero moments instead of a forward/backward transition. **Don't** use it in apps with deep hierarchies (motion becomes excessive) or in utility-focused navigation (style mismatch). |
|
||||
| **Forward and backward** | Navigating between screens at consecutive hierarchy levels (e.g. inbox → message thread) | Lists, cards, buttons, links | **Do** use platform defaults — easy to implement, stays current with platform updates. Android fades content as screens slide (reduces motion vs. a full-width slide); iOS uses parallax (background slides slower than foreground). **Caution**: container transform requires custom implementation and can feel excessive if overused for this role. |
|
||||
| **Lateral** | Navigating between peer content at the same hierarchy level (e.g. swiping tabs in a content library) | Tabs, carousels, image galleries | Elements are grouped and slide in unison with **no fade or parallax**, creating a strong peer relationship and hinting at swipe-ability. **Caution**: fading content while it slides weakens the peer/swipe cue and can be confused with forward/backward. **Don't** use for hierarchical navigation — full-width sliding is excessive and wrongly implies a peer (non-hierarchical) relationship. |
|
||||
| **Top level** | Navigating between top-level destinations (e.g. tapping a nav bar/rail/drawer item) | Navigation bar, navigation rail, navigation drawer | Exiting screen quickly fades out, then the entering screen fades in — intentionally **no** grouping or persistent elements, since top-level destinations aren't necessarily related. **Don't** use a lateral transition here — it wrongly implies you can swipe between top-level destinations, conflicting with carousel/list swipe gestures. |
|
||||
| **Enter and exit — within screen bounds** | Introducing/removing a component in the context of the main UI (modal, e.g. a dialog; or non-modal, e.g. a bottom sheet coexisting with content) | FABs, dialogs, menus, snackbars, time pickers, tooltips | Android: expand/collapse along x or y axis only (scale and z-axis motion are avoided — they'd imply an elevation change, which conflicts with M3's reduced-elevation model). iOS: uniform scale to enter, fade to exit. Enter direction is informed by on-screen location, expanding away from the device edge (e.g. a top menu expands downward; a bottom snackbar expands upward). **Don't** use this pattern for navigating hierarchical screens — full-height sliding is excessive and creates an unclear screen-to-screen relationship. |
|
||||
| **Enter and exit — beyond screen bounds** | Components that slide fully on/off screen | App bars, banners, navigation bar, navigation rail, navigation drawer, sheets | Android: expand/collapse along x or y axis while sliding on/off (adds shape emphasis). iOS: slides on/off without changing shape. Coplanar side sheets shrink the available content area rather than overlaying it. Can be scroll-driven (e.g. a top app bar or nav bar sliding off/on during scroll, to reclaim screen space). Entry/exit location establishes spatial memory: notifications enter from the top (matches the pull-down drawer), a nav drawer enters from the left (matches its off-screen position), a bottom sheet/keyboard enters from the bottom (easiest to reach). |
|
||||
| **Skeleton loaders** | Transitioning from a temporary loading state to the fully loaded UI | Used in combination with other transition patterns | Subtle pulsing animation indicates indeterminate progress, animating from top-left to bottom-right. Once content loads, it quickly fades in **on top of** the skeleton loader (not instantly popping in). |
|
||||
|
||||
### Values
|
||||
|
||||
Durations and easing for these patterns are governed by the already-referenced tables (Suggested
|
||||
easing/duration pairs; legacy duration tokens) — no separate duration table is given per pattern on these
|
||||
pages. The pattern choice itself is the "value" documented here; see the Rules table above for the
|
||||
authoritative per-pattern mapping (this *is* the durations/patterns cross-reference the task asked for —
|
||||
the site does not tie a specific numeric duration to "container transform" etc. beyond the general
|
||||
begin/end-on-screen vs. enter vs. exit rules already in the reference).
|
||||
|
||||
### M2 vs M3 easing note (from `applying-easing-and-duration`; not an M3-vs-Expressive distinction)
|
||||
|
||||
- Compared to M2's more utilitarian easing style, **M3 easing is more expressive**: transitions have
|
||||
"snappy take-offs and very soft landings."
|
||||
- **M3 durations are slightly longer than M2's**, deliberately, to give transitions time to come to a
|
||||
gentle rest without feeling abrupt.
|
||||
|
||||
### Accessibility requirements
|
||||
|
||||
- General reduced-motion policy for transitions (see Principles above): substitute subtle fades for
|
||||
intense sliding/scaling, and disable decorative effects (parallax, shape morphing) when a platform's
|
||||
reduced-motion setting is on. This generalizes — and predates in priority — the component-level
|
||||
reduced-motion notes already in the reference (carousel, menus).
|
||||
|
||||
### Source URLs
|
||||
|
||||
- https://m3.material.io/styles/motion/transitions/applying-transitions
|
||||
- https://m3.material.io/styles/motion/transitions/transition-patterns
|
||||
- https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration (mostly duplicate
|
||||
of already-referenced content; see M2-vs-M3 note above for the one new fact)
|
||||
@@ -0,0 +1,896 @@
|
||||
# Material Design 3 — Styles Reference (Color, Elevation, Icons, Motion, Shape, Typography)
|
||||
|
||||
Ground-truth extraction from m3.material.io/styles and its sub-pages, plus androidx Compose Material3
|
||||
token source files where the site only renders a value inside an unrendered interactive widget.
|
||||
Anything not from the live site text is explicitly marked "(not on the site; from <source>)".
|
||||
|
||||
---
|
||||
|
||||
## Color
|
||||
|
||||
### Principles
|
||||
|
||||
- Color is used to express style and communicate meaning. With dynamic color, Material puts personal
|
||||
color preferences and individual needs at the forefront of systematic color application.
|
||||
- The Material color system includes: a built-in set of accessible color relationships (e.g. a dark
|
||||
surface color is algorithmically paired with a light text-label color so the UI automatically meets
|
||||
contrast requirements); 26+ color roles mapped to components; a built-in dark theme; a static baseline
|
||||
color scheme; and dynamic color (user-generated from wallpaper, or content-based from in-app content).
|
||||
- Color roles are "like the numbers in a paint-by-number canvas" — the connective tissue between UI
|
||||
elements and what color goes where.
|
||||
- **Color roles are mapped to Material Components.** Custom components must be mapped to this same
|
||||
role set.
|
||||
- **Color roles ensure accessibility.** The system is built on accessible color pairings; color pairs
|
||||
provide an accessible minimum 3:1 contrast.
|
||||
- **Color roles are tokenized.**
|
||||
- General vocabulary used in role names:
|
||||
- **Surface** — backgrounds and large, low-emphasis areas.
|
||||
- **Primary / Secondary / Tertiary** — accent roles used to emphasize or de-emphasize foreground elements.
|
||||
- **Container** — fill color for foreground elements like buttons; not for text or icons.
|
||||
- **On** — a color for text/icons *on top of* its paired parent color (e.g. "on primary" on "primary").
|
||||
- **Variant** — a lower-emphasis alternative to its non-variant pair (e.g. "outline variant" vs "outline").
|
||||
- The system uses a color space called **HCT** (Hue, Chroma, Tone), not HSL/RGB. HCT lets you manipulate
|
||||
hue and chroma without affecting tone.
|
||||
- **Hue**: 0–360, circular (perception as red/orange/yellow/…).
|
||||
- **Chroma**: 0 (grey/black/white) up to roughly 120 in HCT (colorfulness); max chroma varies by hue/tone.
|
||||
- **Tone**: 0 (pure black) to 100 (pure white); determines contrast. HCT tone is not interchangeable
|
||||
with HSL lightness — two colors can share an HCT tone (perceived brightness) while having different
|
||||
HSL lightness values.
|
||||
- Dynamic color pipeline: (1) start with a source color (from wallpaper, in-app content, or hand-picked);
|
||||
(2) feed it into an algorithm; (3) the algorithm generates key colors; (4) it creates a **tonal palette**
|
||||
per key color (tones 0–100 in steps of 10, plus 95/98/99, some palettes have more); (5) it assigns tones
|
||||
to the ~26 standard color roles for both light and dark theme; (6) the new colors are applied to the UI.
|
||||
|
||||
### Rules (Google's wording)
|
||||
|
||||
- Pair and layer color roles only as intended (e.g. primary + on primary + secondary container + on
|
||||
secondary container) to keep them legible as contrast level changes. **Don't** mix roles improperly
|
||||
(e.g. primary + primary container + secondary container + on surface) — this can become illegible as
|
||||
contrast changes.
|
||||
- **Don't** use the **outline** color for dividers (different contrast requirements) — use **outline
|
||||
variant** instead.
|
||||
- **Don't** use **outline** for components containing multiple elements (e.g. cards) — use **outline
|
||||
variant**.
|
||||
- **Don't** use **outline variant** to create visual hierarchy or define a target's visual boundary —
|
||||
use **outline** or another color giving 3:1 contrast with the surface.
|
||||
- **Caution**: **outline variant** can border targets like chips/buttons only if elements inside them
|
||||
(icons/text) already meet 4.5:1 contrast.
|
||||
- Most products won't need the **add-on** color roles (fixed/fixed-dim, on-fixed/on-fixed-variant,
|
||||
surface-dim/surface-bright). "If you aren't sure whether your product should use the add-on roles, it
|
||||
probably shouldn't."
|
||||
- **Don't** use **fixed** colors where contrast is necessary — they don't adapt to light/dark theme and
|
||||
are likely to cause contrast issues (e.g. a button fill on a permissions screen). **Do** use primary/
|
||||
secondary/tertiary where contrast is needed.
|
||||
- All color mappings — especially surface colors — should remain the same for a given layout region
|
||||
across breakpoints (e.g. body always **surface**, navigation always **surface container**, on both
|
||||
mobile and tablet).
|
||||
- Error is a **static** color by default (doesn't change with dynamic color schemes) but still adapts to
|
||||
light/dark theme.
|
||||
- Advanced customization best practices:
|
||||
- Any role starting with "on-" is guaranteed sufficient contrast with its paired role; other role pairs
|
||||
may not meet the 4.5:1 (small text) / 3:1 (large text) Material contrast requirements — check before
|
||||
reusing a pairing.
|
||||
- Always apply **color roles**, never static hex values or raw tonal-palette values, for anything that
|
||||
should respond to theme, contrast level, or dynamic color.
|
||||
- Test a dynamic-color component under multiple source-color themes (light/dark; red/yellow/green/blue).
|
||||
- Limit a screen to two color schemes from different source types (e.g. baseline/user-generated combined
|
||||
with one content-based source) to avoid visual disarray.
|
||||
- Don't replace semantic colors (e.g. red error, green success) with content-based dynamic color.
|
||||
- Don't harmonize colors whose appearance should stay absolutely consistent (e.g. brand colors).
|
||||
- Defining custom color roles should be considered only if the existing Material roles can't achieve the
|
||||
desired result.
|
||||
- Choosing a scheme: use **static (baseline)** if not ready for dynamic color, migrating from M2, building
|
||||
for enterprise users, or building for iOS. Use **dynamic** if the product should personalize, react to
|
||||
wallpaper/content, or offer user-controlled contrast.
|
||||
|
||||
### Values
|
||||
|
||||
#### Color roles (Compose `ColorSchemeKeyTokens`, 47 tokens; tone source `ColorLightTokens.kt` / `ColorDarkTokens.kt`)
|
||||
|
||||
Each role resolves to a tone from one of 5 tonal palettes (Primary, Secondary, Tertiary, Neutral,
|
||||
Neutral Variant) or Error. Format: role — light theme tone — dark theme tone.
|
||||
|
||||
| Role | Light tone | Dark tone | Purpose (site wording) |
|
||||
| --- | --- | --- | --- |
|
||||
| Primary | Primary40 | Primary80 | High-emphasis fills, text, icons against surface |
|
||||
| On primary | Primary100 | Primary20 | Text/icons against primary |
|
||||
| Primary container | Primary90 | Primary30 | Standout fill against surface, key components like FAB |
|
||||
| On primary container | Primary10 | Primary90 | Text/icons against primary container |
|
||||
| Secondary | Secondary40 | Secondary80 | Less prominent fills/text/icons against surface |
|
||||
| On secondary | Secondary100 | Secondary20 | Text/icons against secondary |
|
||||
| Secondary container | Secondary90 | Secondary30 | Recessive fill, e.g. tonal buttons |
|
||||
| On secondary container | Secondary10 | Secondary90 | Text/icons against secondary container |
|
||||
| Tertiary | Tertiary40 | Tertiary80 | Complementary accent, e.g. input fields |
|
||||
| On tertiary | Tertiary100 | Tertiary20 | Text/icons against tertiary |
|
||||
| Tertiary container | Tertiary90 | Tertiary30 | Complementary container fill |
|
||||
| On tertiary container | Tertiary10 | Tertiary90 | Text/icons against tertiary container |
|
||||
| Error | Error40 | Error80 | Attention-grabbing color for urgency |
|
||||
| On error | Error100 | Error20 | Text/icons against error |
|
||||
| Error container | Error90 | Error30 | Attention-grabbing fill |
|
||||
| On error container | Error10 | Error90 | Text/icons against error container |
|
||||
| Surface | Neutral98 | Neutral6 | Default background |
|
||||
| On surface | Neutral10 | Neutral90 | Text/icons on surface / surface container |
|
||||
| On surface variant | NeutralVariant30 | NeutralVariant80 | Lower-emphasis text/icons on surface |
|
||||
| Surface variant | NeutralVariant90 | NeutralVariant30 | (legacy; superseded by surface container roles Feb 2023) |
|
||||
| Background / On background | Neutral98 / Neutral10 | Neutral6 / Neutral90 | Legacy roles, same values as surface/on surface |
|
||||
| Outline | NeutralVariant50 | NeutralVariant60 | Important boundaries, e.g. text-field outline |
|
||||
| Outline variant | NeutralVariant80 | NeutralVariant30 | Decorative elements, e.g. dividers |
|
||||
| Inverse surface | Neutral20 | Neutral90 | Background contrasting against surrounding surface |
|
||||
| Inverse on surface | Neutral95 | Neutral20 | Text/icons against inverse surface |
|
||||
| Inverse primary | Primary80 | Primary40 | Actionable elements (e.g. text buttons) against inverse surface |
|
||||
| Scrim | Neutral0 | Neutral0 | Scrim behind modals, at 32% opacity |
|
||||
| Surface tint | (=Primary) | (=Primary) | Deprecated — use elevation level tokens instead |
|
||||
|
||||
Surface container family (5 levels, named by emphasis — not tied to elevation, replaced "surface +1..+5"
|
||||
in Feb 2023):
|
||||
|
||||
| Role | Light tone | Dark tone |
|
||||
| --- | --- | --- |
|
||||
| Surface container lowest | Neutral100 | Neutral4 |
|
||||
| Surface container low | Neutral96 | Neutral10 |
|
||||
| Surface container | Neutral94 | Neutral12 |
|
||||
| Surface container high | Neutral92 | Neutral17 |
|
||||
| Surface container highest | Neutral90 | Neutral22 |
|
||||
|
||||
Bright/dim add-on surface roles (keep relative brightness across both themes, unlike default surface
|
||||
which inverts):
|
||||
|
||||
| Role | Light tone | Dark tone |
|
||||
| --- | --- | --- |
|
||||
| Surface dim | Neutral87 | Neutral6 |
|
||||
| Surface bright | Neutral98 | Neutral24 |
|
||||
|
||||
Fixed accent add-on roles (same tone in light **and** dark theme; added Feb 2023):
|
||||
|
||||
| Role | Tone (both themes) | Purpose |
|
||||
| --- | --- | --- |
|
||||
| Primary fixed | Primary90 | Fill color that stays constant across themes |
|
||||
| Primary fixed dim | Primary80 | Stronger/more emphasized fixed tone |
|
||||
| On primary fixed | Primary10 | Text/icons on primary fixed |
|
||||
| On primary fixed variant | Primary30 | Lower-emphasis text/icons on primary fixed |
|
||||
| Secondary fixed | Secondary90 | ” |
|
||||
| Secondary fixed dim | Secondary80 | ” |
|
||||
| On secondary fixed | Secondary10 | ” |
|
||||
| On secondary fixed variant | Secondary30 | ” |
|
||||
| Tertiary fixed | Tertiary90 | ” |
|
||||
| Tertiary fixed dim | Tertiary80 | ” |
|
||||
| On tertiary fixed | Tertiary10 | ” |
|
||||
| On tertiary fixed variant | Tertiary30 | ” |
|
||||
|
||||
#### Baseline tonal palettes (hex; from androidx `PaletteTokens.kt`, since the site renders these only in
|
||||
an interactive, non-text widget)
|
||||
|
||||
| Tone | Primary | Secondary | Tertiary | Neutral | Neutral Variant | Error |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| 0 | #000000 | #000000 | #000000 | #000000 | #000000 | #000000 |
|
||||
| 4 | — | — | — | #0F0D13 | — | — |
|
||||
| 6 | — | — | — | #141218 | — | — |
|
||||
| 10 | #21005D | #1D192B | #31111D | #1D1B20 | #1D1A22 | #410E0B |
|
||||
| 12 | — | — | — | #211F26 | — | — |
|
||||
| 17 | — | — | — | #2B2930 | — | — |
|
||||
| 20 | #381E72 | #332D41 | #492532 | #322F35 | #322F37 | #601410 |
|
||||
| 22 | — | — | — | #36343B | — | — |
|
||||
| 24 | — | — | — | #3B383E | — | — |
|
||||
| 30 | #4F378B | #4A4458 | #633B48 | #48464C | #49454F | #8C1D18 |
|
||||
| 40 | #6750A4 | #625B71 | #7D5260 | #605D64 | #605D66 | #B3261E |
|
||||
| 50 | #7F67BE | #7A7289 | #986977 | #79767D | #79747E | #DC362E |
|
||||
| 60 | #9A82DB | #958DA5 | #B58392 | #938F96 | #938F99 | #E46962 |
|
||||
| 70 | #B69DF8 | #B0A7C0 | #D29DAC | #AEA9B1 | #AEA9B4 | #EC928E |
|
||||
| 80 | #D0BCFF | #CCC2DC | #EFB8C8 | #CAC5CD | #CAC4D0 | #F2B8B5 |
|
||||
| 87 | — | — | — | #DED8E1 | — | — |
|
||||
| 90 | #EADDFF | #E8DEF8 | #FFD8E4 | #E6E0E9 | #E7E0EC | #F9DEDC |
|
||||
| 92 | — | — | — | #ECE6F0 | — | — |
|
||||
| 94 | — | — | — | #F3EDF7 | — | — |
|
||||
| 95 | #F6EDFF | #F6EDFF | #FFECF1 | #F5EFF7 | #F5EEFA | #FCEEEE |
|
||||
| 96 | — | — | — | #F7F2FA | — | — |
|
||||
| 98 | — | — | — | #FEF7FF | — | — |
|
||||
| 99 | #FFFBFE | #FFFBFE | #FFFBFA | #FFFBFF | #FFFBFE | #FFFBF9 |
|
||||
| 100 | #FFFFFF | #FFFFFF | #FFFFFF | #FFFFFF | #FFFFFF | #FFFFFF |
|
||||
|
||||
(Source: androidx Compose Material3 `PaletteTokens.kt` — the purple/violet "baseline" scheme. Any product
|
||||
using a different source color will generate a different palette via the HCT algorithm, but role→tone
|
||||
assignments are the same.)
|
||||
|
||||
#### Contrast levels
|
||||
|
||||
| Level | Ratio (approx.) | Notes |
|
||||
| --- | --- | --- |
|
||||
| Standard (default) | mixed | Emphasizes hierarchy using high- and low-contrast elements together |
|
||||
| Medium | 3:1 minimum | For users needing more contrast without high-contrast side effects like halation |
|
||||
| High | 7:1 | Further emphasizes essential elements (e.g. card content, not the card container) |
|
||||
|
||||
Contrast settings apply automatically in both light and dark theme. Custom components support contrast
|
||||
levels simply by using Material color roles (e.g. primary container / on primary container) — the role's
|
||||
resolved value changes per contrast level automatically.
|
||||
|
||||
### M3 vs M3 Expressive differences
|
||||
|
||||
The color pages do not explicitly label any change "M3 Expressive" the way shape/motion/typography pages
|
||||
do, but the "What's new" timeline on `styles/color` lists (with the May 2025 entry coinciding with the
|
||||
Expressive launch):
|
||||
|
||||
- **May 2025 — Three levels of contrast.** Color roles now support standard/medium/high contrast, each
|
||||
tokenized (see Values above).
|
||||
- **Aug 2024 — More colorful text & icons.** On-primary-container, on-secondary-container,
|
||||
on-tertiary-container, on-error-container were updated in light theme to be more colorful while
|
||||
remaining accessible; affects badges, buttons (all variants), chips, lists, menus, navigation bar,
|
||||
navigation drawer, navigation rail, switches, toolbars.
|
||||
- **Feb 2023 — Tone-based surface colors.** Replaced the old "surface +1 to +5 elevation overlay" model
|
||||
with the surface/surface-container role family (not tied to elevation). Also: default light-theme
|
||||
surface moved from tone 99 to tone 98; neutral palette chroma increased 4→6; dark-theme surfaces
|
||||
slightly darkened (alignment with Android SysUI).
|
||||
- **Feb 2023 — Additional accent colors.** Added fixed / fixed-dim roles for primary, secondary, tertiary.
|
||||
|
||||
### Accessibility requirements
|
||||
|
||||
- Color pairs provide an accessible **minimum 3:1 contrast** by design (color-role pairing system).
|
||||
- Text contrast targets (also stated on `foundations/designing/color-contrast`, W3C-based):
|
||||
large text (≥14pt bold / ≥18pt regular) and graphics: **≥3:1** against background; small text: **≥4.5:1**.
|
||||
Disabled states do not need to meet contrast requirements.
|
||||
- Non-text clustered elements (e.g. a group of buttons) should meet **3:1** contrast between container
|
||||
color and background; a standalone prominent element (e.g. a FAB) does not need this because its
|
||||
prominence already distinguishes it.
|
||||
- High contrast mode targets **7:1**.
|
||||
- Default typography color is **on surface** (or **on surface variant** as a strong alternative).
|
||||
- **Color-alone rule**: hyperlinked text must use primary (or tertiary, for a less prominent link) *and*
|
||||
must be underlined — color alone must not be the only signal.
|
||||
|
||||
### Source URLs
|
||||
|
||||
- https://m3.material.io/styles
|
||||
- https://m3.material.io/styles/color
|
||||
- https://m3.material.io/styles/color/system (identical render to /styles/color)
|
||||
- https://m3.material.io/styles/color/roles
|
||||
- https://m3.material.io/styles/color/system/how-the-system-works
|
||||
- https://m3.material.io/styles/color/choosing-a-scheme
|
||||
- https://m3.material.io/styles/color/static (= /styles/color/static/baseline)
|
||||
- https://m3.material.io/styles/color/dynamic
|
||||
- https://m3.material.io/styles/color/advanced
|
||||
- https://m3.material.io/styles/color/advanced/apply-colors
|
||||
- https://m3.material.io/styles/color/advanced/define-new-colors
|
||||
- https://m3.material.io/styles/color/advanced/adjust-existing-colors
|
||||
- https://m3.material.io/styles/color/resources
|
||||
- https://m3.material.io/foundations/designing/color-contrast (contrast-ratio table)
|
||||
- https://raw.githubusercontent.com/androidx/androidx/androidx-main/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorSchemeKeyTokens.kt
|
||||
- .../tokens/ColorLightTokens.kt
|
||||
- .../tokens/ColorDarkTokens.kt
|
||||
- .../tokens/PaletteTokens.kt
|
||||
|
||||
---
|
||||
|
||||
## Elevation
|
||||
|
||||
### Principles
|
||||
|
||||
- Elevation is applied to all surfaces and components.
|
||||
- Tokens codify the distance on the z-axis so components appear consistently relative to each other.
|
||||
- Tokens carry no shadow or color themselves — each platform determines the specific shadow/value used
|
||||
at each elevation level.
|
||||
- Elevation can be shown as tonal surface colors **or** shadows.
|
||||
- Avoid changing the default elevation of Material 3 components.
|
||||
- Stick to using a small number of elevation levels.
|
||||
- Elevation is measured as the distance between components along the z-axis in density-independent
|
||||
pixels (dp).
|
||||
- To successfully depict elevation, a surface must show: surface edges (contrast with surroundings),
|
||||
overlap with other surfaces (at rest or in motion), and distance from other surfaces.
|
||||
|
||||
### Rules (do/don't)
|
||||
|
||||
- **Do** ensure floating elements (e.g. a FAB) have sufficient contrast with the surface beneath them.
|
||||
**Don't** use colors with insufficient contrast — the relationship between surfaces must stay clear.
|
||||
- By default, Material 3 surfaces use **tonal difference** to indicate separation; shadows and scrims
|
||||
are alternative/additional methods.
|
||||
- Any overlapping containment areas or components should use **different surface/surface-container color
|
||||
roles** to communicate separation — these color roles are not tied to elevation.
|
||||
- Shadow size and softness both express distance: small, sharp shadows = close proximity; larger, softer
|
||||
shadows = more distance.
|
||||
- "When it comes to applying shadows, less is more. The fewer levels in your UI, the more power they have
|
||||
to direct attention and action."
|
||||
- Use **visible shadows** to: (1) protect elements against a patterned/busy background (cards, chips,
|
||||
buttons), or (2) encourage interaction (elements can temporarily lift on focus/selection/hover; a
|
||||
raised element can lower when a higher element appears).
|
||||
- Use a **scrim** to bring focus to elements over a large layered surface (e.g. modals, expanded
|
||||
navigation menus).
|
||||
|
||||
### Values
|
||||
|
||||
#### Elevation levels (`ElevationTokens.kt`; matches the site's dp values)
|
||||
|
||||
| Level | dp |
|
||||
| --- | --- |
|
||||
| 0 | 0dp |
|
||||
| 1 | 1dp |
|
||||
| 2 | 3dp |
|
||||
| 3 | 6dp |
|
||||
| 4 | 8dp |
|
||||
| 5 | 12dp |
|
||||
|
||||
An element's **resting state** is on levels 0–3; levels 4–5 are reserved for user-interacted states
|
||||
(hover, dragged) — e.g. hovering a FAB increases elevation by 1 level (level 3 → 4), consistently across
|
||||
all Material buttons.
|
||||
|
||||
#### Component resting elevation (from `styles/elevation/tokens`)
|
||||
|
||||
| Resting level | dp | Components |
|
||||
| --- | --- | --- |
|
||||
| 5 | 12dp | (not assigned as a resting level) |
|
||||
| 4 | 8dp | (not assigned as a resting level) |
|
||||
| 3 | 6dp | Date pickers, Dialogs (modal), Extended FAB, FAB, FAB menu (close button), Search, Time pickers |
|
||||
| 2 | 3dp | App bar (scrolled), Menu, Navigation bar, Rich tooltip, Toolbar |
|
||||
| 1 | 1dp | Banner, Bottom sheet (modal), Button (elevated), Card (elevated), Chips (elevated), Navigation drawer (modal), Side sheet (modal) |
|
||||
| 0 | 0dp | App bar (not scrolled), Buttons (filled/tonal/outlined), Button groups, Cards (filled/outlined), Carousel, Chips, Dialog (full-screen), Extended FAB (in nav rail), FAB (in nav rail), FAB menu (list items), Icon buttons, List, Navigation rail, Segmented button, Side sheet (docked), Slider, Split button, Tabs |
|
||||
|
||||
#### Scrim
|
||||
|
||||
Scrims use the **scrim** color role at **32% opacity**.
|
||||
|
||||
### Differences from M2
|
||||
|
||||
- **Shadows**: instead of applying shadows by default at all levels, use shadows only when needed for
|
||||
protection against a background or to encourage interaction.
|
||||
- **Color**: new color mappings and compatibility with dynamic color.
|
||||
- **Levels**: elevation is now described in terms of discrete levels (0–5) rather than continuous dp.
|
||||
|
||||
### M3 vs M3 Expressive differences
|
||||
|
||||
No "M3 Expressive" update is documented on the fetched elevation pages (`styles/elevation`,
|
||||
`/applying-elevation`, `/tokens`) — the elevation model appears unchanged by the Expressive update.
|
||||
|
||||
### Accessibility requirements
|
||||
|
||||
- Edges (surface separation) must create sufficient contrast between surfaces — meeting or exceeding
|
||||
accessible contrast ratios — to be perceived as separate. Elevation/shadow alone is treated as a visual
|
||||
cue that must be reinforced with adequate color/tonal contrast, not relied on in isolation.
|
||||
|
||||
### Source URLs
|
||||
|
||||
- https://m3.material.io/styles/elevation (= /styles/elevation/overview)
|
||||
- https://m3.material.io/styles/elevation/applying-elevation
|
||||
- https://m3.material.io/styles/elevation/tokens
|
||||
- https://raw.githubusercontent.com/androidx/androidx/androidx-main/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ElevationTokens.kt
|
||||
|
||||
---
|
||||
|
||||
## Icons
|
||||
|
||||
### Principles
|
||||
|
||||
- Icons are small symbols to easily identify actions and categories.
|
||||
- Material Symbols are the new default: a variable icon font set in three styles — **outlined, rounded,
|
||||
sharp** — across seven weights. (Legacy "Material Icons" remain available but lack the variable-font axes.)
|
||||
- Design principles (do/don't): simplify icons for clarity/legibility, don't be overly literal or complex;
|
||||
use geometric, consistent, bold shapes, don't use delicate/loose organic shapes; maintain one consistent
|
||||
visual style per icon set, don't mix styles within a set.
|
||||
|
||||
### Rules (do/don't)
|
||||
|
||||
- **Don't** use the lightest weight (100) for standard-size (24dp) icons — minimum weight for that size
|
||||
should be **200**. Use caution with excessive weight at 24dp too.
|
||||
- **Do** apply weight consistently across a group (e.g. a navigation rail); **don't** mix different weights.
|
||||
- **Do** use the same size for a Material Symbol and adjacent text; **don't** mix symbol/text sizes.
|
||||
- **Do** use the same optical weight for a symbol and adjacent text; **don't** mismatch optical weights.
|
||||
- **Do** shift a symbol's baseline down ~11.5% of the text size when pairing with text; **don't** use the
|
||||
same baseline for symbol and text.
|
||||
- **Do** position icons "on pixel" within the icon grid; **don't** place on non-integer (sub-pixel)
|
||||
coordinates.
|
||||
- Corners: default corner radius is **2dp**. For the **outlined** style, interior corners are square (not
|
||||
rounded); for shapes ≤2dp wide, stroke corners shouldn't be rounded. **Rounded** style: both exterior
|
||||
and interior corners are rounded. **Sharp** style: both exterior and interior corners reduce from 2dp
|
||||
to **0dp**. **Caution**: overly round corners reduce legibility. **Don't** use inconsistent corner radii.
|
||||
- Stroke: recommended stroke weight is **2dp** (regular weight, 400) with consistent weights and squared
|
||||
stroke terminals; **don't** use inconsistent stroke weights or rounded stroke terminals. Complex icons
|
||||
may use an optically-corrected **1.5dp** stroke to fit multiple curves in the 24×24dp space.
|
||||
- **Do** make icons face forward (flat); **don't** tilt, rotate, or render icons as dimensional/isometric.
|
||||
- Icon content must stay inside the **live area**; it may extend into padding toward the **trim area** if
|
||||
extra visual weight is needed, but **no part may extend past the trim area**.
|
||||
|
||||
### Values
|
||||
|
||||
#### Sizes
|
||||
|
||||
| Size | Use |
|
||||
| --- | --- |
|
||||
| 20dp | Desktop, dense layouts, small-scale visuals |
|
||||
| 24dp | Standard (baseline) size |
|
||||
| 40dp | Larger — pairs with display/headline type, larger screens |
|
||||
| 48dp | Larger — pairs with display/headline type, larger screens |
|
||||
|
||||
#### Layout (24dp standard icon)
|
||||
|
||||
| Region | Size |
|
||||
| --- | --- |
|
||||
| Trim area (total canvas) | 24dp × 24dp |
|
||||
| Live area | 20dp × 20dp |
|
||||
| Padding (live→trim) | 2dp around the perimeter |
|
||||
|
||||
#### Grid and keyline shapes (within the 24dp grid)
|
||||
|
||||
| Keyline | Dimensions |
|
||||
| --- | --- |
|
||||
| Square | 18dp × 18dp |
|
||||
| Circle | 20dp diameter |
|
||||
| Vertical rectangle | 20dp height × 16dp width |
|
||||
| Horizontal rectangle | 16dp height × 20dp width |
|
||||
|
||||
#### Corner radius by style
|
||||
|
||||
| Style | Exterior corner | Interior corner |
|
||||
| --- | --- | --- |
|
||||
| Outlined | 2dp rounded | Square (not rounded) |
|
||||
| Rounded | 2dp rounded | Rounded |
|
||||
| Sharp | 0dp | 0dp |
|
||||
|
||||
#### Material Symbols variable-font axes
|
||||
|
||||
| Axis | Range | Notes |
|
||||
| --- | --- | --- |
|
||||
| Weight | 100 (thin) – 700 (bold) | Also affects overall symbol size; min 200 recommended at 24dp |
|
||||
| Fill | 0 – 1 | 0 = unfilled/outlined, 1 = fully filled; communicates state transitions |
|
||||
| Grade | negative / 0 / positive (site examples: −25, 0) | Finer-grained than weight; doesn't change width or line breaks. Default grade is 0 for a dark icon on light background, **−25** for a light icon on dark background (compensates for visual bleed/halation). Positive grade for emphasis (e.g. active state). |
|
||||
| Optical size | 20dp – 48dp | Auto-adjusts stroke weight so the icon "looks the same" as size scales; use 20dp for dense desktop layouts, 40–48dp to highlight primary actions |
|
||||
|
||||
#### Icon styles (3): Outlined, Rounded, Sharp — see corner-radius table above.
|
||||
|
||||
### M3 vs M3 Expressive differences
|
||||
|
||||
No "M3 Expressive" update is documented on the fetched icon pages (`styles/icons`,
|
||||
`/designing-icons`, `/applying-icons`). Material Symbols (three styles + four variable axes) predate the
|
||||
Expressive update; no Expressive-specific icon changes were found on the site.
|
||||
|
||||
### Accessibility requirements
|
||||
|
||||
- Icon meaning must always be unambiguous and accessible; use caution displaying icons without labels.
|
||||
Label text (short, meaningful) should be used especially for navigation.
|
||||
- Below **20dp**, symbols that are complex/highly-detailed, have multiple parts, or represent a key
|
||||
action essential to the product should have an accompanying text label. Simple symbols (e.g. a star for
|
||||
ratings) can be used alone at any size if they remain identifiable.
|
||||
- **Target size**: a 24dp symbol should have a default **target size of 48dp**.
|
||||
|
||||
### Source URLs
|
||||
|
||||
- https://m3.material.io/styles/icons (= /styles/icons/overview)
|
||||
- https://m3.material.io/styles/icons/designing-icons
|
||||
- https://m3.material.io/styles/icons/applying-icons
|
||||
|
||||
---
|
||||
|
||||
## Motion
|
||||
|
||||
### Principles
|
||||
|
||||
- May 2025: Material introduced the **motion physics system** with M3 Expressive — a spring-based system
|
||||
replacing the previous easing-and-duration system, intended to feel "more alive, fluid, and natural,"
|
||||
and to be easier to implement/customize.
|
||||
- Two preset **motion schemes**: **Expressive** (Material's opinionated default; overshoots final values
|
||||
to add bounce; use for most situations, hero moments, key interactions) and **Standard** (minimal
|
||||
bounce, eases into final values; for utilitarian products). Custom schemes can also be created.
|
||||
- A **spring** is defined by three attributes: **stiffness** (hardness — higher = faster resolve),
|
||||
**damping** (how fast bounce wears out — higher = faster stop; damping = 1 removes bounce entirely),
|
||||
and **initial velocity** (starting speed, combines with stiffness/damping to affect total duration).
|
||||
- Springs are versatile (one spring can drive transitions, button effects, gestures — consistent feel)
|
||||
and feel natural/predictable, handling gesture interruption and retargeting seamlessly.
|
||||
- Two spring **styles**: **Spatial** (movement — x/y position, rotation, size, rounded corners; overshoots
|
||||
and bounces into place) and **Effects** (color, opacity; no overshoot).
|
||||
- Three **speeds** per style: **default**, **fast**, **slow**. Most motion uses default; smaller elements
|
||||
may use fast; larger elements may use slow. The exact values differ by device class (wearable/phone/
|
||||
tablet) even though the *relative* speed ordering (fast < default < slow) is constant.
|
||||
- Token naming: e.g. `md.sys.motion.spring.fast.spatial` — the scheme (expressive/standard) is applied at
|
||||
the product level, not baked into the token name, so schemes can be swapped without re-tokenizing.
|
||||
- On Jetpack Compose, **21 Material components** use the motion physics system by default (Android Views/
|
||||
MDC-Android: available but not yet wired into components; Web: compatible via curve approximations).
|
||||
- Legacy **easing and duration** tokens remain available as a fallback and are still used for transition
|
||||
animation on platforms/paths not yet migrated to springs.
|
||||
|
||||
### Rules (do/don't; easing & duration choice)
|
||||
|
||||
- Choose the **Emphasized** easing set for most transitions (captures the M3 style); use **Standard**
|
||||
only for small, utility-focused, quick transitions, and as the fallback on platforms without Emphasized
|
||||
support (iOS, Web).
|
||||
- Easing by transition type:
|
||||
- **Begin and end on screen** → **Emphasized** (speeds up quickly, gentle rest, emphasizes the end).
|
||||
- **Enter the screen** → **Emphasized decelerate** (starts at peak velocity, gentle rest).
|
||||
- **Exit the screen permanently** → **Emphasized accelerate** (starts at rest, ends at peak velocity —
|
||||
implies it can't be retrieved).
|
||||
- **Exit the screen temporarily** → **Emphasized** (ends at rest just off-screen — implies it can be
|
||||
retrieved, e.g. a drawer).
|
||||
- Duration by transition size: small-area transitions get short durations; large-area transitions get
|
||||
long durations (consistent sense of speed).
|
||||
- Duration by direction: **exit/dismiss/collapse** → shorter durations (less attention needed); **enter/
|
||||
persist** → longer durations (focus attention on what's new).
|
||||
- Suggested easing+duration pairs (defaults good for most transitions):
|
||||
|
||||
| Easing | Duration | Transition type |
|
||||
| --- | --- | --- |
|
||||
| Emphasized | 500ms | Begin and end on screen |
|
||||
| Emphasized decelerate | 400ms | Enter the screen |
|
||||
| Emphasized accelerate | 200ms | Exit the screen |
|
||||
| Standard | 300ms | Begin and end on screen |
|
||||
| Standard decelerate | 250ms | Enter the screen |
|
||||
| Standard accelerate | 200ms | Exit the screen |
|
||||
|
||||
- Motion customization levels: **Level 1** — use a default scheme (expressive/standard) as-is; **Level 2**
|
||||
— create a custom `MotionScheme` (custom `AnimationSpec` per property) applied product-wide; **Level 3**
|
||||
— swap the scheme per element/screen (override the `CompositionLocal`) while most of the product uses
|
||||
the primary scheme.
|
||||
- Transition patterns documented: container transform (full-screen and within-screen), forward/backward,
|
||||
lateral, top-level, enter/exit (within screen bounds and beyond screen bounds), skeleton loaders.
|
||||
|
||||
### Values
|
||||
|
||||
#### Expressive motion scheme — spring tokens (`ExpressiveMotionTokens.kt`)
|
||||
|
||||
| Speed | Style | Damping | Stiffness |
|
||||
| --- | --- | --- | --- |
|
||||
| Fast | Spatial | 0.6 | 800.0 |
|
||||
| Fast | Effects | 1.0 | 3800.0 |
|
||||
| Default | Spatial | 0.8 | 380.0 |
|
||||
| Default | Effects | 1.0 | 1600.0 |
|
||||
| Slow | Spatial | 0.8 | 200.0 |
|
||||
| Slow | Effects | 1.0 | 800.0 |
|
||||
|
||||
#### Standard motion scheme — spring tokens (`StandardMotionTokens.kt`)
|
||||
|
||||
| Speed | Style | Damping | Stiffness |
|
||||
| --- | --- | --- | --- |
|
||||
| Fast | Spatial | 0.9 | 1400.0 |
|
||||
| Fast | Effects | 1.0 | 3800.0 |
|
||||
| Default | Spatial | 0.9 | 700.0 |
|
||||
| Default | Effects | 1.0 | 1600.0 |
|
||||
| Slow | Spatial | 0.9 | 300.0 |
|
||||
| Slow | Effects | 1.0 | 800.0 |
|
||||
|
||||
#### Web curve equivalents for springs (from `styles/motion/overview/specs`; springs aren't natively
|
||||
supported on the web, so Web should use these approximated cubic-bezier curves with matching duration)
|
||||
|
||||
| Spring | Cubic-bezier | Duration |
|
||||
| --- | --- | --- |
|
||||
| Expressive fast spatial | 0.42, 1.67, 0.21, 0.90 | 350ms |
|
||||
| Expressive default spatial | 0.38, 1.21, 0.22, 1.00 | 500ms |
|
||||
| Expressive slow spatial | 0.39, 1.29, 0.35, 0.98 | 650ms |
|
||||
| Expressive fast effects | 0.31, 0.94, 0.34, 1.00 | 150ms |
|
||||
| Expressive default effects | 0.34, 0.80, 0.34, 1.00 | 200ms |
|
||||
| Expressive slow effects | 0.34, 0.88, 0.34, 1.00 | 300ms |
|
||||
| Standard fast spatial | 0.27, 1.06, 0.18, 1.00 | 350ms |
|
||||
| Standard default spatial | 0.27, 1.06, 0.18, 1.00 | 500ms |
|
||||
| Standard slow spatial | 0.27, 1.06, 0.18, 1.00 | 750ms |
|
||||
| Standard fast effects | 0.31, 0.94, 0.34, 1.00 | 150ms |
|
||||
| Standard default effects | 0.34, 0.80, 0.34, 1.00 | 200ms |
|
||||
| Standard slow effects | 0.34, 0.88, 0.34, 1.00 | 300ms |
|
||||
|
||||
#### Legacy easing tokens (`styles/motion/easing-and-duration/tokens-specs`; curves confirmed in
|
||||
androidx `MotionTokens.kt`, noted where Compose's single-cubic-bezier approximation differs from the
|
||||
platform-accurate multi-segment Android `PathInterpolator`)
|
||||
|
||||
| Token | Android | CSS | Flutter | iOS |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `md.sys.motion.easing.emphasized` | PathInterpolator(M 0,0 C 0.05,0 0.133333,0.06 0.166666,0.4 C 0.208333,0.82 0.25,1 1,1) | N/A (use Standard) | `easeInOutCubicEmphasized` | N/A (use Standard) |
|
||||
| `md.sys.motion.easing.emphasized.decelerate` | PathInterpolator(0.05, 0.7, 0.1, 1) | cubic-bezier(0.05,0.7,0.1,1.0) | Cubic(0.05,0.7,0.1,1.0) | ControlPoints 0.05,0.7,0.1,1.0 |
|
||||
| `md.sys.motion.easing.emphasized.accelerate` | PathInterpolator(0.3, 0, 0.8, 0.15) | cubic-bezier(0.3,0.0,0.8,0.15) | Cubic(0.3,0.0,0.8,0.15) | ControlPoints 0.3,0.0,0.8,0.15 |
|
||||
| `md.sys.motion.easing.standard` | PathInterpolator(0.2, 0, 0, 1) | cubic-bezier(0.2,0.0,0,1.0) | Cubic(0.2,0.0,0,1.0) | ControlPoints 0.2,0.0,0.0,1.0 |
|
||||
| `md.sys.motion.easing.standard.decelerate` | PathInterpolator(0, 0, 0, 1) | cubic-bezier(0,0,0,1) | Cubic(0,0,0,1) | ControlPoints 0.0,0.0,0.0,1.0 |
|
||||
| `md.sys.motion.easing.standard.accelerate` | PathInterpolator(0.3, 0, 1, 1) | cubic-bezier(0.3,0,1,1) | Cubic(0.3,0,1,1) | ControlPoints 0.3,0.0,1.0,1.0 |
|
||||
|
||||
Compose `MotionTokens.kt` also defines `EasingLegacyCubicBezier` = CubicBezierEasing(0.4, 0.0, 0.2, 1.0)
|
||||
(M2-era "standard" curve) and `EasingLinearCubicBezier` = (0,0,1,1) — not shown on the site pages fetched
|
||||
(not on the site; from androidx `MotionTokens.kt`).
|
||||
|
||||
#### Legacy duration tokens (site table; matches androidx `MotionTokens.kt` exactly)
|
||||
|
||||
| Token | Value |
|
||||
| --- | --- |
|
||||
| `md.sys.motion.duration.short1` | 50ms |
|
||||
| `md.sys.motion.duration.short2` | 100ms |
|
||||
| `md.sys.motion.duration.short3` | 150ms |
|
||||
| `md.sys.motion.duration.short4` | 200ms |
|
||||
| `md.sys.motion.duration.medium1` | 250ms |
|
||||
| `md.sys.motion.duration.medium2` | 300ms |
|
||||
| `md.sys.motion.duration.medium3` | 350ms |
|
||||
| `md.sys.motion.duration.medium4` | 400ms |
|
||||
| `md.sys.motion.duration.long1` | 450ms |
|
||||
| `md.sys.motion.duration.long2` | 500ms |
|
||||
| `md.sys.motion.duration.long3` | 550ms |
|
||||
| `md.sys.motion.duration.long4` | 600ms |
|
||||
| `md.sys.motion.duration.extra-long1` | 700ms |
|
||||
| `md.sys.motion.duration.extra-long2` | 800ms |
|
||||
| `md.sys.motion.duration.extra-long3` | 900ms |
|
||||
| `md.sys.motion.duration.extra-long4` | 1000ms |
|
||||
|
||||
### M3 vs M3 Expressive differences
|
||||
|
||||
- **Physics-based motion system** (springs: stiffness/damping/initial velocity) replaces the
|
||||
easing-and-duration system as the primary motion model. Legacy easing/duration tokens remain as a
|
||||
fallback and still drive some transitions.
|
||||
- Two schemes, **Expressive** and **Standard**, selectable/customizable at up to 3 levels of depth
|
||||
(default scheme → custom scheme → per-element scheme override).
|
||||
- Spring tokens split into **spatial** vs **effects**, each with **fast/default/slow** — a 2×3 matrix per
|
||||
scheme (12 values total across both schemes).
|
||||
- 21 Jetpack Compose components use the physics system by default; component motion is largely driven by
|
||||
just two tokens in practice: **expressive fast spatial** and **expressive fast effects**.
|
||||
- Shape morph (see Shape section) uses the **expressive** motion scheme by default.
|
||||
|
||||
### Accessibility requirements
|
||||
|
||||
- No general "prefers-reduced-motion" policy was found stated on `styles/motion` itself. Reduced-motion
|
||||
handling is documented per component instead — e.g. `components/carousel/accessibility`: "When reduced
|
||||
motion settings are turned on, the parallax effect should be removed and carousel items should no
|
||||
longer expand as they come into view. All items are the same size," and hero carousels should keep the
|
||||
small item only partially visible rather than animating it. (Component-level source, included here
|
||||
because it is the only "reduced motion" guidance found in the crawl.)
|
||||
- `components/menus/guidelines`: "In dense products, such as on desktop, menus can open instantly to
|
||||
reduce motion."
|
||||
|
||||
### Source URLs
|
||||
|
||||
- https://m3.material.io/styles/motion (= /styles/motion/overview = /styles/motion/overview/how-it-works)
|
||||
- https://m3.material.io/styles/motion/overview/specs
|
||||
- https://m3.material.io/styles/motion/easing-and-duration
|
||||
- https://m3.material.io/styles/motion/easing-and-duration/tokens-specs
|
||||
- https://m3.material.io/styles/motion/transitions
|
||||
- https://m3.material.io/components/carousel/accessibility (reduced motion example)
|
||||
- https://m3.material.io/components/menus/guidelines (reduced motion example)
|
||||
- https://raw.githubusercontent.com/androidx/androidx/androidx-main/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/MotionTokens.kt
|
||||
- .../tokens/ExpressiveMotionTokens.kt
|
||||
- .../tokens/StandardMotionTokens.kt
|
||||
|
||||
---
|
||||
|
||||
## Shape
|
||||
|
||||
### Principles
|
||||
|
||||
- The M3 shape system includes original shapes, a corner radius scale, and built-in shape morphing.
|
||||
- Use abstract shapes thoughtfully to add emphasis and decorative flair; leverage Material shapes for
|
||||
built-in shape morphing.
|
||||
- Rectangular shapes are fully rounded in all corners by default; individual corners can be adjusted to
|
||||
create asymmetrical shapes.
|
||||
- Use shape and type "in harmony" — M3 shapes are designed to echo key visual attributes of M3 typography
|
||||
(e.g. M3 shapes and Google Sans Flex share roundness attributes).
|
||||
- Shape morph should respond to user interaction and connect function and feeling — use it to communicate
|
||||
interaction states (e.g. a selected button), actions in progress (e.g. typing, loading), or environment
|
||||
changes (sound, temperature, time of day).
|
||||
- "Be bold and dare to embrace tension" — contrast (square + rounded, unconventional shapes) creates more
|
||||
dynamic, memorable, expressive design; Material historically favored rounded shapes only.
|
||||
- Shape is **versatile, not semantic** — avoid making a shape literal or assigning it one fixed meaning
|
||||
(e.g. a wavy loading indicator isn't a literal progress waveform and could be reused elsewhere).
|
||||
- Use abstract shapes sparingly — don't compromise clarity for visual design; be intentional and consider
|
||||
how a shape fits the product narrative.
|
||||
- Emphasize aesthetic moments (imagery, avatar masking) with shape — these are the most flexible,
|
||||
creative uses.
|
||||
- Shape (with motion) can create a 2.5D illusion of depth on flat visuals.
|
||||
|
||||
### Rules (do/don't)
|
||||
|
||||
- Components can have symmetric (all corners equal) or asymmetric ("inner corner") shapes; both use the
|
||||
same 10-step scale. Asymmetric/inner corners are used in components with closely-grouped items (menus,
|
||||
split buttons) — inner-corner component tokens always map to individual corner-shape tokens.
|
||||
- Customization happens at **style** level (changing what a named style like "medium" resolves to —
|
||||
affects every component mapped to it, unless overridden) or **component** level (remapping one
|
||||
component to a different named style, e.g. remapping a button's default "full" to "small"/"medium").
|
||||
- The shape style family can be customized from **rounded** to **cut** (straight line instead of curve);
|
||||
add extra padding to avoid clipping content, especially in information-dense components. **Caution**:
|
||||
don't apply large/full corners to information-dense components (e.g. cards).
|
||||
- **Optical roundness**: when nesting rounded objects, don't reuse the same radius for both. Formula:
|
||||
**outer radius − padding = inner radius** (example given: 48dp − 14dp = 34dp). **Do** use different,
|
||||
proportional radii for nested components; **don't** use the same radius as the container.
|
||||
- Use the shape library for mostly visual/decorative elements; avoid unconventional shapes on text-heavy
|
||||
containers; use shapes sparingly for emphasis and delight.
|
||||
|
||||
### Values
|
||||
|
||||
#### Corner radius scale (10 styles; matches androidx `ShapeTokens.kt` exactly)
|
||||
|
||||
| # | Style | Value |
|
||||
| --- | --- | --- |
|
||||
| 1 | None | 0dp |
|
||||
| 2 | Extra small | 4dp |
|
||||
| 3 | Small | 8dp |
|
||||
| 4 | Medium | 12dp |
|
||||
| 5 | Large | 16dp |
|
||||
| 6 | Large increased | 20dp |
|
||||
| 7 | Extra large | 28dp |
|
||||
| 8 | Extra large increased | 32dp |
|
||||
| 9 | Extra extra large | 48dp |
|
||||
| 10 | Full | fully rounded (Compose: `CircleShape`, not a fixed dp value) |
|
||||
|
||||
(M2 used a 3-level shape scale based on component-container size; M3 uses this 10-level scale based on
|
||||
corner roundedness instead.)
|
||||
|
||||
#### Shape library
|
||||
|
||||
35 pre-made shapes in the Material shape library (Figma Design Kit / Jetpack Compose `MaterialShapes`
|
||||
API), designed to morph seamlessly into one another. Used by the standard **button group** and
|
||||
**loading indicator** components by default.
|
||||
|
||||
### M3 vs M3 Expressive differences (May 2025 "M3 Expressive update")
|
||||
|
||||
- Added **35 new shapes** and **shape morphing** to the Material Shape Library (Figma) and Jetpack
|
||||
Compose (`MaterialShapes` API).
|
||||
- Added new shape principles and a refreshed art direction.
|
||||
- Added corner-radius tokens: **Large increased (20dp)**, **Extra large increased (32dp)**,
|
||||
**Extra extra large (48dp)** — these three styles are new to the 10-step scale.
|
||||
- Updated "fully rounded" corners to use the dedicated **Full** style; previously this was defined as
|
||||
50% of the component's size.
|
||||
- Shape morphing uses the **expressive** motion scheme by default (see Motion section); can be switched
|
||||
to the standard scheme. Access is via a platform API — Android: Compose `MaterialShapes`; Web: not
|
||||
currently available.
|
||||
|
||||
### Accessibility requirements
|
||||
|
||||
No shape-specific accessibility requirements (contrast ratios, color-alone rule, reduced motion) were
|
||||
found on the fetched shape pages. The only adjacent accessibility-flavored guidance is the "optical
|
||||
roundness" and "cut corners need extra padding" rules above, which are about legibility/clipping rather
|
||||
than a formal accessibility requirement.
|
||||
|
||||
### Source URLs
|
||||
|
||||
- https://m3.material.io/styles/shape (= /styles/shape/overview-principles)
|
||||
- https://m3.material.io/styles/shape/corner-radius-scale
|
||||
- https://m3.material.io/styles/shape/shape-morph
|
||||
- https://raw.githubusercontent.com/androidx/androidx/androidx-main/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ShapeTokens.kt
|
||||
|
||||
---
|
||||
|
||||
## Typography
|
||||
|
||||
### Principles
|
||||
|
||||
- Use typography to make content readable and beautiful.
|
||||
- M3 has **one type scale** containing two sets of type styles: **15 baseline** and **15 emphasized**
|
||||
(added in the Expressive update) — 30 styles total, both sets following the same scale from Display
|
||||
Large to Label Small.
|
||||
- Five style **roles**: **Display, Headline, Title, Body, Label** — each with Large/Medium/Small (Display/
|
||||
Headline/Title/Body) — see the type-scale table below for exact members.
|
||||
- **Display**: largest text on screen; short, important text/numerals; best on large screens; can use
|
||||
a more expressive/handwritten/script font; set optical size appropriately.
|
||||
- **Headline**: short, high-emphasis text on smaller screens; can use expressive typefaces if line
|
||||
height/letter spacing are adjusted to preserve readability.
|
||||
- **Title**: medium-emphasis, relatively short text (e.g. dividing secondary content); use caution with
|
||||
expressive/display/handwritten/script fonts here.
|
||||
- **Body**: longer passages; use typefaces meant for body copy, readable at small size; avoid
|
||||
expressive/decorative fonts.
|
||||
- **Label**: small, utilitarian text inside components (e.g. buttons use Label Large) or captions.
|
||||
- Two typeface roles: **Brand** (larger styles — Display/Headline — focused on expression) and **Plain**
|
||||
(smaller styles — Body/Label — focused on readability). Roboto is the default for both.
|
||||
- Typesetting: use **padding + bounding box** method for Web/iOS (line height = bounding box height, text
|
||||
vertically centered — CSS "half-leading"); use the **baseline** method for Android/platform-agnostic
|
||||
specs (distances measured to the invisible baseline a line of text rests on).
|
||||
- **Line height**: recommend a **1.2×** ratio of type size for larger styles (title/headline/display);
|
||||
recommend **~1.5×** for smaller styles (body/label) — too tight undermines flow, too loose feels
|
||||
incohesive.
|
||||
- Use **tabular (monospaced) numbers**, not proportional digits, wherever values change often (tables,
|
||||
clocks) to prevent layout shift and aid scanning.
|
||||
- Language script height: line height can auto-adapt by script category so components (with vertical
|
||||
padding) don't clip. Categories: **Small/base** (~Latin, Cyrillic, Greek, Hebrew, except Vietnamese);
|
||||
**Medium** (~7% taller: Arabic, Chinese, Hindi, Japanese, Korean, Vietnamese, most others); **Large**
|
||||
(~30% taller: Burmese, Telugu); **Extra large** (~100% taller: Nastaliq). Default to Medium since most
|
||||
languages fall there; components with a fixed height are built for Small and may not auto-adapt.
|
||||
- Material's default type scale follows the **Major Second (1.125)** modular scale, anchored to 14 (the
|
||||
base size for body text).
|
||||
|
||||
### Rules (do/don't)
|
||||
|
||||
- Emphasized styles are **not** used by default by Material components — swap the baseline token for the
|
||||
matching emphasized token (e.g. `md.sys.typescale.display-large` → `md.sys.typescale.emphasized.
|
||||
display-large`) deliberately.
|
||||
- Recommended emphasized-style use: badges, buttons (primary actions), extended FAB, selected list items,
|
||||
selected menu items, and generally selection/actions/headlines/other editorial-treatment moments —
|
||||
not just for decoration.
|
||||
- Emphasized can be combined with **weight** (apply to already-bold text) and **context** (apply
|
||||
selectively to selected/unread/key-interaction states) simultaneously.
|
||||
- When customizing the scale: change brand/plain typeface tokens if swapping fonts; adjust line height
|
||||
and letter spacing to refine appearance; **avoid changing type size** (can break component
|
||||
layout/reflow); keep both baseline and emphasized styles visually consistent (e.g. all wider than
|
||||
baseline, not just some).
|
||||
- Customizing the M3 type scale or individual styles may forgo future Material typography token updates.
|
||||
- Hyperlinks: use **primary** (or **tertiary** for a less prominent link) and the text must be
|
||||
**underlined** — never color alone.
|
||||
- **Do** use the same size and same optical weight for Material Symbols paired with text; **don't** mix.
|
||||
|
||||
### Values
|
||||
|
||||
#### Type scale — baseline set (`TypeScaleTokens.kt`)
|
||||
|
||||
| Style | Size | Line height | Tracking | Weight | Family |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| Display Large | 57sp | 64sp | −0.2sp | Regular | Brand |
|
||||
| Display Medium | 45sp | 52sp | 0sp | Regular | Brand |
|
||||
| Display Small | 36sp | 44sp | 0sp | Regular | Brand |
|
||||
| Headline Large | 32sp | 40sp | 0sp | Regular | Brand |
|
||||
| Headline Medium | 28sp | 36sp | 0sp | Regular | Brand |
|
||||
| Headline Small | 24sp | 32sp | 0sp | Regular | Brand |
|
||||
| Title Large | 22sp | 28sp | 0sp | Regular | Brand |
|
||||
| Title Medium | 16sp | 24sp | 0.2sp | Medium | Plain |
|
||||
| Title Small | 14sp | 20sp | 0.1sp | Medium | Plain |
|
||||
| Body Large | 16sp | 24sp | 0.5sp | Regular | Plain |
|
||||
| Body Medium | 14sp | 20sp | 0.2sp | Regular | Plain |
|
||||
| Body Small | 12sp | 16sp | 0.4sp | Regular | Plain |
|
||||
| Label Large | 14sp | 20sp | 0.1sp | Medium | Plain |
|
||||
| Label Medium | 12sp | 16sp | 0.5sp | Medium | Plain |
|
||||
| Label Small | 11sp | 16sp | 0.5sp | Medium | Plain |
|
||||
|
||||
#### Type scale — emphasized set (Expressive; same size/line-height/family as baseline, heavier weight
|
||||
and slightly different tracking on some styles)
|
||||
|
||||
| Style | Size | Line height | Tracking | Weight | Family |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| Display Large Emphasized | 57sp | 64sp | 0sp | Medium | Brand |
|
||||
| Display Medium Emphasized | 45sp | 52sp | 0sp | Medium | Brand |
|
||||
| Display Small Emphasized | 36sp | 44sp | 0sp | Medium | Brand |
|
||||
| Headline Large Emphasized | 32sp | 40sp | 0sp | Medium | Brand |
|
||||
| Headline Medium Emphasized | 28sp | 36sp | 0sp | Medium | Brand |
|
||||
| Headline Small Emphasized | 24sp | 32sp | 0sp | Medium | Brand |
|
||||
| Title Large Emphasized | 22sp | 28sp | 0sp | Medium | Brand |
|
||||
| Title Medium Emphasized | 16sp | 24sp | 0.15sp | Bold | Plain |
|
||||
| Title Small Emphasized | 14sp | 20sp | 0.1sp | Bold | Plain |
|
||||
| Body Large Emphasized | 16sp | 24sp | 0.15sp | Medium | Plain |
|
||||
| Body Medium Emphasized | 14sp | 20sp | 0.25sp | Medium | Plain |
|
||||
| Body Small Emphasized | 12sp | 16sp | 0.4sp | Medium | Plain |
|
||||
| Label Large Emphasized | 14sp | 20sp | 0.1sp | Bold | Plain |
|
||||
| Label Medium Emphasized | 12sp | 16sp | 0.5sp | Bold | Plain |
|
||||
| Label Small Emphasized | 11sp | 16sp | 0.5sp | Bold | Plain |
|
||||
|
||||
(Size, line height and font family are unchanged between a baseline style and its emphasized counterpart;
|
||||
weight steps up — Regular→Medium or Medium→Bold — and a few styles gain slightly wider tracking. Source:
|
||||
`TypeScaleTokens.kt`; the site's `styles/typography/type-scale-tokens` page describes these sets in prose
|
||||
and confirms "30 styles: 15 baseline, 15 emphasized" and the naming convention, but renders the actual
|
||||
numeric table only inside an interactive, non-text widget.)
|
||||
|
||||
`TypefaceTokens.kt` weight mapping used above: Regular = `FontWeight.Normal`, Medium = `FontWeight.
|
||||
Medium`, Bold = `FontWeight.Bold`; Brand/Plain both default to the platform sans-serif (Compose fallback;
|
||||
the actual M3 web/design spec default typeface is **Roboto** for both Brand and Plain, per
|
||||
`styles/typography/fonts`).
|
||||
|
||||
#### Fonts
|
||||
|
||||
| Font | Role | Notes |
|
||||
| --- | --- | --- |
|
||||
| Roboto | Default (Brand + Plain) | Static; used in the M3 type scale by default; 3,300+ glyphs |
|
||||
| Roboto Flex | Variable, not yet part of the type scale | Axes: Slant, Width, Weight, Grade, Optical Size, plus advanced axes (thick stroke XOPQ, thin stroke YOPQ, counter width XTRA, uppercase height YTUC, lowercase height YTLC, ascender height YTAS, descender depth YTDE, figure height YTFI); width range 25–150 (example) |
|
||||
| Roboto Serif | Variable | Reading-focused; wide weight/width range |
|
||||
| Roboto Mono | Variable | Axes: Weight, Italic; monospaced, good for code/tabular numbers |
|
||||
| Noto Sans | Fallback | Axes: Width, Weight, Italic; 150+ scripts, used when a language is unsupported |
|
||||
|
||||
Font fallback order (e.g. for Roboto Flex): **Roboto Flex → Roboto → Noto Sans**.
|
||||
|
||||
#### Units
|
||||
|
||||
| Platform | Font size unit | Conversion | Letter spacing unit | Conversion |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Android | sp | 1.0 (base) | em | tracking(px) / font size(sp) |
|
||||
| Web | rem | sp / 16 = rem (16px root default) | rem | tracking(px) / font size(sp) |
|
||||
|
||||
Example: 16sp = 1rem; 24sp = 1.5rem; 60sp = 3.75rem. 0.2 tracking / 16sp = 0.0125 em (or rem on web).
|
||||
|
||||
### M3 vs M3 Expressive differences (May 2025 "M3 Expressive update")
|
||||
|
||||
- Added **15 emphasized type styles** alongside the existing 15 baseline styles — 30 total. Emphasized
|
||||
styles have higher weight (and minor tracking adjustments) and are meant for bold/selection/emphasis
|
||||
moments; baseline and emphasized are meant to be used together, not as a full replacement.
|
||||
- New **emphasized token namespace**: `md.sys.typescale.emphasized.<style>` alongside
|
||||
`md.sys.typescale.<style>`.
|
||||
- (Aug 2026, post-Expressive) **Language script height support**: line height can automatically adapt by
|
||||
language script category (small/medium/large/extra large) — see Principles above.
|
||||
- Roboto Flex / variable-font axis guidance (weight, grade, width, optical size — "editorial treatments")
|
||||
is presented as an Expressive-era authoring technique, though Roboto Flex itself is "not yet part of
|
||||
the M3 type scale."
|
||||
|
||||
### Accessibility requirements
|
||||
|
||||
- Text contrast: **≥3:1** for large text (≥14pt bold / ≥18pt regular) and graphics; **≥4.5:1** for small
|
||||
text (restated from `foundations/designing/color-contrast`).
|
||||
- Default typography color is **on surface** (or **on surface variant**).
|
||||
- **Color-alone rule**: hyperlinked text must be **underlined**, in addition to using primary/tertiary color.
|
||||
- Avoid very light/thin weights for body text — low-resolution displays can struggle to render thin type,
|
||||
especially at small sizes; reserve very light weights for larger sizes such as display type.
|
||||
- Avoid excessive weight/bold at small label sizes — it can reduce readability (navigation-bar example).
|
||||
- Tabular numbers should be used where digits change frequently, to avoid disorienting layout shifts.
|
||||
|
||||
### Source URLs
|
||||
|
||||
- https://m3.material.io/styles/typography (= /styles/typography/overview)
|
||||
- https://m3.material.io/styles/typography/type-scale-tokens
|
||||
- https://m3.material.io/styles/typography/fonts
|
||||
- https://m3.material.io/styles/typography/applying-type
|
||||
- https://m3.material.io/styles/typography/editorial-treatments
|
||||
- https://raw.githubusercontent.com/androidx/androidx/androidx-main/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/TypeScaleTokens.kt
|
||||
- .../tokens/TypefaceTokens.kt
|
||||
Reference in New Issue
Block a user