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,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.
|
||||
Reference in New Issue
Block a user