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
693 lines
55 KiB
Markdown
693 lines
55 KiB
Markdown
# 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.
|