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
28 KiB
name, description
| name | description |
|---|---|
| material-3-design | 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 ison-surface-variant; decoration isoutline. Emphasis is never an opacity: M3 reserves 38% (/38) for disabled content and 12% (/12) for a disabled container. outlinefor a boundary that has to be perceived (a text field's edge, a target's edge — 3:1 against the surface);outline-variantfor 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-variantmay edge a chip or a button only because the content inside already carries the contrast.- A hyperlink in running text is
primary(ortertiaryfor a quieter link) and underlined: thelinkutility.
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-proseis close;max-w-[60ch]is exact). Figures that change taketabular-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-allmixes the two and is wrong. - The Standard motion scheme (
<html data-motion="standard">, configmotion.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) takesease-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-300or 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 (onsecondary-containerit ison-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-selectedplus the container, an error colour plus an icon and a message). - Targets: 48×48px minimum, 8px between targets, on every device;
touch-targetextends 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 fromexpanded:,<x-supporting-pane>puts a supporting pane (360px, beside the focus pane) fromexpanded: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-2on 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-5besidetype-label-lg,size-6beside 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 isaria-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 styleddivthat fakes one needs everything re-implemented and tested. - Landmarks: one
main, onebanner, onecontentinfoper page;nav,search,complementary,form,regionlabelled 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=""oraria-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/.