Plan step 36 (containment group): <x-drawer>'s class lists move into resources/css/components/drawer.css, keyed on data-md-drawer (with data-md-open, data-md-standard) and data-md-drawer-scrim/-sheet (data-md-side)/-head/-head-row/-heading/-title/-subtitle/-close/-body/ -actions. The modal sheet slides in on the emphasized-decelerate easing and out on emphasized-accelerate (an asymmetric CSS transition expressed by giving the open-state selector its own transition-duration/-timing-function, which the "after-change style" rule picks up); the sheet stays x-show-driven, with @starting-style + transition-behavior: allow-discrete taking the place of x-transition's enter/leave classes, matching resources/css/components/search.css's already-rewritten scrim/view. A standard sheet drops the scrim and trap from 840px and only fades in place (an in-flow box does not travel); data-md-drawer-collapsed (from the view's `wide` Alpine state) takes it out of the layout while closed there. <x-drawer pane> is removed: step 35's <x-list-detail> is M3's form of that job (the plan's decision, docs/plans/material-3-alignment.md "Decisions"). Removed the prop, its CSS classes and its tests (OverlayTest's two pane-only tests deleted, the others' pane cases dropped); pointed each mention at <x-list-detail> in the material-3 guideline, the material-3-design and livewire-material-development skills, and added a line to UPGRADE.md's 2.0.0 notes. No other living file named `<x-drawer pane>` (grepped views, tests, README, boost, workbench excluding build output); docs/plans and docs/audits keep their history unchanged. Imported from the Containment block of components.css, importing button.css and divider.css for what the view renders. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
300 lines
28 KiB
Markdown
300 lines
28 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 utility 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 the library's utility or attribute. The tokens are CSS variables (`--md-sys-color-*`, `--md-sys-typescale-*`, `--md-sys-shape-*`, `--md-sys-elevation-*`, `--md-sys-motion-*`, `--md-sys-state-*`, `--md-sys-measurement-*`, `--md-ref-typeface-*`) and the utilities read them, so a template names a utility and never a variable or 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 template names a role and nothing else — never a hex, a palette tone, an arbitrary value or an opacity — because only a role follows the theme, the contrast level and a colour profile.
|
||
|
||
### Roles
|
||
|
||
| Role | Purpose | Its `on-` pair | Utility |
|
||
| --- | --- | --- | --- |
|
||
| primary | High-emphasis fills, text and icons: the key action on a screen | on-primary | `bg-primary text-on-primary`, `text-primary` |
|
||
| primary-container | A standout fill for key components (FAB, an emphasised panel) | on-primary-container | `bg-primary-container text-on-primary-container` |
|
||
| primary-dim | A darker primary for a pressed or contrasting fill (2025 spec) | on-primary | `bg-primary-dim` |
|
||
| secondary | Less prominent fills, text and icons | on-secondary | `bg-secondary text-on-secondary` |
|
||
| secondary-container | The recessive fill: tonal buttons, selected navigation, selected chips | on-secondary-container | `bg-secondary-container text-on-secondary-container` |
|
||
| tertiary | A complementary accent, used sparingly for contrast | on-tertiary | `bg-tertiary text-on-tertiary` |
|
||
| tertiary-container | The complementary fill | on-tertiary-container | `bg-tertiary-container text-on-tertiary-container` |
|
||
| error | Urgency and errors; static, does not follow dynamic colour | on-error | `text-error`, `bg-error text-on-error` |
|
||
| error-container | An error panel | on-error-container | `bg-error-container text-on-error-container` |
|
||
| success, warning, info | This library's custom state colours, built like error on the 2025 spec, with `-container` and `on-` pairs | on-success … | `text-success`, `bg-warning-container text-on-warning-container` |
|
||
| surface | The page background | on-surface | `bg-surface text-on-surface` |
|
||
| on-surface-variant | Lower-emphasis text and icons on any surface | — | `text-on-surface-variant`, `text-body`, `text-meta` |
|
||
| outline | A boundary that must be read: a text field, a target's edge (3:1 against surface) | — | `border-outline`, `text-outline`, `text-quiet` |
|
||
| outline-variant | Decorative lines: dividers, card edges | — | `border-outline-variant`, `border-divider`, `border-structure`, `border-chrome`, `divide-divider` |
|
||
| inverse-surface | A surface that contrasts with its surroundings (the snackbar) | inverse-on-surface | `bg-inverse-surface text-inverse-on-surface` |
|
||
| inverse-primary | An action on an inverse surface (the snackbar's action) | — | `text-inverse-primary` |
|
||
| scrim | Behind a modal, at 32% | — | `bg-scrim/32` |
|
||
| shadow | The shadow colour, inside `shadow-elevation-*` | — | — |
|
||
| surface-dim, surface-bright | Add-on surfaces that keep their relative brightness in both themes | on-surface | `bg-surface-dim`, `bg-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 | — | `bg-primary-fixed text-on-primary-fixed` |
|
||
|
||
Pairing: a role's `on-` pair is the only combination whose contrast is guaranteed at every contrast level. `bg-primary text-on-primary` and `bg-secondary-container text-on-secondary-container` are right; `bg-primary-container text-on-surface` or `bg-secondary-container text-primary` are not, and break as the contrast level rises. 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 | Utility |
|
||
| --- | --- | --- |
|
||
| surface | The page | `bg-surface` |
|
||
| surface-container-lowest | The most recessed panel; an elevated card's body in dark themes | `bg-surface-container-lowest` |
|
||
| surface-container-low | An elevated card, a modal bottom or side sheet, the full-screen search view | `bg-surface-container-low` |
|
||
| surface-container | Navigation bar and rail, docked and floating toolbars, menus, the segmented list | `bg-surface-container` |
|
||
| surface-container-high | Dialogs, the search bar, date and time pickers, a rich tooltip | `bg-surface-container-high` |
|
||
| surface-container-highest | A filled card, a filled text field, a filled chip's selected state | `bg-surface-container-highest` |
|
||
|
||
### Emphasis and lines
|
||
|
||
- Default ink is `on-surface`; lower emphasis is `on-surface-variant`; decoration is `outline`. Emphasis is never an opacity: M3 reserves 38% (`/38`) for disabled content and 12% (`/12`) 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: the `link` utility.
|
||
|
||
### 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 | Utility |
|
||
| --- | --- | --- | --- |
|
||
| 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 | `shadow-elevation-1` |
|
||
| 2 | 3dp | Menus, the navigation bar, a scrolled app bar, toolbars, rich tooltips | `shadow-elevation-2` |
|
||
| 3 | 6dp | FAB and extended FAB, the FAB menu's close button, dialogs, date and time pickers, the search bar | `shadow-elevation-3` |
|
||
| 4 | 8dp | Interaction only: a level-3 element on hover or while dragged | `shadow-elevation-4` |
|
||
| 5 | 12dp | Interaction only | `shadow-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 (`bg-scrim/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 | Utility |
|
||
| --- | --- | --- |
|
||
| None | 0 | `rounded-corner-none` |
|
||
| Extra small | 4px | `rounded-corner-xs` |
|
||
| Small | 8px | `rounded-corner-sm` |
|
||
| Medium | 12px | `rounded-corner-md` |
|
||
| Large | 16px | `rounded-corner-lg` |
|
||
| Large increased | 20px | `rounded-corner-lg-increased` |
|
||
| Extra large | 28px | `rounded-corner-xl` |
|
||
| Extra large increased | 32px | `rounded-corner-xl-increased` |
|
||
| Extra extra large | 48px | `rounded-corner-xxl` |
|
||
| Full | a stadium or circle | `rounded-corner-full` |
|
||
|
||
Tailwind's `rounded-sm`, `rounded-lg`, `rounded-full`, … do not compile. The `-t`, `-s`, `-e` sides work (`rounded-t-corner-xl` for a bottom sheet).
|
||
|
||
### 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="…">`, 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 (`font-sans`, `--md-ref-typeface-brand`); an application may replace it after importing the stylesheet. Each style is one utility that sets size, line height, weight, family and tracking together; `text-sm`, `font-medium`, `leading-*` and `tracking-*` do not compile.
|
||
|
||
| Role | Style | Size / line | Weight | Utility | Use for |
|
||
| --- | --- | --- | --- | --- | --- |
|
||
| Display | large / medium / small | 57/64 · 45/52 · 36/44 | 400 | `type-display-lg` … | hero figures, one short marketing line; never running text |
|
||
| Headline | large / medium / small | 32/40 · 28/36 · 24/32 | 400 | `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 | `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 | `type-body-lg` … | paragraphs (`lg` for reading, `md` in components), supporting text (`sm`) |
|
||
| Label | large / medium / small | 14/20 · 12/16 · 11/16 | 500 | `type-label-lg` … | buttons and tabs (`lg`), chips and navigation (`md`), captions and badges (`sm`) |
|
||
|
||
- `type-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-w-prose` is close; `max-w-[60ch]` is exact). Figures that change take `tabular-nums`.
|
||
- 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 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 | Utility | For |
|
||
| --- | --- | --- | --- | --- |
|
||
| Spatial fast | 0.6 / 800 | 350ms | `ease-spatial-fast duration-(--md-sys-motion-spatial-fast-duration)` | small elements: a button's press morph, a switch, a chip |
|
||
| Spatial default | 0.8 / 380 | 500ms | `ease-spatial-default duration-(--md-sys-motion-spatial-default-duration)` | most position, size and shape changes |
|
||
| Spatial slow | 0.8 / 200 | 650ms | `ease-spatial-slow duration-(--md-sys-motion-spatial-slow-duration)` | large surfaces: a sheet, a pane, a full-screen transition |
|
||
| Effects fast | 1.0 / 3800 | 150ms | `ease-effects-fast duration-(--md-sys-motion-effects-fast-duration)` | state layers, small fades |
|
||
| Effects default | 1.0 / 1600 | 200ms | `ease-effects-default duration-(--md-sys-motion-effects-default-duration)` | most colour and opacity changes |
|
||
| Effects slow | 1.0 / 800 | 300ms | `ease-effects-slow duration-(--md-sys-motion-effects-slow-duration)` | large fades, a scrim |
|
||
|
||
- 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-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 (`ease-emphasized-decelerate`, or a spatial spring from off-screen), a permanent exit accelerates (`ease-emphasized-accelerate`), a temporary exit that can be recalled (a drawer, a sheet) takes `ease-emphasized`; exits are shorter than entrances, and larger areas move longer.
|
||
- The cubic-bezier set (`ease-standard`, `ease-emphasized`, `ease-emphasized-decelerate`, `ease-emphasized-accelerate`, `--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 `duration-300` or a keyframe 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 | Utility or hook | Also |
|
||
| --- | --- | --- | --- |
|
||
| Enabled | none | — | |
|
||
| Hover | 8% of the content colour | `state-layer` (pointer devices only) | one level of elevation on floating elements |
|
||
| Focused | 10% | `state-layer focus-ring` (keyboard focus: a 3px `secondary` ring, 2px out) | only one focused element at a time |
|
||
| Pressed | 10% | `state-layer` (`:active`) | the shape morph on buttons |
|
||
| Dragged | 16% | `state-layer` with `data-dragged` | one level of elevation |
|
||
| Disabled | content 38%, container 12%, no state layer, not focusable | `disabled:text-on-surface/38 disabled:bg-on-surface/12` | 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.
|
||
- 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; `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; they are the only responsive variants that compile, and `resources/js/breakpoints.js` (`from('expanded')`, `upTo('medium')`) gives scripts the same numbers.
|
||
|
||
| Class | Width | Variant | Navigation | Panes | Dialogs and choices | Margins |
|
||
| --- | --- | --- | --- | --- | --- | --- |
|
||
| Compact | below 600px | the default; `max-medium:` for "only here" | navigation bar; the rail opens as a modal | 1 | full-screen or basic dialog; a bottom sheet for choices | 16px |
|
||
| Medium | 600–839px | `medium:` | collapsed rail (96px) | 1, or 2 for low-density content at 50% each | basic dialog; a menu for choices | 24px |
|
||
| Expanded | 840–1199px | `expanded:` | rail, collapsed or expanded, collapsible | 2 recommended; a fixed pane 360px | basic dialog; menu | 24px |
|
||
| Large | 1200–1599px | `large:` | rail expanded | 2; a fixed pane 412px | basic dialog; menu | 24px |
|
||
| Extra-large | 1600px and up | `extra-large:` | rail expanded | 2, or 3 with a standard side sheet (at most 400px) | basic dialog; menu | 24px |
|
||
|
||
- `<x-scaffold>` implements the navigation column; `<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 (a grid of cards that gains columns per class), 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 (`ps-4`, `me-2`, `start-0`, `text-start`, `border-s`); 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; Tailwind's spacing scale is that grid, so `p-4` is `space200`.
|
||
|
||
| Token | Value | Utility |
|
||
| --- | --- | --- |
|
||
| space25 | 2px | `p-0.5`, `gap-0.5` |
|
||
| space50 | 4px | `p-1` |
|
||
| space75 | 6px | `p-1.5` |
|
||
| space100 | 8px (the base) | `p-2`, `gap-2` |
|
||
| space125 | 10px | `p-2.5` |
|
||
| space200 | 16px | `p-4` — a component's padding, compact margins |
|
||
| space300 | 24px | `p-6` — a dialog's padding, margins from `medium:` |
|
||
| space400 | 32px | `p-8` |
|
||
| space500 | 40px | `p-10` |
|
||
| space600 | 48px | `p-12` — a target |
|
||
| space700 | 56px | `p-14` |
|
||
| space800 | 64px | `p-16` |
|
||
| space900 | 72px | `p-18` |
|
||
|
||
- Padding and gaps live on the parent (`p-4 gap-2` on the container), 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="…">` draws a Material Symbol Rounded (weight 400, grade 0), outlined or `filled`, at optical size 24 or `optical="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 | `optical="20"` when drawn at 20px or less (small buttons, chips, dense lists), 24 otherwise |
|
||
|
||
- An icon beside text takes the text's size and colour (`size-5` beside `type-label-lg`, `size-6` 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, `type-*` 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`.
|
||
- 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 | `border-divider` (`outline-variant`) |
|
||
| Use a hex, `white`, `black` or an opacity for ink | it ignores theme, contrast level and profile | a role |
|
||
| Use Tailwind's breakpoints or scales | they are not M3's values | `medium:` … and `type-*`, `rounded-corner-*`, `shadow-elevation-*`, `ease-*` |
|
||
| 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/`.
|