An over-engineering audit of the whole tree, applied in five reviewed batches. Behaviour stays the same except where UPGRADE.md says otherwise. PHP: the showcase and error-page stylesheets are prebuilt into resources/dist by bin/stylesheets.mjs, through Vite's own postcss-import (first occurrence kept, the order an application's build gives), instead of Stylesheets::bundle() inlining imports on every request; only the import walk DesignGuard needs stays. SchemeStylesheet::withProfiles() replaces three copies of the scheme-plus-profiles loop, material:scheme leaves spec and contrast checks to the node script that already made them, and the error page's scheme cache, the hashed view namespace, the translations path with no lang/ folder and DesignGuard's 1.x-name hints are gone. JS: the androidx shape port progress.js and both bin scripts each carried lives once in resources/js/shapes.js (the generated SVGs are unchanged); util.js holds ringIndex(), ms(), reopenGuard() and remember(), which were written out several times; listeners are released through AbortController; tooltip.js's hoverPopover() serves the rich tooltip too. CSS: every rule for an element inside the navigation rail queries `--md-navigation-rail-value` instead of repeating the seven collapsed conditions under five media branches; badge, alert, progress, slider and button read one non-inheriting colour-role table (components/color.css); the dialog chrome, the submenu's popover chrome, the chip's state layer and touch target, and the visually-hidden inputs use the shared rules they copied; foundation/tokens.css is folded into foundation.css. Views: Support\Field and Support\Link replace the error-key, bound-value and link-attribute blocks copied into the fields and link components; the timepicker period group, the menu filter and the showcase head are partials; the datepicker's steppers and entry fields are loops; component docblocks no longer restate SKILL.md. Tests and tooling: one dataset-driven ComponentStylesheetsTest replaces four per-group files, DesignGuardTest and the layout-component tests use datasets, browser tests share one ready() helper, CSS parsing lives in ComponentStylesheet alone. docs/audits and the finding IDs citing it are removed, as are pestphp/pest-plugin-laravel, the unused composer scripts and check:font; the lint job runs in the feature job, which now installs node packages so the prebuilt-stylesheet staleness test runs in CI. Feature suite 1177 passed, Chrome browser suite 299 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
294 lines
30 KiB
Markdown
294 lines
30 KiB
Markdown
---
|
||
name: material-3-design
|
||
description: Material 3 Expressive's design system as Livewire Material implements it — colour roles and surface containers, elevation, shape, type, motion, states and targets, window size classes, spacing, icons, accessibility — each M3 name beside the class, prop or token that draws it and Google's source page, for deciding how a screen should look and behave before writing it.
|
||
---
|
||
|
||
# Material 3 design
|
||
|
||
## When to use this skill
|
||
|
||
Use this skill when deciding how a screen, panel or control should look or behave — which colour, container, corner, type style, motion, breakpoint or spacing — in an application that requires `nonameweb/livewire-material`, and when reviewing a view against Material 3. The props and slots of each component are in the `livewire-material-development` skill; this one is the design language they implement. The rules an agent must always follow are in the `material-3` guideline; the tables here are what those rules compress.
|
||
|
||
Every table pairs the M3 name with what the library gives for it. The library is plain CSS with no utility classes, so that is one of three things: a component or layout component prop (`color="error"`, `<x-surface level="surface-container">`, `gap="space200"`), one of the fixed text and interaction classes (`md-type-*`, `md-ink-*`, `md-state-layer`, `md-focus-ring`, `md-touch-target`, `md-link`), or a token the application's own CSS reads with `var()`. The tokens are CSS custom properties (`--md-sys-color-*`, `--md-sys-typescale-*`, `--md-sys-shape-*`, `--md-sys-elevation-*`, `--md-sys-motion-*`, `--md-sys-state-*`, `--md-sys-measurement-*`, `--md-ref-typeface-*`), so a stylesheet names a token and never a value.
|
||
|
||
## Colour
|
||
|
||
A colour scheme is generated from one seed by Google's colour science (`php artisan material:scheme`); every role below is a slot in that scheme, light and dark, at three contrast levels. A view names a role and nothing else — never a hex, a palette tone or an opacity — because only a role follows the theme, the contrast level and a colour profile.
|
||
|
||
### Roles
|
||
|
||
| Role | Purpose | Its `on-` pair | In this library |
|
||
| --- | --- | --- | --- |
|
||
| primary | High-emphasis fills, text and icons: the key action on a screen | on-primary | `var(--md-sys-color-primary)`, `md-ink-primary`, `<x-button variant="filled">` |
|
||
| primary-container | A standout fill for key components (FAB, an emphasised panel) | on-primary-container | `var(--md-sys-color-primary-container)` |
|
||
| primary-dim | A darker primary for a pressed or contrasting fill (2025 spec) | on-primary | `var(--md-sys-color-primary-dim)` |
|
||
| secondary | Less prominent fills, text and icons | on-secondary | `var(--md-sys-color-secondary)` |
|
||
| secondary-container | The recessive fill: tonal buttons, selected navigation, selected chips | on-secondary-container | `var(--md-sys-color-secondary-container)`, `<x-button variant="tonal">` |
|
||
| tertiary | A complementary accent, used sparingly for contrast | on-tertiary | `var(--md-sys-color-tertiary)`, `color="tertiary"` |
|
||
| tertiary-container | The complementary fill | on-tertiary-container | `var(--md-sys-color-tertiary-container)` |
|
||
| error | Urgency and errors; static, does not follow dynamic colour | on-error | `md-ink-error`, `var(--md-sys-color-error)`, `color="error"` |
|
||
| error-container | An error panel | on-error-container | `var(--md-sys-color-error-container)`, `<x-alert color="error">` |
|
||
| success, warning, info | This library's custom state colours, built like error on the 2025 spec, with `-container` and `on-` pairs | on-success … | `md-ink-success`, `var(--md-sys-color-warning-container)`, `color="info"` |
|
||
| surface | The page background | on-surface | the page itself (the foundation paints it), `<x-surface level="surface">` |
|
||
| on-surface-variant | Lower-emphasis text and icons on any surface | — | `md-ink-variant` |
|
||
| outline | A boundary that must be read: a text field, a target's edge (3:1 against surface) | — | `md-ink-quiet`, `var(--md-sys-color-outline)` |
|
||
| outline-variant | Decorative lines: dividers, card edges | — | `<x-divider>`, `<x-surface outlined>`, `var(--md-sys-color-outline-variant)` |
|
||
| inverse-surface | A surface that contrasts with its surroundings (the snackbar) | inverse-on-surface | `var(--md-sys-color-inverse-surface)` with `md-ink-inverse` |
|
||
| inverse-primary | An action on an inverse surface (the snackbar's action) | — | `var(--md-sys-color-inverse-primary)` |
|
||
| scrim | Behind a modal, at 32% | — | `color-mix(in srgb, var(--md-sys-color-scrim) 32%, transparent)` |
|
||
| shadow | The shadow colour, inside every `--md-sys-elevation-*` | — | — |
|
||
| surface-dim, surface-bright | Add-on surfaces that keep their relative brightness in both themes | on-surface | `<x-surface level="surface-dim">`, `<x-surface level="surface-bright">` |
|
||
| primary-fixed, primary-fixed-dim, on-primary-fixed, on-primary-fixed-variant (and secondary, tertiary) | Add-on roles with the same tone in light and dark; for a colour that must not change with the theme; never where contrast matters | — | `var(--md-sys-color-primary-fixed)` with `var(--md-sys-color-on-primary-fixed)` |
|
||
|
||
Pairing: a role's `on-` pair is the only combination whose contrast is guaranteed at every contrast level. A `primary` fill under `on-primary` text and a `secondary-container` fill under `on-secondary-container` are right; `primary-container` under `on-surface`, or `secondary-container` under `primary`, are not, and break as the contrast level rises. A component sets its own pair; the application's CSS writes both halves (`background-color: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary)`). Google: "Pair and layer color roles only as intended … Don't mix roles improperly."
|
||
|
||
### Surface containers
|
||
|
||
A hierarchy of emphasis, not of height: the tone separates panels before any shadow does, and a region keeps its role at every breakpoint (body always `surface`, navigation always `surface-container`).
|
||
|
||
| Role | Use | In this library |
|
||
| --- | --- | --- |
|
||
| surface | The page | the page itself, `<x-surface level="surface">` |
|
||
| surface-container-lowest | The most recessed panel; an elevated card's body in dark themes | `<x-surface level="surface-container-lowest">` |
|
||
| surface-container-low | An elevated card, a modal bottom or side sheet, the full-screen search view | `<x-surface level="surface-container-low">` |
|
||
| surface-container | Navigation bar and rail, docked and floating toolbars, menus, the segmented list | `<x-surface>` (the default level) |
|
||
| surface-container-high | Dialogs, the search bar, date and time pickers, a rich tooltip | `<x-surface level="surface-container-high">` |
|
||
| surface-container-highest | A filled card, a filled text field, a filled chip's selected state | `<x-surface level="surface-container-highest">` |
|
||
|
||
In the application's CSS each is `var(--md-sys-color-surface-container-low)` and so on; the ink on every one of them is `on-surface`.
|
||
|
||
### Emphasis and lines
|
||
|
||
- Default ink is `on-surface` (`md-ink`); lower emphasis is `on-surface-variant` (`md-ink-variant`); decoration is `outline` (`md-ink-quiet`). Emphasis is never an opacity: M3 reserves 38% (`--md-sys-state-disabled-content-opacity`) for disabled content and 12% (`--md-sys-state-disabled-container-opacity`) for a disabled container.
|
||
- `outline` for a boundary that has to be perceived (a text field's edge, a target's edge — 3:1 against the surface); `outline-variant` for dividers and the edge of a card or any component holding several elements. Google: "Don't use the outline color for dividers … use outline variant instead." `outline-variant` may edge a chip or a button only because the content inside already carries the contrast.
|
||
- A hyperlink in running text is `primary` (or `tertiary` for a quieter link) **and** underlined: `md-link` with `md-ink-primary`.
|
||
|
||
### Contrast
|
||
|
||
| Level | Target | How |
|
||
| --- | --- | --- |
|
||
| Standard | Hierarchy from high- and low-contrast elements together; text 4.5:1, large text and icons 3:1, grouped non-text controls 3:1 | the default scheme |
|
||
| Medium | 3:1 minimum everywhere, without halation | `<html data-contrast="medium">` |
|
||
| High | 7:1 | `<html data-contrast="high">`, or the visitor's OS setting (`theme.contrast.default` = `system`) |
|
||
|
||
Every role changes with the level automatically; a component built from roles needs nothing else. Disabled states are exempt from contrast. A colour outside the roles (a hex, white, black) does not change and is the one thing that breaks a contrast level.
|
||
|
||
Sources: https://m3.material.io/styles/color/roles · https://m3.material.io/styles/color/system/how-the-system-works · https://m3.material.io/styles/color/advanced/apply-colors · https://m3.material.io/foundations/designing/color-contrast
|
||
|
||
## Surfaces and elevation
|
||
|
||
M3 separates surfaces by tone first; a shadow says that something floats over the content or is being interacted with. "When it comes to applying shadows, less is more."
|
||
|
||
| Level | Shadow | Rests here | In this library |
|
||
| --- | --- | --- | --- |
|
||
| 0 | none | The page, cards (filled, outlined), buttons (filled, tonal, outlined), button groups, icon buttons, lists, chips, tabs, sliders, the rail, a docked side sheet, a carousel, a full-screen dialog, a FAB inside the rail, an app bar at rest | — |
|
||
| 1 | 1dp | Elevated cards, elevated buttons and chips, modal bottom and side sheets, a banner | `box-shadow: var(--md-sys-elevation-1)` |
|
||
| 2 | 3dp | Menus, the navigation bar, a scrolled app bar, toolbars, rich tooltips | `var(--md-sys-elevation-2)` |
|
||
| 3 | 6dp | FAB and extended FAB, the FAB menu's close button, dialogs, date and time pickers, the search bar | `var(--md-sys-elevation-3)` |
|
||
| 4 | 8dp | Interaction only: a level-3 element on hover or while dragged | `var(--md-sys-elevation-4)` |
|
||
| 5 | 12dp | Interaction only | `var(--md-sys-elevation-5)` |
|
||
|
||
- Hover lifts an element one level (a FAB 3 → 4, an elevated card 1 → 2); focus and selection may too; a raised element lowers when something higher appears.
|
||
- Overlapping panels take different surface-container roles to show separation; the roles are not tied to the levels.
|
||
- A scrim (`scrim` at 32%) brings focus to a modal over a large surface; it is never a substitute for a shadow on a small floating element.
|
||
- On a dark surface a shadow is nearly invisible, so the tone does the work there.
|
||
|
||
Sources: https://m3.material.io/styles/elevation/overview · https://m3.material.io/styles/elevation/applying-elevation · https://m3.material.io/styles/elevation/tokens
|
||
|
||
## Shape
|
||
|
||
### The corner scale
|
||
|
||
| Style | Value | In this library |
|
||
| --- | --- | --- |
|
||
| None | 0 | `var(--md-sys-shape-corner-none)`, `corner="none"` |
|
||
| Extra small | 4px | `var(--md-sys-shape-corner-xs)`, `corner="xs"` |
|
||
| Small | 8px | `var(--md-sys-shape-corner-sm)`, `corner="sm"` |
|
||
| Medium | 12px | `var(--md-sys-shape-corner-md)`, `corner="md"` |
|
||
| Large | 16px | `var(--md-sys-shape-corner-lg)`, `corner="lg"` |
|
||
| Large increased | 20px | `var(--md-sys-shape-corner-lg-increased)`, `corner="lg-increased"` |
|
||
| Extra large | 28px | `var(--md-sys-shape-corner-xl)`, `corner="xl"` |
|
||
| Extra large increased | 32px | `var(--md-sys-shape-corner-xl-increased)`, `corner="xl-increased"` |
|
||
| Extra extra large | 48px | `var(--md-sys-shape-corner-xxl)`, `corner="xxl"` |
|
||
| Full | a stadium or circle | `var(--md-sys-shape-corner-full)`, `corner="full"` |
|
||
|
||
`corner` is `<x-surface>`'s prop. In the application's CSS a corner is `border-radius` on a token, and one side at a time a logical longhand (`border-start-start-radius` and `border-start-end-radius` for a bottom sheet's top); a length of your own is off the scale.
|
||
|
||
### Corner by component
|
||
|
||
| Component | Corner | Note |
|
||
| --- | --- | --- |
|
||
| Buttons, icon buttons, split button (outer), FAB menu items | full | a press morphs to `md` (xs/sm sizes), `lg` (md), `xl` (lg/xl); a selected toggle swaps round ↔ square |
|
||
| Connected button group | full outside, `sm` between segments | segments press to `xs` |
|
||
| FAB | `lg` 16 (baseline 56px), `lg-increased` 20 (medium 80px), `xl` 28 (large 96px) | extended FAB `lg` |
|
||
| Chips | `sm` 8 | an avatar in a chip `md` 12 |
|
||
| Cards | `md` 12 | no change on hover |
|
||
| Text fields | `xs` 4 (outlined: all corners; filled: top corners only) | |
|
||
| Menus, snackbar, plain tooltip | `xs` 4 | the Expressive vertical menu rounds the focused item |
|
||
| Rich tooltip | `md` 12 | |
|
||
| Dialogs | `xl` 28 | full-screen dialog `none` |
|
||
| Bottom sheet | `xl` 28 on top | |
|
||
| Side sheet | `lg` 16 on the inner side | |
|
||
| Search bar | full | search view `xl` 28 when docked, `none` full-screen |
|
||
| Date and time pickers | `xl` 28 | date cells full |
|
||
| Carousel items | `xl` 28 | |
|
||
| Navigation indicator, badges, switch, slider handle, checkbox state layer | full | checkbox box 2px, tab indicator 3px on top |
|
||
| Navigation bar, app bar, docked toolbar, tabs | none | floating toolbar full |
|
||
| Segmented list rows | `xs` inner, `lg` outer; a selected row `lg` | |
|
||
|
||
### Rules
|
||
|
||
- Optical roundness: a shape nested in a rounded container takes inner radius = outer radius − padding (48 − 14 = 34), never the container's own radius.
|
||
- Large and full corners do not belong on information-dense containers (cards, tables, text fields).
|
||
- A press squares a round shape and rounds a square one (the components carry the morph on the fast spatial spring); nothing morphs on hover.
|
||
- The 35 Expressive shapes (`<x-shape name="cookie-9">`, also the loading indicator and the standard button group's press shape) are decoration for emphasis and delight — never a carrier of meaning, never behind text-heavy content, and used sparingly.
|
||
|
||
Sources: https://m3.material.io/styles/shape/corner-radius-scale · https://m3.material.io/styles/shape/shape-morph · https://m3.material.io/styles/shape/overview-principles
|
||
|
||
## Type
|
||
|
||
The typeface is Google Sans Flex for brand and plain styles (`--md-ref-typeface-brand`, `--md-ref-typeface-plain`); an application may replace it after importing the stylesheet. Each style is one class that sets size, line height, weight, family and tracking together — or, in the application's CSS, `font: var(--md-sys-typescale-body-md)` with `letter-spacing: var(--md-sys-typescale-body-md-tracking)`. A size, weight, line height or letter spacing of your own is off the scale.
|
||
|
||
| Role | Style | Size / line | Weight | In this library | Use for |
|
||
| --- | --- | --- | --- | --- | --- |
|
||
| Display | large / medium / small | 57/64 · 45/52 · 36/44 | 400 | `md-type-display-lg` … | hero figures, one short marketing line; never running text |
|
||
| Headline | large / medium / small | 32/40 · 28/36 · 24/32 | 400 | `md-type-headline-lg` … | page titles, section titles, a dialog's headline (`headline-sm`) |
|
||
| Title | large / medium / small | 22/28 · 16/24 · 14/20 | 400 / 500 / 500 | `md-type-title-lg` … | app bar title (`lg`), card and list-section titles (`md`), dense headers (`sm`) |
|
||
| Body | large / medium / small | 16/24 · 14/20 · 12/16 | 400 | `md-type-body-lg` … | paragraphs (`lg` for reading, `md` in components), supporting text (`sm`) |
|
||
| Label | large / medium / small | 14/20 · 12/16 · 11/16 | 500 | `md-type-label-lg` … | buttons and tabs (`lg`), chips and navigation (`md`), captions and badges (`sm`) |
|
||
|
||
- `md-type-emphasized-*` (`--md-sys-typescale-emphasized-*`) is the same size and line height one weight step heavier (400 → 500, 500 → 700), fully rounded in Google Sans Flex, with its own tracking. M3 uses it deliberately, never by default: a selected list or menu item, a button's label on a primary action, an extended FAB, a badge, a headline given editorial weight.
|
||
- Tracking follows Compose's `TypeScaleTokens`: display-large −0.2, title-medium 0.2, title-small 0.1, body-large 0.5, body-medium 0.2, body-small 0.4, label-large 0.1, label-medium and small 0.5 (sp; rem = sp/16); the emphasized set tightens a few (display-large 0, title-medium 0.15, body-large 0.15, body-medium 0.25).
|
||
- Line length 40–60 characters (`max-inline-size: 60ch` in the application's CSS). Figures that change take `md-tabular`.
|
||
- Text must scale to 200%: containers grow, side-by-side controls stack, padding stays; components without text (progress, checkboxes) do not scale. Truncate to an ellipsis (`md-truncate`) only when the full text is one tooltip or link away.
|
||
- When customising, change the typeface or tracking, never the sizes: component layout depends on them.
|
||
|
||
Sources: https://m3.material.io/styles/typography/type-scale-tokens · https://m3.material.io/styles/typography/applying-type · https://m3.material.io/styles/typography/fonts · https://m3.material.io/foundations/writing/text-resizing · https://m3.material.io/foundations/writing/text-truncation
|
||
|
||
## Motion
|
||
|
||
M3 Expressive moves on physics: every transition is a spring, and the library samples each spring into a CSS `linear()` easing paired with a duration. Use the pair together, or the curve is stretched over the wrong time.
|
||
|
||
| Spring | Damping / stiffness | Duration | In this library | For |
|
||
| --- | --- | --- | --- | --- |
|
||
| Spatial fast | 0.6 / 800 | 350ms | `var(--md-sys-motion-spatial-fast-duration) var(--md-sys-motion-spatial-fast)` | small elements: a button's press morph, a switch, a chip |
|
||
| Spatial default | 0.8 / 380 | 500ms | `var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-spatial-default)` | most position, size and shape changes |
|
||
| Spatial slow | 0.8 / 200 | 650ms | `var(--md-sys-motion-spatial-slow-duration) var(--md-sys-motion-spatial-slow)` | large surfaces: a sheet, a pane, a full-screen transition |
|
||
| Effects fast | 1.0 / 3800 | 150ms | `var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast)` | state layers, small fades |
|
||
| Effects default | 1.0 / 1600 | 200ms | `var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-effects-default)` | most colour and opacity changes |
|
||
| Effects slow | 1.0 / 800 | 300ms | `var(--md-sys-motion-effects-slow-duration) var(--md-sys-motion-effects-slow)` | large fades, a scrim |
|
||
|
||
A transition names the property, then the pair: `transition: transform var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-spatial-default), opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast)`.
|
||
|
||
- Spatial springs are underdamped and overshoot — that bounce is what reads as Expressive — so they carry only position, size and shape. Effects springs are critically damped and carry colour and opacity, which must never overshoot. A transition on `all` mixes the two and is wrong.
|
||
- The Standard motion scheme (`<html data-motion="standard">`, config `motion.scheme`) swaps the spatial springs for stiffer ones with almost no bounce (0.9 / 1400, 700, 300; 350, 500, 750ms) for utilitarian products; effects are shared.
|
||
- Direction: something entering decelerates (`--md-sys-motion-easing-emphasized-decelerate`, or a spatial spring from off-screen), a permanent exit accelerates (`--md-sys-motion-easing-emphasized-accelerate`), a temporary exit that can be recalled (a drawer, a sheet) takes `--md-sys-motion-easing-emphasized`; exits are shorter than entrances, and larger areas move longer.
|
||
- The cubic-bezier set (`--md-sys-motion-easing-standard`, `-emphasized`, `-emphasized-decelerate`, `-emphasized-accelerate`, with `--md-sys-motion-duration-short|medium|long`) is for the few transitions whose duration is fixed from outside: a view transition, an animated scroll.
|
||
- Reduced motion zeroes every duration token, so anything animated through them turns instant; a literal `300ms`, or a keyframe animation with its own timing, ignores the visitor's setting and is a bug. Container transforms, parallax and expansions are removed, not slowed.
|
||
|
||
Sources: https://m3.material.io/styles/motion/overview · https://m3.material.io/styles/motion/overview/specs · https://m3.material.io/styles/motion/easing-and-duration/tokens-specs · https://m3.material.io/styles/motion/transitions/transition-patterns
|
||
|
||
## States and targets
|
||
|
||
| State | Layer | Class or hook | Also |
|
||
| --- | --- | --- | --- |
|
||
| Enabled | none | — | |
|
||
| Hover | 8% of the content colour | `md-state-layer` (pointer devices only) | one level of elevation on floating elements |
|
||
| Focused | 10% | `md-state-layer md-focus-ring` (keyboard focus: a 3px `secondary` ring, 2px out) | only one focused element at a time |
|
||
| Pressed | 10% | `md-state-layer` (`:active`) | the shape morph on buttons |
|
||
| Dragged | 16% | `md-state-layer` with `data-md-dragged` | one level of elevation |
|
||
| Disabled | content 38%, container 12%, no state layer, not focusable | `color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent)`, and the container likewise with `--md-sys-state-disabled-container-opacity` | exempt from contrast; a FAB is hidden rather than disabled |
|
||
| Selected | the `secondary-container` pair, a filled icon, the emphasized style | component props (`selected`, `aria-selected`, `aria-pressed`) | combines with hover, focus and press |
|
||
|
||
- The state layer takes the content's `on-` colour (on `secondary-container` it is `on-secondary-container`), is 40px on a 48px target, and only one shows at a time. `md-state-layer` draws it in `currentColor` as a `::before`, so the element becomes `position: relative`.
|
||
- Every state shows two indicators, so a colour change alone is never a state: add a shape, an outline, an icon, a weight or a word (`aria-selected` plus the container, an error colour plus an icon and a message).
|
||
- Targets: 48×48px minimum, 8px between targets, on every device; `md-touch-target` extends a smaller drawing to 48px. Density is an opt-in prop (`dense`) that steps padding by 4px and never applies to menus, snackbars, dialogs or settings controls, and never takes a target below 48px.
|
||
- Keyboard: Tab and Shift+Tab between components in DOM order, arrows within a component (menu, tabs, grid, radio group), Enter and Space activate, Escape dismisses; a dialog moves focus in on open and back to its opener on close.
|
||
|
||
Sources: https://m3.material.io/foundations/interaction/states/state-layers · https://m3.material.io/foundations/interaction/states/applying-states · https://m3.material.io/foundations/designing/structure · https://m3.material.io/foundations/layout/grids-spacing/density
|
||
|
||
## Layout and breakpoints
|
||
|
||
Layout keys on the width of the window, in M3's five window size classes and only those. A layout component names the class in a prop (`hide-below`, `hide-from`, `stack-below`, `<x-grid>`'s `columns` map); the application's CSS writes the width as a range media query; a script asks `resources/js/breakpoints.js` (`from('expanded')`, `upTo('medium')`) for the same numbers.
|
||
|
||
| Class | Width | Prop value · CSS | Navigation | Panes | Dialogs and choices | Margins |
|
||
| --- | --- | --- | --- | --- | --- | --- |
|
||
| Compact | below 600px | the default; `hide-from="medium"` for "only here" · `@media (width < 600px)` | navigation bar; the rail opens as a modal | 1 | full-screen or basic dialog; a bottom sheet for choices | 16px |
|
||
| Medium | 600–839px | `medium` · `@media (width >= 600px)` | collapsed rail (96px) | 1, or 2 for low-density content at 50% each | basic dialog; a menu for choices | 24px |
|
||
| Expanded | 840–1199px | `expanded` · `@media (width >= 840px)` | rail, collapsed or expanded, collapsible | 2 recommended; a fixed pane 360px | basic dialog; menu | 24px |
|
||
| Large | 1200–1599px | `large` · `@media (width >= 1200px)` | rail expanded | 2; a fixed pane 412px | basic dialog; menu | 24px |
|
||
| Extra-large | 1600px and up | `extra-large` · `@media (width >= 1600px)` | rail expanded | 2, or 3 with a standard side sheet (at most 400px) | basic dialog; menu | 24px |
|
||
|
||
- `<x-scaffold>` implements the navigation column; `<x-pane>` is a content region with the margins above; `<x-list-detail>` is the second pane of a list-detail layout from expanded, `<x-supporting-pane>` puts a supporting pane (360px, beside the focus pane) from expanded and below it before that. Moving up a class, ask what to reveal, divide into panes, resize, reposition or swap — never swap a component for one that does not do the same job.
|
||
- Scaffold: bars (app bar at the top, navigation bar at the bottom: 3–5 destinations), rails (the navigation rail, toolbars, the FAB, on the leading edge), panes (all content), around a safety region that stays clear of the device's own chrome (`--material-safe-top|bottom|left|right`).
|
||
- Canonical layouts: feed (`<x-feed>`, a grid of cards that gains columns as the room grows), list-detail (one pane on compact, two from expanded; a back button only in single-pane mode, a selected row only in two-pane mode), supporting pane (two thirds focus, one third support).
|
||
- Bidirectionality: write logical properties (`padding-inline-start`, `margin-inline-end`, `inset-inline-start`, `border-inline-start`, `md-text-start`); `<x-row>` runs in the inline direction and mirrors by itself; leading and trailing icons swap, directional icons (back, send) mirror, the rail moves to the right; charts, media controls, clocks and Hebrew progress bars stay left-to-right.
|
||
|
||
Sources: https://m3.material.io/foundations/layout/breakpoints/overview · https://m3.material.io/foundations/layout/breakpoints/compact (medium, expanded, large-extra-large) · https://m3.material.io/foundations/layout/scaffold/overview · https://m3.material.io/foundations/layout/canonical-examples/overview · https://m3.material.io/foundations/layout/bidirectionality-rtl
|
||
|
||
## Spacing
|
||
|
||
M3's spacing tokens are multiples of an 8px base on a 4px grid. A layout component takes the token's name (`gap="space200"`, `<x-surface padding="space300">`); the application's CSS reads it (`var(--md-sys-measurement-space200)`).
|
||
|
||
| Token | Value | In this library |
|
||
| --- | --- | --- |
|
||
| space25 | 2px | `space25` |
|
||
| space50 | 4px | `space50` |
|
||
| space75 | 6px | `space75` |
|
||
| space100 | 8px (the base) | `space100` |
|
||
| space125 | 10px | `space125` |
|
||
| space200 | 16px | `space200` — a component's padding, compact margins |
|
||
| space300 | 24px | `space300` — a dialog's padding, margins from medium |
|
||
| space400 | 32px | `space400` |
|
||
| space500 | 40px | `space500` |
|
||
| space600 | 48px | `space600` — a target |
|
||
| space700 | 56px | `space700` |
|
||
| space800 | 64px | `space800` |
|
||
| space900 | 72px | `space900` |
|
||
|
||
- Padding and gaps live on the parent (`<x-surface padding="space200">` around `<x-stack gap="space100">`), never as margins on children; a margin is for space beyond a container's padding or between layout regions.
|
||
- Spacing does not scale with text: at 200% text size the same padding and gaps stay.
|
||
- Name a gap by what it separates when a component has several (icon–label 8px, label–supporting text 4px).
|
||
|
||
Sources: https://m3.material.io/styles/spacing/overview · https://m3.material.io/styles/spacing/tokens · https://m3.material.io/styles/spacing/applying-spacing
|
||
|
||
## Icons
|
||
|
||
`<x-icon name="lock">` draws a Material Symbol Rounded (weight 400, grade 0), outlined or `filled`, at optical size 24 or 20.
|
||
|
||
| Axis | Values | In this library |
|
||
| --- | --- | --- |
|
||
| Fill | 0 outlined, 1 filled | `filled` — active, selected or on state (a selected navigation item, a FAB's icon, a checked filter chip) |
|
||
| Weight | 100–700; never below 200 at 24px | 400 for every icon; one weight per group |
|
||
| Grade | −25 on dark backgrounds, 0 otherwise, positive for emphasis | 0 |
|
||
| Optical size | 20 dense, 24 standard, 40–48 with display type | `size="20"` and below pick the 20 cut (small buttons, chips, dense lists); `optical="20"` for an icon sized by the application's own CSS |
|
||
|
||
- An icon beside text takes the text's size and colour (`size="20"` beside `md-type-label-lg`, 24 beside body) and the same optical weight; its baseline sits about 11.5% of the text size below the text's.
|
||
- Icons stay flat and forward-facing, on the pixel grid, inside their 20px live area of the 24px canvas.
|
||
- An icon-only control has an accessible name (`aria-label`, or a tooltip that names it); a decorative icon is `aria-hidden`; a complex icon drawn below 20px needs a label beside it.
|
||
|
||
Sources: https://m3.material.io/styles/icons/overview · https://m3.material.io/styles/icons/designing-icons · https://m3.material.io/styles/icons/applying-icons
|
||
|
||
## Accessibility
|
||
|
||
The guideline's own Accessibility line has the rule; beyond it: every repeated landmark —
|
||
`search`, `complementary`, `form`, `region`, not just `nav` — is labelled the same way; an
|
||
ambiguous button ("Save", "Learn more") needs a name that says what it does, not just what kind
|
||
of control it is; DOM order is reading order, a dialog returns focus to its opener, and a group of
|
||
related controls is one Tab stop with the arrows moving inside it; an invalid field also carries
|
||
`aria-invalid`, and a loading state has a name too.
|
||
|
||
Sources: https://m3.material.io/foundations/overview/principles · https://m3.material.io/foundations/designing/structure · https://m3.material.io/foundations/designing/flow · https://m3.material.io/foundations/designing/elements · https://m3.material.io/foundations/overview/assistive-technology
|
||
|
||
## Don'ts
|
||
|
||
The guideline's Don'ts, Type and Motion bullets name them; where they name no replacement — a vertical
|
||
group or chips for radios in a row, `<x-divider>` for the outline case, the `md-type-*`/`md-ink-*`
|
||
classes and `--md-sys-*` tokens for the utility-class case, wrap/grow/a tooltip instead of a bare
|
||
ellipsis, the paired motion tokens instead of a literal duration — the components and layout
|
||
sections above have it.
|
||
|
||
## Attribution
|
||
|
||
The rules, tables and wording here are Google's, condensed from the Material Design 3 documentation at https://m3.material.io (Foundations, Styles and Components), which Google publishes under the Creative Commons Attribution 4.0 License except as otherwise noted; the numeric token values are from the Android Open Source Project's Material 3 token files in androidx Compose (Apache License 2.0). Copyright Google LLC; Copyright The Android Open Source Project. The library's `NOTICE` records the same. Dates and page names are those of the site as read on 2026-09-13; the full extracted references, with every source page, are kept in the package repository under `docs/reference/m3/`.
|