2.0.0 took Tailwind out of the stack, but the texts Boost copies into every application still taught its utilities: the core guideline said "built on Tailwind CSS", the always-on material-3 guideline wrote every rule as `bg-primary`, `type-*`, `rounded-corner-*`, `state-layer` and `medium:`, and both skills' tables and examples did the same. None of those classes exists in 2.0's stylesheets, so an agent following the guideline wrote markup that compiled to nothing. Found moving ReStride onto 2.0: its CLAUDE.md, and SealShare's, carry these texts. The guidelines and skills now teach what 2.0 has: component and layout component props (`gap="space200"`, `hide-from="medium"`), the `md-type-*`, `md-ink-*` and interaction classes, and `--md-sys-*` tokens in the application's own CSS, with breakpoints as range media queries. UPGRADE.md §1, §2 and §4 describe the finished move instead of the in-between state, and header comments that pointed at the removed tokens/utilities.css and tailwind.css, or called a component "still Tailwind", say what is true now. BoostVocabularyTest runs DesignGuard over the code in every shipped guideline and skill (fenced Blade and CSS, and each inline class list), so a text that teaches a class the stylesheets do not define fails the suite; it finds 268 violations in the texts as 2.0.0 shipped them. The guard's own table of what it reports is exempt. BoostResourcesTest now asks the design skill for each breakpoint's prop value and media query instead of the removed `medium:` variants. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
304 lines
32 KiB
Markdown
304 lines
32 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
|
||
|
||
- Native elements before ARIA: `<button>`, `<a href>`, `<dialog>`, `<input>`, `<select>`; a styled `div` that fakes one needs everything re-implemented and tested.
|
||
- Landmarks: one `main`, one `banner`, one `contentinfo` per page; `nav`, `search`, `complementary`, `form`, `region` labelled when they repeat, never with their own role in the label ("Primary", not "Primary navigation").
|
||
- Headings: one H1 for the page, then H2–H6 in order without skipping; the level is the document's structure, the `md-type-*` class is the appearance, and they need not match.
|
||
- Names: an interactive icon, image or ambiguous button ("Save", "Learn more") has a name that says what it does, without the word "button"; decorative images are `alt=""` or `aria-hidden`; text only a screen reader needs is `md-visually-hidden`.
|
||
- Focus: DOM order is reading order; a dialog moves focus to its first meaningful control and returns it to the opener; grouped controls are one Tab stop with arrows inside; a keyboard shortcut is two keys, or a single key only while its component is focused.
|
||
- Announcements: an error is tied to its field (`aria-invalid`, `aria-describedby`) and announced; a snackbar is a polite live region that never steals focus and stays while it carries an action; a loading state has a name.
|
||
- Contrast and states: 4.5:1 text, 3:1 large text and icons and grouped controls, disabled exempt; every state has two indicators; targets 48px with 8px between; text scales to 200%; motion honours reduced motion.
|
||
|
||
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
|
||
|
||
What Google's pages say not to do, and this library follows:
|
||
|
||
| Don't | Because | Instead |
|
||
| --- | --- | --- |
|
||
| Put an icon in a snackbar | a snackbar is a short message with at most one action | `<x-toast>` types choose the announcement, not a picture |
|
||
| Disable a FAB | "if the action is unavailable, the FAB shouldn't appear" | hide it |
|
||
| Lay radio buttons in a row | a row reads as one control | a vertical group; chips or a connected button group for a horizontal choice |
|
||
| Morph a card's corners on hover | shape morphs mark a press or a selection | the state layer and one level of elevation |
|
||
| Use `outline` on a divider | dividers carry no contrast requirement and read too heavy | `<x-divider>` (`outline-variant`) |
|
||
| Use a hex, white, black or an opacity for ink | it ignores theme, contrast level and profile | a role |
|
||
| Write a utility class, or a breakpoint, radius, shadow, type size or easing of your own | nothing defines utility classes, and other values are not M3's | the layout components' props, `md-type-*` and `md-ink-*`, and `--md-sys-*` tokens at 600/840/1200/1600px in your own CSS |
|
||
| Use segmented buttons, a navigation drawer or a bottom app bar | deprecated in M3 Expressive | `<x-button-group connected>`, the expanded rail, `<x-toolbar>` |
|
||
| Truncate without a way to read the rest | an ellipsis alone is not accessible | wrap, grow the container, or a tooltip |
|
||
| Animate with a literal duration | it ignores reduced motion | the paired tokens |
|
||
|
||
## 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/`.
|