--- name: livewire-material-development description: Build Laravel and Livewire views with Livewire Material's Material 3 Expressive Blade components — props, slots, colour roles, type, shape, motion, theming, toasts, the design guard, and the Livewire traps each component handles. --- # Livewire Material Development ## When to use this skill Use this skill when writing or changing any Blade view, Livewire component view or layout in an application that requires `nonameweb/livewire-material`, and when styling, theming or testing such views. ## Setup Composer packages must be installed before the Vite build (in Dockerfiles and CI alike), because the application's build imports from `vendor/`: ```css /* resources/css/app.css */ @import '../../vendor/nonameweb/livewire-material/resources/css/foundation.css'; @import '../../vendor/nonameweb/livewire-material/resources/css/layout/scaffold.css'; @import '../../vendor/nonameweb/livewire-material/resources/css/components/button.css'; @import '../../vendor/nonameweb/livewire-material/resources/css/components/input.css'; @import './material-scheme.css'; ``` `foundation.css` is required and first; then one stylesheet per component the views render, under `resources/css/components/` or `resources/css/layout/`, named like the component (`` is drawn by `tabs.css`, and `` needs none). Each imports the stylesheets of the components it draws (a split button's button and menu), and Vite keeps a file several of them import once. `resources/css/all.css` stands for the foundation and every component at once. A component rendered without its stylesheet is unstyled, so add the import with the tag. ```js // resources/js/app.js import '../../vendor/nonameweb/livewire-material/resources/js/material.js' ``` Every layout puts the theme script in ``, before `@vite`: ```blade @vite(['resources/css/app.css', 'resources/js/app.js']) ``` ## Colour scheme The scheme is generated, never hand-edited. Regenerate it with the seed and variant recorded at the top of `resources/css/material-scheme.css`: ```bash php artisan material:scheme "#4f46e5" --variant=tonal-spot ``` Variants: `tonal-spot` (M3's default), `vibrant`, `expressive`, `neutral`, `fidelity`, `content`, `monochrome`, `rainbow`, `fruit-salad`. `--spec` is the colour spec: `2025` (default, M3 Expressive) or `2021` (M3's original colour — keep it for a palette generated before Expressive; the library itself uses 2021 for variants 2025 does not define, and the header records the spec actually used). `--success`, `--warning` and `--info` set the source of the state colours; `--harmonize` pulls those three towards the seed (off by default: a state has to stay recognisable). The header of the stylesheet records the whole command, every option that differs from its default included. The command also writes `material-scheme.json` beside the stylesheet. `success`, `warning` and `info` are built exactly as M3 builds `error` — dynamic colours on their own tonal palette — so they follow the scheme's spec, its dark tones and its contrast level like every other role. Use them as roles (`md-ink-success` on text, `var(--md-sys-color-on-warning-container)` in your own CSS, a component's `color="warning"`), never as a hex. ### Contrast levels Every scheme is generated at M3's three levels: standard, medium (3:1) and high (7:1), for light and dark and for every profile. `--contrast` (and a profile's `contrast`) moves the **standard** level only and must stay below 0.5; medium and high are Google's fixed levels and are always written, under `[data-contrast='medium']` and `[data-contrast='high']`. - `` is the level on screen, written by `` before the first paint; standard writes no attribute, because the plain blocks are already standard. No stylesheet ever asks `prefers-contrast` — the head script does, once. - `theme.contrast.default` is `system` (follow the operating system), `standard`, `medium` or `high`, kept in localStorage under `theme.contrast.storage_key`. - In Alpine: `$store.theme.contrast` (the choice), `$store.theme.resolvedContrast` (the level showing) and `$store.theme.setContrast('high')`. `` is the ready-made row of three. - Nothing in a template names a level: a role's value changes underneath it. Never set `data-contrast` on an element to make a corner of the page higher-contrast — the level is the visitor's, page-wide. ### Colour profiles An installation that switches between several schemes lists them in `config/livewire-material.php` and runs the command without a seed, which generates every profile into the same stylesheet, keyed by ``: ```php 'profiles' => [ 'indigo' => ['label' => 'Indigo', 'seed' => '#4f46e5', 'variant' => 'vibrant'], 'teal' => ['label' => 'Teal', 'seed' => '#00897b', 'variant' => 'vibrant'], ], 'profile' => 'indigo', // the default; else the first ``` ```bash php artisan material:scheme ``` - Each profile: `seed`, and optionally `label` (default: the name as a headline), `variant` (default `tonal-spot`), `contrast` (default 0, below 0.5), `harmonize`, `spec`, `success`, `warning`, `info` (for these five, without the key the command's `--harmonize`, `--spec`, `--success`, `--warning`, `--info` or their defaults apply). Every profile is generated at all three contrast levels, keyed on `[data-scheme='x'][data-contrast='high']` and so on. - Names are lowercase letters, digits and dashes. Regenerate after changing the list; only generated profiles exist for the picker, the resolver and the stylesheet. - The application says which profile is active, once, in a service provider. The closure runs every time a colour is drawn (head script, mail, error page), so it may read the database; a name that is not a generated profile, or a closure that throws, falls back to the default: ```php use NoNameWeb\LivewireMaterial\Support\Scheme; Scheme::resolveProfileUsing(fn (): ?string => Setting::get('color_profile')); ``` - `` writes the active profile to `` before the first paint; mails and error pages draw it too. `Scheme::profiles()` lists the generated profiles (name ⇒ label, light and dark roles) and `Scheme::profile()` names the active one — validate a stored choice with `Rule::in(array_keys(Scheme::profiles()))`. Both take a contrast level as their last argument (`Scheme::load($path, $profile, 'high')`, `Scheme::profiles(contrast: 'medium')`); without one they answer with the standard level, which is what a mail wears. - Choose with `` (see Components). Never set `data-scheme` on an element inside the page expecting a different profile there: profiles key on ``. ## Tokens 2.0.0's vocabulary is plain CSS. There are no utility classes: a Tailwind class, or one of 1.x's own colour, type, shape, elevation, motion or breakpoint utilities, compiles to nothing, and the design guard names the replacement for each (see Testing the design). `resources/css/foundation.css` is the one required import, first in the entry, followed by the stylesheets of the components the views render (see Setup; 1.x's `material.css` shortcut is gone). It declares the layer order `material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility`, so an application's unlayered CSS beats every package rule, and `hidden` or `x-cloak` hides any element. It brings the reset, the page's `surface` and `on-surface` in the brand typeface smoothed in grayscale (drop Tailwind's `antialiased`), every `--md-sys-*` token, and the classes below. An application's views reach for four things, in this order: - **A component's own props** — `color`, `variant`, `size`, `dense` — before anything else. - **Layout components** for arrangement and spacing: ``, ``, ``, ``, `` inside ``s (see Layout), with `gap` and `padding` as spacing tokens (`space25` … `space900`, 2–72px) and `hide-below`, `hide-from` and `stack-below` for the window size classes. Nothing arranges by class. - **The fixed text and interaction classes** on plain elements, and nothing else: - Type, from `resources/css/text.css`: `md-type-{display|headline|title|body|label}-{lg|md|sm}` and `md-type-emphasized-…` — M3's 15 styles and their emphasized twins, one weight step heavier, rounder, with their own tracking; each sets size, line height, weight and tracking together. Emphasis is deliberate and one element at a time (M3 asks for it on badges, primary buttons, selected rows, headlines), so a regular class inside an emphasized one goes back to plain. - Ink, from the same file: `md-ink` (on-surface), `md-ink-variant` (on-surface-variant: body copy beside a title, metadata), `md-ink-quiet` (outline: decoration only), `md-ink-primary`, `md-ink-error`, `md-ink-success`, `md-ink-warning`, `md-ink-info`, `md-ink-inverse`. On a container the ink is the container's `on-` role, which the component sets. - Text layout: `md-text-start|center|end`, `md-truncate`, `md-line-clamp-2|3`, `md-nowrap`, `md-tabular`, `md-mono`, `md-visually-hidden`. Monospace is for a value read or typed character by character (a token, a recovery code, a file name, a key): put it in ``, `` or ``, which take `--md-ref-typeface-mono` already, or give another element `md-mono` after its `md-type-*` class; `` for a field. `--md-ref-typeface-mono` is the package's addition, the platform's monospace faces, not an M3 token. - Interaction, from `foundation/interaction.css`: `md-state-layer` (M3's hover, focus, press and dragged overlay in the content colour; makes the element `position: relative` and `isolation: isolate`; `data-md-dragged` draws the dragged layer), `md-focus-ring` (the keyboard focus indicator), `md-touch-target` (a 48px target around a smaller drawing), `md-link` (a link in running text, underlined; add `md-ink-primary` where it should read as primary). - **The application's own stylesheet** for the rest, on the tokens, never on a literal value: - Colour roles, `var(--md-sys-color-…)`: `primary`, `on-primary`, `primary-container`, `on-primary-container`, `primary-dim`, `inverse-primary`, `primary-fixed`, `primary-fixed-dim`, `on-primary-fixed`, `on-primary-fixed-variant`; the same for `secondary` and `tertiary`; `error`, `on-error`, `error-container`, `on-error-container`, `error-dim`; `success`, `warning` and `info` with their `on-`, `-container` and `on-…-container`; `inverse-error|success|warning|info`; `surface`, `surface-dim`, `surface-bright`, `surface-container-lowest|low||high|highest`, `on-surface`, `on-surface-variant`, `inverse-surface`, `inverse-on-surface`, `outline` (a boundary that must be read), `outline-variant` (a divider, a card's edge), `scrim`, `shadow`. There is no white or black. - Type: `font: var(--md-sys-typescale-body-md)` with `letter-spacing: var(--md-sys-typescale-body-md-tracking)`, never a size, weight or line height of your own. The typefaces are `--md-ref-typeface-brand` (Google Sans Flex), `--md-ref-typeface-plain` and `--md-ref-typeface-mono`; the weights `--md-ref-typeface-weight-regular|medium|bold`. - Shape: `var(--md-sys-shape-corner-{none|xs|sm|md|lg|lg-increased|xl|xl-increased|xxl|full})`. - Elevation: `box-shadow: var(--md-sys-elevation-{1…5})` — for what floats over content, not for panels (a panel separates by its container tone). - Motion: `var(--md-sys-motion-spatial-{fast|default|slow})` (position, size, shape; springs that overshoot) and `var(--md-sys-motion-effects-{fast|default|slow})` (colour, opacity, which never overshoot), each always with its own `-duration`: `transition: transform var(--md-sys-motion-spatial-fast-duration) var(--md-sys-motion-spatial-fast)` — M3's published web durations, spatial 350/500/650 ms and effects 150/200/300 ms. `motion.scheme` in the config picks `expressive` (the default, with the bounce) or `standard` (minimal bounce), which the head script writes to `` and which swaps the three spatial springs; a rule names a spring, never a scheme. Reduced motion zeroes every duration in both schemes. - Spacing: `var(--md-sys-measurement-space25)` … `var(--md-sys-measurement-space900)`. - States: `--md-sys-state-{hover|focus|pressed|dragged}-state-layer-opacity` and `--md-sys-state-disabled-{content|container}-opacity`, mixed into a role: `color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent)`. A scrim is `color-mix(in srgb, var(--md-sys-color-scrim) 32%, transparent)`. The rules behind these names — which role, surface container, corner, type style, elevation level, motion spring, state and window size class to use, and M3's don'ts — are the `material-3` guideline (always on) and the `material-3-design` skill (the tables and Google's source pages); activate that skill before designing a screen. ```css /* resources/css/app.css, after the imports */ .quota-meter { border-radius: var(--md-sys-shape-corner-md); background-color: var(--md-sys-color-surface-container-high); padding: var(--md-sys-measurement-space200); transition: background-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast); @media (width >= 840px) { padding: var(--md-sys-measurement-space300); } } ``` - Breakpoints are M3's window size classes, and only those: compact below 600px, medium 600px, expanded 840px, large 1200px, extra-large 1600px. A layout component takes them as props; your own CSS writes them as range media queries (`@media (width >= 840px)`, `@media (width < 600px)`), and the design guard rejects any other width; a script asks `resources/js/breakpoints.js` (`from('expanded')`, `upTo('medium')`), so a stylesheet and a script never disagree at the boundary pixel. A component's *own* width is a container query (`@container (width >= 28rem)`), which is a different thing. - The roles switch with the theme by themselves. A rule of your own that only applies in dark keys on `[data-theme='dark']` (the page's theme), never on `prefers-color-scheme` (the operating system's). - `x-figure` on an element holding one number counts it up on first appearance and on change. ## Theme `config/livewire-material.php` → `theme.default` (`light`, `dark` or `system`), `theme.storage_key`, `theme.legacy_keys`, `theme.meta`, `theme.contrast` (`default` and `storage_key`, see Contrast levels) and `motion.scheme` (`expressive`, the default, or `standard` — M3's restrained springs, written to ``). In Alpine, `$store.theme` holds `choice` (what the visitor picked), `resolved` (`light` or `dark`, what shows), `set('light'|'dark'|'system')` and `toggle()`; `x-model="$store.theme.value"` binds a control. It also holds `contrast`, `resolvedContrast` and `setContrast()` for the contrast level. With colour profiles it holds `scheme` (the profile on screen) and `previewScheme(name)`, which shows another profile on this page without storing anything. `theme.meta` (default `false`) keeps the browser's bar in the page's colour, for an installed web app: the head script sets the `content` of every `` without a `media` attribute to the resolved theme's `surface` — of the profile in ``, at the level in `` — before the first paint, adding one to `` when there is none. It follows every later change of `data-theme`, `data-scheme` or `data-contrast` (`$store.theme.set()`/`toggle()`, `setContrast()`, an OS change while `system`, `previewScheme()`), and paints the next page's meta after `wire:navigate`. A theme-color meta the layout renders itself goes before `` (after it, the script has already added one, and the page ends up with two), or is left out. A `media="(prefers-color-scheme: …)"` pair follows the OS instead of the visitor's choice: drop it when turning this on. ## Safe areas Every component that meets the edge of the screen (app bar, navigation bar and rail, docked and placed toolbars, full-screen search, dialog and side sheet, bottom sheet, the skip link) keeps clear of a notch or home indicator through `var(--material-safe-top|bottom|left|right, env(safe-area-inset-…))`. The layout needs `viewport-fit=cover` in its viewport meta for the insets to be non-zero. Set a variable to replace the device's inset, on `` or any ancestor: a browser test fakes a notch with `document.documentElement.style.setProperty('--material-safe-top', '47px')`, and an app that draws its own status strip adds its height. `--material-snackbar-height` is the height of the snackbar on screen, written on `` by `` while one shows and removed when it goes. `` reads it, so the FAB sits above the snackbar rather than under it, as M3 requires; so does an `` in ``'s `fab` slot, which places it. `--material-bottom-toolbar` is the distance from the window's bottom edge to the top of a toolbar at `place="bottom"`, unset while there is none: the snackbar clears it, and a page without `` pads its end with it (see ``). `--material-bottom-extra` (default `0px`) is the height of anything the application docks on top of the phone's navigation bar in `` (an offline banner): the scaffold adds it to `--material-bottom-bar` (64px + the bottom inset), so the snackbar, a `fab` button, the scaffold's FAB and the page's bottom padding clear it too. Set it while the docked element shows, and remove it when it goes; place the docked element itself directly above the bar, at `bottom: calc(4rem + var(--material-safe-bottom, env(safe-area-inset-bottom)))`, on a compact window (below `medium`) only. ## Toasts ```php use NoNameWeb\LivewireMaterial\Concerns\Toasts; class Settings extends Component { use Toasts; public function save(): void { // … $this->success('Settings saved'); // also warning(), error(), info() $this->info('Link copied', timeout: 6000); $this->success('Share created', redirectTo: route('shares.show', $share)); } } ``` The methods are protected. They dispatch a `toast` browser event (`assertDispatched('toast', type: 'success', title: 'Settings saved')` in tests). ## Error pages Laravel's HTTP error pages — 403, 404, 419, 429, 500, 503, and the framework's own 401 and 402 — render in M3 without setup. The provider appends the package's error views to `view.paths` after the application's, so a file in `resources/views/errors/` always wins. - The pages load `config('livewire-material.showcase.vite')` and ``, so they use the app's scheme, font and theme, and inline the error layout's own stylesheet beside them. The application imports nothing for them: no `error-page.css` in the CSS entry (the design guard's `missingStylesheets()` does not ask for it, and an entry that imports it through `all.css` does no harm). While the build is missing (a deploy in progress) they fall back to an inline stylesheet coloured from `resources/css/material-scheme.json`. - `abort(403, 'Only the owner can open this share.')` and `abort(503, '…')` show the message as the sentence. Every other string goes through `__()`; translate them in `lang/{locale}.json`. - To change wording or design, run `php artisan vendor:publish --tag=livewire-material-errors`, which copies the layout and pages to `resources/views/errors`. A page extends `errors::minimal` and sets `title`, `code`, `headline`, `message`, `shape` (an `` name) and optionally `actions`: ```blade @extends('errors::minimal') @section('title', __('Payment Required')) @section('code', '402') @section('shape', 'cookie-4') @section('headline', __('Your plan has ended')) @section('message', __('Choose a plan to keep using the app.')) @section('actions') @endsection ``` - Maintenance mode: `php artisan down --render="errors::503"`. - The showcase previews each page at `/material/errors/{code}`. ## Mail Markdown mail (notifications, and mailables built with `->markdown()`) wears M3 once the application selects the theme: ```dotenv MAIL_MARKDOWN_THEME=livewire-material::mail.theme ``` or per mail: `(new MailMessage)->theme('livewire-material::mail.theme')`, or `public $theme = 'livewire-material::mail.theme';` on a mailable. - Colours are the light scheme from `resources/css/material-scheme.json` (`livewire-material.scheme`), inlined as hexes; regenerate the scheme and mail follows. Without the file, the package's default scheme applies. - Write the body as Markdown; the theme styles the bare tags (`#` headings, prose, lists, tables) with M3's typescale. `` is a filled pill in `primary`; `color` also takes `secondary`, `tertiary`, `error`, `success`, `warning` and `info`. `` is a tinted container. - There is no dark mail. Never put `@media` rules, CSS variables or `color-mix()` in mail CSS: the inliner strips media queries and mail clients resolve no variables. - The package's mail header (the app name, or a logo) and message (with a replaceable footer) are opt-in: set `MATERIAL_MAIL_COMPONENTS=true`, or `php artisan vendor:publish --tag=livewire-material-mail` to copy them into `resources/views/vendor/mail`. For a logo set `livewire-material.mail.logo` to `['src' => 'https://example.com/logo.png', 'width' => 160, 'height' => 40]`: an absolute URL, with the image at twice those dimensions. - With those components, a mail can replace the footer: ```blade Your export is ready. © {{ date('Y') }} {{ config('app.name') }} · [Unsubscribe]({{ $unsubscribeUrl }}) ``` - The showcase renders a sample mail at `/material/mail`. ## Components ### `` A Material Symbol (Rounded, weight 400, grade 0), inline. Every symbol on fonts.google.com/icons exists, by Google's name with underscores. An unknown name throws. | Prop | Default | | |---|---|---| | `name` | required | `calendar_month`, `cloud_upload`, `content_copy` | | `filled` | `false` | the filled symbol — M3 uses it for active or selected | | `size` | `24` | the drawn size in px, a whole number from 8 to 256 (`16`, `18`, `20`, `32`, `40`, `48` …), written as `--md-icon-size` | | `optical` | from `size` | the cut the glyph is drawn from, `24` or `20`; anything else falls back to 24 | | `label` | `null` | names the icon for screen readers when it carries the meaning alone; otherwise it is `aria-hidden` | | `mirror-rtl` | `false` | flips a directional symbol (an arrow, a chevron) in a right-to-left document | Colour follows the text: ``. M3's optical size axis redraws a symbol so its strokes look equally heavy at every size, so **an icon drawn at 20px or smaller takes the 20 cut**; scaling the 24 cut down thins its strokes by about a sixth. `size="20"` and below pick it for you. Pass `optical="20"` yourself only when the icon's size comes from somewhere else (your own CSS setting its width and height, which outranks the package's layer). ### `` One of M3 Expressive's 35 shapes, filled in the text colour, `aria-hidden`: ``. `size` is its size in px (8 to 1024); without it your own CSS sizes it. Names: `circle`, `square`, `slanted`, `arch`, `fan`, `arrow`, `semi-circle`, `oval`, `pill`, `triangle`, `diamond`, `clam-shell`, `pentagon`, `gem`, `very-sunny`, `sunny`, `cookie-4`, `cookie-6`, `cookie-7`, `cookie-9`, `cookie-12`, `ghostish`, `clover-4`, `clover-8`, `burst`, `soft-burst`, `boom`, `soft-boom`, `flower`, `puffy`, `puffy-diamond`, `pixel-circle`, `pixel-triangle`, `bun`, `heart`. ### `` The theme decided before the first paint. Exactly once per layout, in ``, before `@vite`. No props; configured in `config/livewire-material.php`. With `theme.meta` on it also paints `` (see Theme); a layout's own theme-color meta goes before it. ### `` Label button, icon button, toggle and responsive FAB in one component. | Prop | Default | | |---|---|---| | `label` / slot | | the words; without them and with an `icon` it is an icon button | | `variant` | `text` | `filled`, `tonal`, `outlined`, `elevated`, `text` | | `color` (alias `tone`) | `primary` | `primary`, `secondary`, `tertiary`, `error`, `success`, `warning`, `info` | | `primary`, `danger`, `caution` | | shorthands: filled primary, filled error, filled warning | | `size` | `sm` | `xs` 32px, `sm` 40px, `md` 56px, `lg` 96px, `xl` 136px | | `shape` | `round` | or `square`; both square off further while pressed | | `icon`, `icon-right` | | Material Symbol names; drawn from M3's 20px cut where the glyph is 20px, and filled on a default icon button | | `width` | `default` | icon buttons only: `narrow`, `default`, `wide` | | `selected` | `null` | `true`/`false` makes it a toggle (`aria-pressed`, selected colours and shape) | | `link`, `external`, `no-wire-navigate` | | renders ``, with `wire:navigate` unless external | | `spinner` | | `true` shows the loading indicator while its `wire:click` runs; a string names the action | | `tooltip`, `tooltip-left`, `tooltip-right`, `tooltip-bottom` | | plain tooltip; also the icon button's accessible name | | `disabled`, `type`, `responsive`, `fab` | | `responsive` hides the label below `expanded`; `fab` is an extended FAB on a compact window (below `medium`), a filled button from there | ```blade ``` ### `` M3's plain tooltip, standalone around any trigger: ``. `side`: `top` (default), `bottom`, `left`, `right`. Shows on hover (fine pointers) and keyboard focus, and goes 1.5s after the pointer or the focus leaves it (M3's transient tooltip); only one is on screen at a time. It is `aria-hidden`, so the trigger has to carry the same words itself — as an icon button's `aria-label` does. Where the tip says something the trigger does not, use ``, which points the trigger at its text. Buttons and FABs take a `tooltip` prop instead. ### ``, ``, ``, `` ```blade ``` ``: `trigger` slot (its first button or link becomes the menu button, and the menu hangs on that button — a `position: fixed` trigger such as `` carries it along, and a menu with no room flips to the other side, end or both), `label`, `position` (`bottom-start` default, `bottom-end`, `top-start`, `top-end`), `vibrant`, `filter`, `sheet-at-compact`. ``: `label`, `icon`, `icon-class` (classes for the leading icon; a colour there paints it, a selected item's too, but not a disabled one's — `icon-class="sport-run"`, a class of your own), `icon-right`, `description`, `shortcut`, `link`, `external`, `selected` (makes it a `menuitemcheckbox`, ticked at its end unless it has an `icon-right`), `current` (for a menu of places: marks the page you are on with `aria-current="page"` in secondary-container, never a checked choice), `badge` (`true` for a dot, or a count, at the end of the row), `disabled`, `keep-open`, `submenu`. Choosing an item closes the menu unless `keep-open`; a second press on the menu button closes it too. An open menu stays open while the Livewire component around it renders, a `keep-open` item's own `wire:click` included. Keyboard: arrows, Home, End, a letter, Escape (focus returns to the trigger), Tab; a `disabled` item keeps its place in that order, as M3 asks, but cannot be activated. A menu longer than the window scrolls. `submenu` makes an item a menu of its own — the slot holds the nested ``s instead of a label, and they open beside it, on its end, flipping to its start where the window has no room: ```blade ``` The item says so with `aria-haspopup="menu"`, `aria-expanded` and a chevron; Right, Enter or Space open it on its first item, Left or Escape close it and come back, and on a fine pointer resting on the item opens it. Choosing anything inside closes the whole menu. Arrows stay inside the list they are in. M3 calls submenus a large-screen pattern — on a phone give the menu `sheet-at-compact`, or keep the list flat. `filter` puts a text field at the top of the list (M3's menu as a filtering surface) and narrows the items to those whose label holds what has been typed — in the browser, over the items already rendered, so nothing is fetched and every `wire:click` stays where it was. `filter="Find a person"` names the field; bare `filter` calls it "Filter". The field keeps the focus while the arrow keys, Home and End move a highlighted row and Enter chooses it (`aria-activedescendant`, as ``); a query that leaves nothing says "Nothing matches". Reach for it once a menu is long enough to hunt through; for a value bound to a property, `` is the field, not the menu. `sheet-at-compact` is M3's adaptive menu ("at compact breakpoints, consider swapping a menu for a bottom sheet"): below `medium` (600px) the trigger opens the same items in a modal ``, and from `medium` up it opens the popover. Write the items once — the slot is drawn in both: ```blade ``` On a compact window the trigger says `aria-haspopup="dialog"` (and `menu` from `medium`), with `aria-expanded` in both. In the sheet the items keep their `menuitem` roles and the menu keyboard (arrows, Home, End, a letter; Escape or Tab close it and focus returns to the trigger); choosing an item closes it, as do the scrim and a swipe down; a `submenu` opens in place under its item rather than beside it; a `filter` field sits at the top; a `keep-open` item's render keeps the sheet open. Resizing the window across 600px while it is open closes it. The sheet is teleported to the end of ``, so it covers the window from inside a sticky app bar or a toolbar, and is surface-container-low even for a `vibrant` menu. Because the items are rendered twice, do not give them an `id` or nest a Livewire component in a `sheet-at-compact` menu. Clusters: `` draws M3's line, `` M3 Expressive's grouped layout — no line, the cluster set 8px off its neighbours with its items 2px apart and its ends rounded. Reach for the divider first (M3: "on web, use dividers to separate items", and it is the only one a scrolling menu may use); reach for the gap for one or two clusters in a menu short enough not to scroll, and never vary the gap. `` takes `label` (optional) and `gap`; a labelled group without `gap` is the plain heading it always was. ### `` A row of ``s: ``. `connected` sets them 2px apart with small inner corners (a selected toggle rounds fully). Pass the `size` of the buttons inside. `shape="square"` is M3's square group and covers every button in it, so do not write `shape` on each one: a connected group's ends square to the corner its inner edges take, a standard group's buttons take the square corner scale, and a selected button still rounds — M3 has the toggle morph the other way. `selection` is M3's third configuration — `single`, `multi`, and either with `required` ("selection-required"). The group then owns `aria-pressed`: ```blade ``` Pressing a button writes its `value` (an array with `multi`) to `wire:model` or `x-model`, deselects the others in `single`, and with `required` refuses the press that would leave nothing selected; without a model it reads the buttons' own `aria-pressed` once and goes on from there. A button with no `value` is known by its label. The group manages state and shape, not colour — each button draws its selected colours from its own `:selected`, so bind both from one property as above. **Reach for `` first**: it is the component for a choice whose options are data (real radios or checkboxes, a plain form post, the browser's keyboard, segments that paint themselves). `` is for buttons you write yourself — icons, tooltips, mixed content — and never becomes a form control. ### `` A choice between a few options as a connected button group of native radios (checkboxes with `multiple`): ```blade ``` Props: `label`, `hint`, `hint-class` (classes for the hint, as on the fields; a colour there paints it), `name` (required with `x-model`), `options`, `option-value` (`id`), `option-label` (`name`), `option-icon` (`icon`), `size`, `variant` (`tonal`, `filled`, `outlined`), `shape` (`round`, `square`), `multiple`, `inline` (intrinsic width instead of sharing the row). A validation error for the bound property replaces the hint. ### `` ```blade ``` Attributes go to the leading button; the slot is the menu. `variant` (`filled` default, `tonal`, `outlined`, `elevated`), `color`, `size`, `disabled`, `spinner`, `menu-label`, `position`. ### `` `` — `size` `sm` 56px (default), `md` 80px, `lg` 96px; with `label` it is an extended FAB. The glyph is filled, as M3 requires of a FAB. `color` `primary`/`secondary`/`tertiary`, drawn in the container, or `variant="filled"`. It does not position itself: put the page's FAB in ``'s `fab` slot, which places it at the bottom-end corner with M3's margin and clear of the navigation bar and a snackbar. `link`, `external`, `type`. There is no `disabled`: M3 says to remove a FAB whose action is unavailable, so hide it instead. `data-md-fab` on the root lets a place restyle a nested FAB (a rail flattens it to elevation 0). `collapse-on-scroll` on an extended FAB with an `icon` (``) is M3's scroll behaviour: it shrinks to the FAB of its size while the window scrolls down and extends again on scroll-up or at the top of the page. The width morphs on the spatial spring and the label fades; under reduced motion it swaps outright. The label stays in the page, clipped, so the collapsed FAB keeps its accessible name. It watches the window, so it is for a FAB pinned over a scrolling page, not one inside a scrolling pane. ### ``, `` ```blade
``` ```css .new-share-menu { position: fixed; inset-inline-end: var(--md-sys-measurement-space200); inset-block-end: var(--md-sys-measurement-space200); @media (width >= 1200px) { inset-inline-end: var(--md-sys-measurement-space300); inset-block-end: var(--md-sys-measurement-space300); } } ``` Two to six items open above the FAB, which turns into a close button, rising into place as it opens and sinking back as it closes; a window too short for them scrolls the list while the FAB stays put. ``: `icon` (`add`), `label` (defaults to "Toggle menu" — the trigger has no other accessible name), `color`, `position` (`top-end` default). Give items the same `color`. Keyboard, and staying open through a Livewire render, as ``. It does not place itself: the wrapper keeps M3's margin from the window edge, 16dp, 24dp from `large`. ### `` M3 Expressive's loading indicator — a shape morphing through seven Expressive shapes as it turns — for a wait of unknown length. 48px and `primary` unless sized or coloured: `size` is its size in px, a whole number from 24 to 240 — M3's responsive range, which it asks you never to leave; scale it up on large and extra-large windows — and the container and the shape keep their ratio at every size (outside the range it is ignored, and your own CSS may size it). Colour it with your own CSS. `contained` sets it on a primary-container circle. A `progressbar` named by `label` ("Loading"); `:label="false"` makes it decorative. It rests under reduced motion. ```blade
``` `` shows a decorative one in place of its icon. ### `` The snackbar host. Once per layout, near the end of ``: `` (`position="bottom-start"` to leave the centre free). It is `@persist`ed across `wire:navigate` and shows, one at a time, every toast from the `Toasts` concern (see Toasts above) or from JavaScript: ```js materialToast('Share deleted', { type: 'success', description: null, timeout: 4000, action: { label: 'Undo', handler: () => $wire.restore() } }) ``` `type` (`success`, `error`, `warning`, `info`) picks the announcement role and draws no icon (M3 tells you to avoid one in a snackbar); `timeout: 0` keeps it until dismissed; a toast with an action or no timeout gets a close button. Hover or focus pauses the timer. A toast with an `action` never auto-dismisses (M3's rule) unless you write a `timeout` out. - `action`: `label`, plus `handler` (a function) and/or `event` (a name). Pressing it closes the snackbar, calls `handler`, then dispatches `new CustomEvent(event)` on `window`; give both and both run. Use `event` where a function cannot travel, such as a toast built from JSON. - `sticky: true` keeps a toast until it is dismissed or its action is pressed (any `timeout` is ignored), without holding the queue up: a toast dispatched meanwhile shows in its place, and the sticky one comes back once the queue is empty. One sticky toast is kept at a time; a newer one replaces it. Use it for a question that must be answered, not for news: ```js window.dispatchEvent(new CustomEvent('toast', { detail: { type: 'info', title: 'A new version is ready', sticky: true, action: { label: 'Reload', event: 'app:update' } } })) window.addEventListener('app:update', () => location.reload()) ``` - `description` is M3's second line: the container goes from 48px to 68px (`SnackbarTokens.TwoLinesContainerHeight`), and below `medium` a two-line snackbar with an action wraps the action under the text. - Escape dismisses a snackbar that holds the focus, and **Alt+G** moves the focus to a snackbar that carries an action from wherever the page had it — M3 asks the web for a documented shortcut of that kind, since a snackbar never takes the focus itself. Say so where your users read about keyboard shortcuts. - Hooks: `data-md-toast` on the host, `data-md-toast-snackbar` on the snackbar on screen (absent while nothing shows), `data-md-toast-action` on its action button and `data-md-toast-dismiss` on its close button. Target these in tests, not classes. ### `` M3 Expressive's progress indicator: linear (as wide as its container) or `circular` (40px, 48px wavy, unless your own CSS sizes it), flat or `wavy`, determinate with a `value` or indeterminate without one. | Prop | Default | | |---|---|---| | `value` | `null` | 0 to `max`, clamped; `null` is indeterminate | | `max` | `100` | | | `bind` | `null` | an Alpine expression it follows in the browser; `null`/`undefined` is indeterminate | | `circular` | `false` | circular instead of linear | | `wavy` | `false` | Expressive's wave (flat below 10% and from 95%) | | `thick` | `false` | 8px track and indicator instead of 4px | | `color` | `primary` | `primary`, `secondary`, `tertiary`, `error`, `success`, `warning`, `info`; the track is the colour's container (secondary-container for primary) | | `label` | `"Progress"` | names the `progressbar`; `:label="false"` makes it decorative | ```blade
``` A value the server changes animates after a morph (the SVG is `wire:ignore`; only the root's attributes change). Under reduced motion values jump, the wave stands still and an indeterminate indicator holds one frame. A width of your own on its `class` narrows a linear one (`inline-size: 12rem`); leave its `display` and `position` to it. ### `` - `` — M3's small badge, a dot. `` — M3's large badge, a count. Both `error` by default. `floating` pins it to the top-end corner of a `position: relative` parent: ``. A dot or count is `aria-hidden` unless it has a `label`; name the control instead ("Messages, 4 unread"). - ``, ``, ``, `` — a status label (not an M3 badge) in the colour's container, in the colour itself (`solid`, for a label that has to stand out), or a neutral `outline` edge (the role that has to be seen, not the decorative `outline-variant` dividers use). `color` (alias `tone`): `error` default, `primary`, `secondary`, `tertiary`, `success`, `warning`, `info`, `neutral`, `plain`; an unknown colour is `error`. - `color="neutral"` — neutral ink on every variant: a dot or count in on-surface-variant with surface text, `tonal` in surface-container-high with on-surface-variant text, `outline` in the outline edge with on-surface-variant text. - `color="plain"` — no background, text or border colour in any variant (shape, size and type stay), so your own CSS paints it: `` with `.sport-run-label { background-color: var(--md-sys-color-tertiary-container); color: var(--md-sys-color-on-tertiary-container); }`. Set both a background and a colour; an `outline` badge's edge takes the text colour unless you set a `border-color`. - The value is `value` or the slot; the slot renders as HTML: ` Pro`. `value` is escaped. A slot that holds only whitespace or comments is still a dot. ### `` A notice in the page, in the state's container colour with its icon: ```blade The upload failed. ``` `color` (alias `tone`): `info` (default), `success`, `warning`, `error`, `primary`, `secondary`, `tertiary`, `neutral`. `icon` overrides the state icon; `:icon="false"` removes it. Every alert is `role="status"`, whatever its colour: it is usually on the page as it renders, and an assertive region talks over the page title on load. Pass `assertive` for one put on screen in answer to something the person just did. ### `` A few lines of context around a trigger, with an optional `title` and `actions` slot: ```blade ``` Shows on hover and keyboard focus and goes 1.5s after the pointer or the focus leaves, as M3 times a plain tooltip too; `persistent` opens it on press and keeps it until a press elsewhere or Escape (use it when there are actions). The trigger is pointed at the bubble with `aria-describedby`, so its words are read out with the control. An open bubble stays open while the Livewire component around it renders, its actions' `wire:click` included. `side`: `bottom` (default), `top`, `left`, `right`. ### `` `` — a figure on a surface-container panel; the value counts up on first appearance and when it changes. The slot goes under the description (e.g. a quota's ``). Its fill is surface-container: do not repaint it with a class of your own; wrap it. ### `` "Nothing here yet": `icon` on an Expressive `shape` (`cookie-9` by default), `title`, `description` or slot, and an `actions` slot. Use it for an empty collection, not for a filter that matched nothing. The `illustration` slot draws the application's own artwork in place of the shape and icon (`icon` and `shape` are then unused). Size the artwork yourself; the slot's attributes go on the element around it, so its `style` (or a class of your own) sets the colour `currentColor` takes. Mark decorative SVG `aria-hidden="true"`. A slot holding only whitespace or comments leaves the shape and icon. ```blade ``` ### `` `variant`: `filled` (default, surface-container-highest), `elevated`, `outlined`; medium corner. Props `title`, `subtitle`, `separator`, `heading` (the title's element, `h3` by default: pass `h2` for a card straight under the page's `h1`); slots `figure` (full-bleed media), `menu` (top-end), `actions` (end-aligned). A card holds content and actions about one subject (M3); a table of many rows or a group of settings is a headed section, not a card. Choose its fill with `variant`, not a background of your own. A card or list item that opens something is a **row**: `data-md-list-row` on it and `data-md-list-open` on its one opener (the title link or a button). A press anywhere else on the row reaches the opener; its other controls keep their own presses. Never wrap a card in `
` or use a stretched link. A row answers with the state layer and one step of elevation; its corner does not move. ```blade {{ $share->name }} ``` `data-md-dragged` on the card draws M3's dragged state — elevation 8dp elevated / 6dp filled and outlined, under the 16% dragged state layer. The application sets it when its drag starts and removes it on drop; pair any drag with a single-pointer alternative (a menu with the same actions), as M3 requires. That is M3's *non-actionable card with actionable elements*: Tab walks the controls inside. For M3's *directly actionable card*, where Tab lands on the card and then moves to the next card, add `actionable` (and `role="link"` where it goes somewhere) and put `tabindex="-1"` on the opener — the card is then the tab stop, named by its `title`, and Enter or Space on it reaches the opener while the card's other actions follow it. ### ``, `` ``: `label`, `dividers`, `segmented` (M3 Expressive: separate tiles 2px apart). ``: `title` (or slot), `overline`, `description`, leading `icon` / `avatar` (image URL or initials) / `image` / `video` / `leading` slot, trailing `trailing` text / `icon-right` / `end` slot, `link` (the whole item becomes a row that opens it), `selected`, `disabled`. One-, two- and three-line heights follow from the content, and a three-line item top-aligns as M3 asks. Its icons are 24px, 20px in a `segmented` list. `video` is M3's leading media in landscape: a poster URL, or `` for a `