A modal side sheet, bottom sheet or the modal rail closed on any Escape the window heard, so a dialog opened from a sheet, a menu, select list or searchable choice inside one, a sheet opened from a sheet, and a sheet inside a dialog each closed two layers on one press. And a dialog or a second sheet rendered elsewhere on the page sat inside the `aria-hidden` the first sheet's `x-trap.inert` put on its siblings, so a screen reader could not read it, while the sheet's focus trap took every Tab inside the dialog back to the inert sheet. resources/js/layers.js adds `x-layer`, on each of those panels beside its `x-trap`. An Escape is the panel's only when nothing has handled it and the nearest open layer around its target is the panel itself - not an open dialog, popover or customizable select, nor a panel inside it; the panel claims it with preventDefault(), which also keeps a dialog around it from cancelling, and dispatches `material-escape`, which the views close on. A panel that opens lifts `aria-hidden` from its own ancestors and puts it back on close only where a panel still open hides them; materialShowModal() does the same for `<x-modal>`, whose new `x-trap.noautofocus.noreturn` pauses the sheet's focus trap while it is open and moves no focus of its own. The searchable choice, the search view and the supporting pane's sheet now preventDefault() the Escape they act on, so the dialog or sheet around them stays. Four browser tests stack the layers every way above and fail without the change in Chrome. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
282 lines
20 KiB
Markdown
282 lines
20 KiB
Markdown
# Upgrading
|
|
|
|
## From 2.1.0 to 2.1.1
|
|
|
|
- **`$store.rail.toggle()`** flips what the first rail on the page draws, as its menu button does.
|
|
With nothing stored, `<x-scaffold>`'s rail is drawn collapsed from 840 to 1199px whatever
|
|
`rail.default` says, and `toggle()` collapsed it again, so the first press of an application's
|
|
shortcut changed nothing; it now expands the rail. Below 840px, where there is no room to expand
|
|
it in the layout, `toggle()` opens and closes the modal rail (`show()`, `hide()`) instead of
|
|
changing a choice nothing draws there. A shortcut that pressed the rail's menu button to get this
|
|
can call `$store.rail.toggle()` again.
|
|
- **`DesignGuard`** no longer reads a Markdown mail component's classes as Tailwind: a view under
|
|
`mail.markdown.paths` (`resources/views/vendor/mail` by default) is drawn by the mail theme, whose
|
|
`table`, `button` and `panel` share Tailwind's names, so only its icon names, the directives in
|
|
its component tags and the application's own bans are checked there. A mail theme stylesheet
|
|
under that path is no longer checked for literal values, and its classes no longer exempt the
|
|
same names in the application's other views. A filter an application wrote around such a
|
|
finding can go.
|
|
- **`DesignGuard`** reports a colour utility on a colour the application's own Tailwind theme
|
|
named (`bg-brand`, `text-sport-run`, `from-brand`, `border-l-zone-4`, `bg-brand/8`) in a class
|
|
list, with `var(--…)` of the application's own as its replacement, unless the application's
|
|
CSS declares the class. None of them has compiled since 2.0.0, and they were the leftovers its
|
|
count never showed. Tailwind's other utilities on the same prefixes are named by their own
|
|
family now too: `border-collapse`, `border-spacing-*`, `from-10%`, `via-none`, `fill-none`,
|
|
`stroke-*`, `text-shadow-*`, `text-inherit`, and Tailwind 4.2's `mauve`, `olive`, `mist` and
|
|
`taupe` palettes. A test that expects no findings may fail on them.
|
|
- **A selected row written by hand** — `data-md-list-row` with `data-md-selected` on an `<li>`, a
|
|
`<div>` or a `<tr>`, not `<x-list-item>` or `<x-card>` — is drawn in `secondary-container` with
|
|
`on-secondary-container` ink again, as 1.x's `data-list-row` with `data-selected` was; 2.0.0
|
|
dropped the fill without saying so. Hover, focus and press now tint the fill instead of hiding
|
|
it. In `<x-table>` a plain row takes `data-md-selected` too, beside `aria-selected="true"`. A
|
|
card still takes no selected fill.
|
|
- **67 Material Symbols** in each cut (`auto_awesome`, `insights`, `tips_and_updates`,
|
|
`battery_50` …) are drawn whole at every size. Google publishes them in 24 units (20 in the 20
|
|
cut) with a width and height but no viewBox, and the package dropped the size, so they fitted
|
|
only at exactly 24px and were cut off or misplaced at any other — the 20 cut every small button
|
|
draws from included. Nothing to change.
|
|
- **A focused text field keeps its focus edge under the pointer.** The hover rule outweighed focus,
|
|
so a focused field the pointer rested on drew a 2px `on-surface` edge instead of `primary`, and
|
|
a field in error `on-error-container` instead of `error` — in the filled variant too, and on a
|
|
customizable `<x-select>` while its menu is open. An open select in error now draws `error`
|
|
rather than `primary`. A test that read the hover colour off a focused field reads `primary`
|
|
now.
|
|
- **Escape closes one layer.** A dialog, menu, customizable select or searchable choice opened
|
|
over or inside a modal `<x-drawer>`, `<x-bottom-sheet>` or modal navigation rail — or a sheet
|
|
opened from a sheet, or inside `<x-modal>` — closed the layer under it on the same Escape, since
|
|
each sheet closed on any Escape the window heard. Now only the topmost layer closes; a sheet
|
|
with `close-on-escape` off still keeps the Escape from closing what is under it. A dialog or
|
|
sheet opened from a modal sheet but rendered elsewhere on the page was also hidden from screen
|
|
readers by the sheet's `aria-hidden`, and Tab could not move inside the dialog; both work now.
|
|
An application's own guard around a sheet's Escape can go.
|
|
|
|
## From 2.0.0 to 2.1.0
|
|
|
|
- **Browsers:** Firefox 151 or later (was 147), for container style queries on a custom property;
|
|
Chrome 125 and Safari 18.4 are unchanged.
|
|
- **`<x-navigation-rail>`** publishes its value as `--md-navigation-rail-value`, `collapsed` or
|
|
`expanded` (M3's two rail values). Content an application puts in a rail reads it with
|
|
`@container style(--md-navigation-rail-value: collapsed)` instead of copying the rail's
|
|
conditions from `navigation-rail.css`.
|
|
- **`<x-loading size="96">`** sizes the loading indicator in px, 24 to 240 (M3's responsive range),
|
|
with the container and the shape in proportion. A width and height of the application's own
|
|
still work.
|
|
|
|
## From 1.x to 2.0.0
|
|
|
|
2.0.0 aligns the library with Material Design 3 Expressive as Google documents it
|
|
(m3.material.io, checked page by page; the audits are in `docs/audits/m3-alignment/`). Most of
|
|
the change is inside the components. What reaches an application is below, in the order to do it.
|
|
|
|
### 1. Breakpoints are M3's window size classes
|
|
|
|
Tailwind's `sm:`, `md:`, `lg:`, `xl:` and `2xl:` no longer compile, and there are no responsive
|
|
variants in their place: the breakpoints are M3's window size classes — compact below 600px, then
|
|
medium (600px), expanded (840px), large (1200px) and extra-large (1600px) — and only those. A
|
|
layout component takes the class as a prop (`hide-below`, `hide-from`, `stack-below`, `<x-grid>`'s
|
|
`columns` map); the application's own CSS writes the width as a range media query:
|
|
|
|
| Was | Becomes |
|
|
|---|---|
|
|
| `sm:` / `max-sm:` (640) | `medium` · `@media (width >= 600px)` / `(width < 600px)` |
|
|
| `md:` / `max-md:` (768) | `medium` or `expanded` — choose by what the layout means |
|
|
| `lg:` / `max-lg:` (1024) | `expanded` · `@media (width >= 840px)` / `(width < 840px)` |
|
|
| `xl:` / `max-xl:` (1280) | `large` · `@media (width >= 1200px)` / `(width < 1200px)` |
|
|
| `2xl:` (1536) | `extra-large` · `@media (width >= 1600px)` |
|
|
|
|
`<div class="hidden lg:block">` is `<x-stack hide-below="expanded">`; `flex flex-col sm:flex-row` is
|
|
`<x-row stack-below="medium">`; `grid-cols-1 lg:grid-cols-2` is
|
|
`<x-grid :columns="['compact' => 1, 'expanded' => 2]">`. Scripts read the same numbers from
|
|
`resources/js/breakpoints.js` (`from('expanded')`, `upTo('medium')`). `DesignGuard` reports every
|
|
leftover prefix with its replacement.
|
|
|
|
### 2. Only M3's scales, as tokens
|
|
|
|
Tailwind's radius, shadow, text-size, weight, leading, tracking and easing utilities compile to
|
|
nothing, and so do the 1.x utilities that stood for M3's scales. A text style is a class; the
|
|
rest is a token in the application's own CSS:
|
|
|
|
| Was | Becomes |
|
|
|---|---|
|
|
| `rounded-sm` … `rounded-4xl`, `rounded-corner-*` (1.x) | `var(--md-sys-shape-corner-xs)` … `var(--md-sys-shape-corner-xxl)` (`-full`, `-none`), or `<x-surface corner="xs">` |
|
|
| `shadow-sm` … `shadow-2xl`, `shadow-elevation-*` (1.x) | `var(--md-sys-elevation-1)` … `var(--md-sys-elevation-5)` |
|
|
| `text-xs` … `text-9xl`, `leading-*`, `tracking-*`, `font-medium` … `font-black`, `type-*` (1.x) | one `md-type-*` class (`md-type-body-md`, `md-type-emphasized-title-md` …), or `font: var(--md-sys-typescale-body-md)` with its `-tracking` |
|
|
| `ease-in`, `ease-out`, `ease-in-out`, `duration-300`, `ease-spatial-*` (1.x) | `var(--md-sys-motion-spatial-*)` / `var(--md-sys-motion-effects-*)` with its `-duration`, in a `transition` |
|
|
| `gap-4`, `p-4`, `space-y-2` | `gap="space200"`, `<x-surface padding="space200">`, `<x-stack gap="space100">`, or `var(--md-sys-measurement-space200)` |
|
|
| `state-layer`, `focus-ring`, `touch-target`, `link` (1.x) | `md-state-layer`, `md-focus-ring`, `md-touch-target`, `md-link` |
|
|
|
|
The spacing tokens are the 4px grid Tailwind's scale was (`space200` is 16px). `DesignGuard` names
|
|
each one with its replacement.
|
|
|
|
### 3. Regenerate the colour scheme
|
|
|
|
```bash
|
|
php artisan material:scheme "#4f46e5" --variant=tonal-spot # the command in the file's header
|
|
```
|
|
|
|
- `success`, `warning` and `info` are now built on the 2025 colour spec with the contrast level,
|
|
like `error`; their values change. `--harmonize` blends them toward the seed (off by default).
|
|
- The stylesheet gains medium and high contrast blocks, and `material-scheme.json` gains a
|
|
`contrast` key; `light`/`dark` at the top level are still the standard scheme, so the mail
|
|
theme needs nothing. `--contrast` must now be below 0.5; medium (0.5) and high (1.0) are
|
|
generated alongside.
|
|
- Config: `theme.contrast` (`default` `system` | `standard` | `medium` | `high`, `storage_key`)
|
|
and `motion.scheme` (`expressive` | `standard`). `$store.theme` gains `contrast`,
|
|
`resolvedContrast` and `setContrast()`; `<x-theme-toggle mode="contrast">` is a row of three.
|
|
|
|
### 4. Inks are roles, not opacities
|
|
|
|
1.x's `text-meta`, `text-body`, `text-quiet`, `border-chrome`, `border-structure` and
|
|
`border-divider` are gone with the rest of the utilities. Text takes the role's `md-ink-*` class —
|
|
`md-ink-variant` (on-surface-variant) for `text-meta` and `text-body`, `md-ink-quiet` (outline) for
|
|
`text-quiet` — and a line is `<x-divider>`, `<x-surface outlined>` or
|
|
`var(--md-sys-color-outline-variant)` in the application's CSS. Where the old translucent grey was
|
|
intended, the role is the same colour; where a template relied on the opacity to blend over a
|
|
colour, use the role directly.
|
|
|
|
### 5. Changed defaults and props
|
|
|
|
- `<x-toast>` no longer draws a state icon; `type` only picks the announcement role (`alert` for
|
|
error and warning, `status` otherwise). An actioned snackbar stays until acted on; Escape
|
|
dismisses a focused one.
|
|
- `<x-fab>` has no `disabled` prop: M3 says to remove a FAB whose action is unavailable, so hide
|
|
it instead (a form-submit FAB uses `wire:loading.attr="disabled"`).
|
|
- `<x-alert>` is `role="status"` for every colour; pass `assertive` where the notice answers
|
|
something the person just did.
|
|
- `<x-button size="xs">` and `<x-group size="xs">` are 8px wider; a `<x-button-group>` no longer
|
|
wraps; connected `xs`/`sm` segments have a 48px minimum width.
|
|
- `<x-menu-item>` rows are 48px with 16px sides; a selected item draws a trailing check unless it
|
|
has `icon-right`; a long menu scrolls.
|
|
- `<x-drawer>` renders a close button by default (`:with-close-button="false"` to drop it, ignored
|
|
on a `standard` sheet or when Escape and the scrim are off) and left-aligns its `actions` in a
|
|
72px row. `<x-drawer pane>` is removed; use `<x-list-detail>` for the second pane of a
|
|
list-detail layout.
|
|
- `<x-bottom-sheet>` opens at `50dvh` (was `90dvh`); any `height` is capped at
|
|
`calc(100dvh - 72px)`.
|
|
- `<x-carousel>` `padding` defaults to `16`; `layout="full-screen"` scrolls vertically with
|
|
edge-to-edge items; items, not the row, are the tab stops.
|
|
- `<x-list dividers>` draws a 16px-inset rule; `<x-list selectable>` or `selection="single|multi"`
|
|
makes it a `listbox` of `option`s with `aria-selected`.
|
|
- `<x-card>` changes elevation on hover instead of its corner; `data-md-card` carries the variant.
|
|
- A button directly in `<x-stack>` (stretched, the default) or `<x-form>` keeps its label's width at
|
|
the start edge instead of filling the width, as M3 asks; a full-width submit is your own CSS.
|
|
- `<x-modal>` pins its header and actions and scrolls only the body; a `box-class` that set
|
|
`overflow` no longer applies.
|
|
- `<x-table>` rows are 52px and the automatic fine-pointer density is gone: pass `dense` for 36px
|
|
rows (`size="xs" dense` is 24px).
|
|
- A field in error draws a trailing `error` icon unless it already trails something; a read-only
|
|
field no longer draws a dashed outline.
|
|
- The time picker's AM/PM buttons are radios (`aria-checked`); the password reveal no longer sets
|
|
`aria-pressed`.
|
|
- `<x-slider size="md">` grows from 44 to 52px and every value indicator from 32 to 44px tall.
|
|
- `<x-theme-toggle mode="picker"|"contrast">` is a connected group over native radios: the
|
|
`data-theme-option` hooks are gone; target `input[name="material-theme"][value="…"]`.
|
|
- `<x-tab>` panels render `id`, `aria-labelledby` and `display: none` from the server.
|
|
- `<x-section-nav>` with five or more sections is a scrollable tab bar, not a grid. The tab bar is
|
|
10px taller; the focus ring sits outside.
|
|
- The navigation bar's horizontal label is `label-md`; `place="bottom"` toolbars sit above
|
|
`--material-bottom-bar`; a standard toolbar's icon buttons are `primary`. An application that
|
|
set `--material-bottom-bar` to lift the snackbar over its own bottom toolbar removes it: the
|
|
toolbar now reads it to place itself, and publishes `--material-bottom-toolbar`, which the
|
|
snackbar clears and the page pads its end with.
|
|
- The centred app bar headline is a grid column; the `search` variant bounds its own width, so
|
|
drop hand-written `max-w-*` wrappers.
|
|
- `<x-app-shell>` is renamed `<x-scaffold>`, with no alias; it is a column with a nested row
|
|
and gains `banner` and `fab` slots. `<x-navigation-rail>`'s `header` slot takes one
|
|
`<x-fab label icon>` that morphs (replace the two-FAB `rail-collapsed:` swap); new `divider`
|
|
and `fill` props. The `rail-collapsed:` variant is gone: style what the application puts in a
|
|
rail by the rail's value, `@container style(--md-navigation-rail-value: collapsed)` (2.1.0).
|
|
`data-app-shell`, `data-app-shell-bar`, `data-app-shell-actions` and
|
|
`data-app-shell-banner` are `data-md-scaffold`, `data-md-scaffold-bar`, `data-md-scaffold-actions`
|
|
and `data-md-scaffold-banner`; the skip link is `data-md-skip-link`.
|
|
- `<x-icon optical="20">` selects the optical-size-20 cut; the components pass it for their own
|
|
small icons, applications pass it for icons drawn at 20px or less.
|
|
- A text field stops at 40rem wide from medium (600px), as M3 bounds fields on wider windows; a
|
|
width rule of the application's own on the field beats it, and `full` removes it.
|
|
- Every `<x-modal>` draws a rule under its header and over its actions while its body scrolls,
|
|
and neither when the body fits. `separator` now means "always draw both rules" and no longer
|
|
renders two `<x-divider>` elements; the spacing between header, body and actions moved to
|
|
M3's split gaps, so a dialog that fits is a few pixels shorter.
|
|
- A snackbar with a `description` is 68px tall, and below medium (600px) a two-line snackbar with an
|
|
action puts the action under the text. Alt+G moves focus to an actioned snackbar.
|
|
- `<x-bottom-sheet>`'s drag now follows the pointer and settles on the nearest preset height when
|
|
`heights` or `snap` is set; without stops it behaves as before.
|
|
- A chip set with `scroll` shows a scroll button over each overflowing edge on fine pointers.
|
|
|
|
### 6. `material.css` is gone, and so is Tailwind
|
|
|
|
The 1.x single-import shortcut no longer exists, and Tailwind has left the whole stack — the
|
|
package, its showcase, error pages and Workbench build carry none, and an application drops it
|
|
too. The package's CSS is plain, no build step of its own, in one entry:
|
|
`resources/css/all.css` for everything, or `foundation.css` first and then the stylesheet of each
|
|
component the views render, opening with the layer statement every package stylesheet does
|
|
(see Installation in `README.md`). An application's views write no utility layer of their own
|
|
either: layout components (`<x-scaffold>`, `<x-pane>`, `<x-stack>`, `<x-row>`, `<x-grid>`, the
|
|
canonical layouts) take M3's spacing tokens and breakpoints as props, a small set of `md-type-*`
|
|
and `md-ink-*` classes covers text on plain elements, and `--md-sys-*` custom properties serve the
|
|
rest of an application's own stylesheet. The foundation smooths text in grayscale, as Tailwind's
|
|
`antialiased` class did: drop the class.
|
|
|
|
### 7. New in 2.0.0
|
|
|
|
Nothing to migrate, but worth knowing: submenus (`<x-menu-item submenu>`), a filtering menu
|
|
(`<x-menu filter>`), a menu that opens as a bottom sheet on compact windows
|
|
(`<x-menu sheet-at-compact>`), gap-grouped menu items (`<x-menu-group gap>`), square button groups
|
|
(`shape="square"`), managed selection on connected groups (`selection="single|multi" required`),
|
|
FAB collapse on scroll (`<x-fab collapse-on-scroll>`), the tall navigation bar and hide-on-scroll
|
|
(`tall`, `hide-on-scroll`; `tall-bar`, `hide-bar-on-scroll` on the shell), the narrow, centred and
|
|
hide-when-collapsed rail (`width="narrow"`, `align="center"`, `hide-when-collapsed`), a docked
|
|
toolbar with a FAB and a rounded large-screen form (`rounded`), app bar actions that overflow into
|
|
a menu (`:actions="[…]"`), the list item's `video` slot, the card's dragged state, the standard
|
|
side sheet (`<x-drawer standard>`), bottom sheet preset heights, the multi-aspect carousel
|
|
(`layout="multi-aspect"`), the divider with a subheader (`<x-divider text>`), the character counter
|
|
(`counter`), the vertical slider (`orientation="vertical"`), the full-screen range date picker on
|
|
compact windows, search's icon entry point and suggestions (`trigger="icon"`, `suggestions` slot),
|
|
the contrast toggle (`<x-theme-toggle mode="contrast">`) and the Standard motion scheme.
|
|
|
|
### 8. Tests and guards
|
|
|
|
`DesignGuard` changes with the stack:
|
|
|
|
- **Removed:** the maryUI tag and daisyUI class checks, `forbidAbsolutes()` and `forbidOpacityInk()`.
|
|
Every Tailwind utility now compiles to nothing and is reported with its replacement, so
|
|
`bg-white` and `text-on-surface/60` still are.
|
|
- **Retargeted:** `forbidColours([...])` keeps its signature and reports a left-out role where 2.0.0
|
|
writes one: `var(--md-sys-color-…)` in CSS or an inline `style`, an `md-ink-*` class, a
|
|
component's `color`/`tone` prop.
|
|
- **New:** `missingStylesheets($cssEntry)` names each `@import` the views need, and
|
|
`unusedStylesheets($cssEntry)` each one they no longer do; a `.css` file passed
|
|
to `scan()` is checked for literal values and off-scale media queries.
|
|
|
|
Browser tests that assert widths switch at 640/1024/1280 now
|
|
switch at 600/840/1200; tests that read `role="alert"` on an alert, `aria-pressed` on the time
|
|
picker's period buttons or `data-theme-option` need the new hooks above.
|
|
|
|
Every component hook is prefixed `data-md-`: `data-toolbar-place` is `data-md-toolbar-place`,
|
|
`data-account-menu` is `data-md-account-menu`, `data-field-copy` is `data-md-field-copy`. The
|
|
attributes on `<html>` keep their names (`data-theme`, `data-contrast`, `data-scheme`,
|
|
`data-motion`, `data-rail`). A component's inner parts also carry its name:
|
|
|
|
| 1.x | 2.0.0 |
|
|
|---|---|
|
|
| `data-app-shell`, `-bar`, `-actions`, `-banner` | `data-md-scaffold`, `-bar`, `-actions`, `-banner` |
|
|
| `data-theme-option="dark"` | `input[name="material-theme"][value="dark"]` |
|
|
| `data-scheme-option="teal"` | `data-md-scheme-picker-option="teal"` |
|
|
| `data-account-theme` | `data-md-account-menu-theme` |
|
|
| `data-section-picker` | `data-md-section-nav-picker` |
|
|
| `data-material-carousel`, `-item`, `-content`, `-label`, `-surface` | `data-md-carousel`, `-item`, `-content`, `-label`, `-surface` |
|
|
| `data-sheet` (drawer) | `data-md-drawer-sheet` |
|
|
| `data-drag-handle` (bottom sheet) | `data-md-bottom-sheet-handle` |
|
|
| `data-check`, `data-mixed` (checkbox) | `data-md-checkbox-check`, `data-md-checkbox-mixed` |
|
|
| `data-on`, `data-off`, `data-handle` (toggle) | `data-md-switch-on`, `data-md-switch-off`, `data-md-switch-handle` |
|
|
| `data-handle`, `data-thumb`, `data-tick`, `data-stop`, `data-segment`, `data-track-icon` (slider) | `data-md-slider-handle`, `-thumb`, `-tick`, `-stop`, `-segment`, `-icon` |
|
|
|
|
Rename the negative assertions too: `->not->toContain('data-app-bar')` passes against 2.0.0
|
|
whatever the page renders.
|
|
|
|
### 9. For AI agents
|
|
|
|
`php artisan boost:update --discover` picks up the new `material-3` guideline and the
|
|
`material-3-design` skill, which state M3's rules and tables beside the library's classes, props
|
|
and tokens.
|