@expressivecss/expressive 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1313 -1
- package/README.md +76 -30
- package/dist/css/expressive.css +4896 -1777
- package/dist/css/expressive.css.map +1 -1
- package/dist/css/expressive.min.css +1 -1
- package/dist/css/expressive.min.css.map +1 -1
- package/dist/fonts/LICENSE-material-symbols +93 -0
- package/dist/fonts/LICENSE-noto-sans +93 -0
- package/dist/fonts/LICENSE-roboto +93 -0
- package/dist/fonts/material-symbols-outlined.woff2 +0 -0
- package/dist/fonts/material-symbols-rounded.woff2 +0 -0
- package/dist/fonts/material-symbols-sharp.woff2 +0 -0
- package/dist/fonts/noto-sans-latin-400.woff2 +0 -0
- package/dist/fonts/noto-sans-latin-500.woff2 +0 -0
- package/dist/fonts/roboto-latin-400.woff2 +0 -0
- package/dist/fonts/roboto-latin-500.woff2 +0 -0
- package/dist/js/expressive.cjs +1673 -1338
- package/dist/js/expressive.cjs.map +4 -4
- package/dist/js/expressive.js +1673 -1338
- package/dist/js/expressive.js.map +4 -4
- package/dist/js/expressive.min.js +13 -13
- package/dist/js/expressive.min.js.map +4 -4
- package/dist/js/expressive.mjs +1673 -1338
- package/dist/js/expressive.mjs.map +4 -4
- package/dist/types/behaviors/bottomSheets.d.ts +19 -11
- package/dist/types/behaviors/sheetDrag.d.ts +40 -0
- package/dist/types/behaviors/sideSheets.d.ts +2 -9
- package/dist/types/components/appBar.d.ts +26 -0
- package/dist/types/components/autocomplete.d.ts +15 -3
- package/dist/types/components/buttonGroup.d.ts +39 -0
- package/dist/types/components/buttons.d.ts +5 -1
- package/dist/types/components/cards.d.ts +11 -13
- package/dist/types/components/carousel.d.ts +87 -53
- package/dist/types/components/chips.d.ts +21 -1
- package/dist/types/components/expandingCard.d.ts +31 -0
- package/dist/types/components/index.d.ts +23 -20
- package/dist/types/components/lightbox.d.ts +1 -0
- package/dist/types/components/menu.d.ts +11 -1
- package/dist/types/components/{sidenav.d.ts → navigationDrawer.d.ts} +16 -16
- package/dist/types/components/registry.d.ts +18 -8
- package/dist/types/components/slider.d.ts +26 -85
- package/dist/types/components/snackbar.d.ts +20 -5
- package/dist/types/components/tabs.d.ts +12 -0
- package/dist/types/components/timepicker.d.ts +12 -10
- package/dist/types/components/tooltip.d.ts +1 -0
- package/dist/types/core/utils.d.ts +64 -53
- package/dist/types/index.d.ts +8 -9
- package/package.json +39 -3
- package/src/sass/README.md +61 -11
- package/src/sass/abstracts/_breakpoints.scss +17 -19
- package/src/sass/abstracts/_mixins.scss +155 -21
- package/src/sass/abstracts/_variables.scss +69 -5
- package/src/sass/base/_fonts.scss +63 -0
- package/src/sass/base/_global.scss +6 -24
- package/src/sass/base/_grid.scss +15 -12
- package/src/sass/base/_normalize.scss +34 -38
- package/src/sass/base/_typography.scss +4 -18
- package/src/sass/components/_badges.scss +3 -5
- package/src/sass/components/_banners.scss +298 -0
- package/src/sass/components/_bottom-app-bar.scss +180 -0
- package/src/sass/components/_bottom-sheet.scss +12 -4
- package/src/sass/components/_button-groups.scss +358 -0
- package/src/sass/components/_buttons.scss +245 -92
- package/src/sass/components/_cards.scss +514 -78
- package/src/sass/components/_carousel.scss +562 -68
- package/src/sass/components/_chips.scss +64 -5
- package/src/sass/components/_datepicker.scss +24 -24
- package/src/sass/components/_dialog.scss +17 -11
- package/src/sass/components/_drag-handle.scss +130 -0
- package/src/sass/components/_expanding-card.scss +226 -0
- package/src/sass/components/_fab-menu.scss +232 -0
- package/src/sass/components/_floating-sheet.scss +86 -0
- package/src/sass/components/_icon-buttons.scss +172 -0
- package/src/sass/components/_icons-material-design.scss +22 -10
- package/src/sass/components/_index.scss +22 -6
- package/src/sass/components/_list.scss +16 -13
- package/src/sass/components/_loading-indicator.scss +106 -0
- package/src/sass/components/_menu.scss +335 -61
- package/src/sass/components/_navbar.scss +245 -45
- package/src/sass/components/_navigation-bar.scss +22 -11
- package/src/sass/components/{_sidenav.scss → _navigation-drawer.scss} +123 -33
- package/src/sass/components/_navigation-rail.scss +31 -23
- package/src/sass/components/_page-footer.scss +19 -16
- package/src/sass/components/_pagination.scss +24 -6
- package/src/sass/components/_panes.scss +52 -11
- package/src/sass/components/{_preloader.scss → _progress.scss} +11 -3
- package/src/sass/components/_scrim.scss +37 -0
- package/src/sass/components/_search.scss +259 -0
- package/src/sass/components/_segmented-buttons.scss +188 -0
- package/src/sass/components/_side-sheet.scss +2 -2
- package/src/sass/components/_snackbar.scss +20 -15
- package/src/sass/components/_split-button.scss +196 -0
- package/src/sass/components/_tabs.scss +7 -8
- package/src/sass/components/_timepicker.scss +67 -58
- package/src/sass/components/_toolbar.scss +206 -80
- package/src/sass/components/_transitions.scss +8 -1
- package/src/sass/components/forms/_checkboxes.scss +49 -12
- package/src/sass/components/forms/_fieldset.scss +4 -4
- package/src/sass/components/forms/_file-input.scss +8 -1
- package/src/sass/components/forms/_index.scss +1 -1
- package/src/sass/components/forms/_input-fields.scss +50 -44
- package/src/sass/components/forms/_radio-buttons.scss +42 -10
- package/src/sass/components/forms/_select.scss +38 -1
- package/src/sass/components/forms/{_range.scss → _slider.scss} +8 -9
- package/src/sass/components/forms/_switches.scss +6 -7
- package/src/sass/expressive.scss +5 -0
- package/src/sass/tokens/_index.scss +4 -1
- package/src/sass/tokens/_reference.scss +70 -61
- package/src/sass/tokens/_state.scss +24 -0
- package/src/sass/tokens/_theme.scss +6 -3
- package/src/sass/tokens/_vibrant.scss +76 -0
- package/src/sass/utilities/_colors.scss +14 -1
- package/src/sass/utilities/_helpers.scss +5 -5
- package/src/sass/utilities/_visibility.scss +48 -26
- package/dist/types/behaviors/waves.d.ts +0 -15
- package/dist/types/components/parallax.d.ts +0 -16
- package/dist/types/components/range.d.ts +0 -43
- package/dist/types/core/bounding.d.ts +0 -6
- package/dist/types/core/edges.d.ts +0 -6
- package/src/sass/components/_parallax.scss +0 -104
- package/src/sass/components/_pulse.scss +0 -37
- package/src/sass/components/_slider.scss +0 -129
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,1316 @@ the format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.9.0] - 2026-09-08
|
|
9
|
+
|
|
10
|
+
### Migration from 0.8.0
|
|
11
|
+
|
|
12
|
+
- Replace card-reveal `.activator` headings and separate close controls with a
|
|
13
|
+
native `<button type="button" class="card-reveal-trigger" aria-controls="details">`.
|
|
14
|
+
Its closest card must contain exactly one direct reveal panel with the matching
|
|
15
|
+
`id="details"`. Use unique IDs for each card and give every trigger an accessible
|
|
16
|
+
name. The runtime owns `aria-expanded`; legacy triggers no longer toggle panels.
|
|
17
|
+
- Use `<div class="actions">` for card command groups instead of `<nav>`.
|
|
18
|
+
- Review layouts that depend on medium or large app-bar heights and typography;
|
|
19
|
+
their flexible-bar sizes now follow the mappings listed below.
|
|
20
|
+
- Include `dist/fonts/` beside `dist/css/` when copying the compiled distribution.
|
|
21
|
+
Sass consumers that load their own fonts can set `$expressive-include-fonts: false`.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- Community contribution and security policies, required CI, independently versioned MCP releases, and stable-release documentation deployment. Tracks [#134](https://github.com/BaezFJ/ExpressiveCSS/issues/134).
|
|
26
|
+
- **Self-hosted fonts.** The compiled stylesheet ships `@font-face` rules and `dist/fonts/` holds the woff2 files the tokens name: variable Material Symbols (outlined, rounded, sharp), Latin Roboto 400/500, and Latin Noto Sans 400/500. Keep `dist/fonts/` next to `dist/css/`. Sass consumers can set `$expressive-font-path` or `$expressive-include-fonts: false`.
|
|
27
|
+
- **M3 Expressive Button Groups.** Standard groups now redistribute a pressed item's 15% width growth from adjacent items; connected groups support native-button single and multiple selection through `data-selection` and `aria-pressed`. All five sizes support common and icon buttons, explicit child-size overrides, fast-spatial shape motion, 48dp minimum targets, selected color/shape/icon cues, and optional required selection.
|
|
28
|
+
- **M3 Expressive app bar.** `.medium` and `.large` are the flexible bars: 112dp / `headline-medium` and 120dp / `display-small`. A subtitle is `<hgroup>` plus a `<p>` (136dp / 152dp). Titles wrap to two lines. `center` works on all three sizes. The search app bar is a `<search class="search-bar">` in the header. `AppBar` collapses the flexible bars on scroll and opens the related search view when the search field is selected.
|
|
29
|
+
- **ExpressiveCSS design and review skill.** The workflow now uses explicit Implement, Refine, Redesign, Critique, and Audit modes; turns product and brand decisions into Material 3 Expressive components and tokens; covers every reachable state and relevant localization or offline behavior; and closes with responsive browser evidence, an independent finish review when delegation is available, and a non-numeric review matrix with pass, intentional-adaptation, fail, and not-applicable outcomes.
|
|
30
|
+
- **Deterministic skill review evidence.** Critique, Audit, and component-contract checks now use stable criterion IDs, falsifiable claims, criterion-owned evidence rules, a fixture-owned state and responsive-boundary ledger, explicit `Blocked` handling, and exact matched captures collected before Refine or Redesign edits.
|
|
31
|
+
- **Versioned skill contracts and component decisions.** Generated guides now carry framework version, stable component-rule IDs, and source-hash provenance across every contract input. A portable resolver requires a direct dependency, selects one active lockfile, records structured conflicts, and fails closed on ambiguous, malformed, symbolic-linked, or unreadable npm, pnpm, or Yarn metadata. Contract equality follows SemVer precedence, so build metadata does not create a false mismatch. One structured catalogue owns guide extraction and selection guidance for all 46 components.
|
|
32
|
+
- **Behavioral skill evaluations.** Root-only replay cases count exact-path guide reads from tool traces and fail closed on missing, duplicate, unknown, or unevaluated critical invariant IDs. Mutation cases verify no-edit modes, required reads, evidence and event order, fixture-owned states, matched captures, older-version handling, token-based branding, dynamic ARIA, runtime ownership, and scoped MCP claims. Root-skill, case, and replay reads are descriptor-bounded; nested keys and values are capped; matched-capture chronology is bound to the first trusted edit. The runner rejects executable regular-expression assertions. Reports and adapter failures redact credential-shaped keys and values, authorization headers, credential-bearing URLs, and local paths before storage or display.
|
|
33
|
+
- **Bounded MCP evidence.** MCP setup, rule, syntax, and quality steps work independently and report checks performed, evidence sources, inspected and uninspected files, unchecked areas, coverage, blocked checks, and contract compatibility. Static checks detect retired markup, duplicate initialization, missing teardown evidence tied to the initialized variable, raw authored colors, and semantics violations. MCP guide content comes only from its synchronized package data and is checked against the bundled contract; local framework-source contracts affect source provenance but cannot replace package guidance or diverge from its version and source hash. Installed and locked versions outside their direct manifest range are blocked. Missing inputs, failed commands, timeouts, stale or divergent provenance, unresolved contracts, aggregate file limits, and partial static scans block instead of producing a broad pass. Command execution requires an operator allowlist, uses a credential-free environment, and has bounded input and runtime limits. A command timeout still force-kills the process group after SIGTERM.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- Menu cancels superseded transition and deferred event callbacks on reversal and destruction, preserving visibility, focus and end-callback ownership.
|
|
38
|
+
- Menu installs keyboard handlers before moving initial focus, so Escape works immediately after a zero-duration opening.
|
|
39
|
+
- Filled enhanced selects reserve label space so enlarged and multiline RTL/LTR labels do not overlap the selected value.
|
|
40
|
+
|
|
41
|
+
- Scale transitions now honor reduced-motion preferences, including changes during motion.
|
|
42
|
+
- Expanding-card close cleanup follows the CSS container transition instead of a fixed timer. Reopening, cancellation, native close, and teardown cannot leave stale cleanup closing a new dialog or moving focus.
|
|
43
|
+
|
|
44
|
+
- Tooltip teardown now cancels pending entry, exit, and animation callbacks.
|
|
45
|
+
- FormSelect retains ownership of its generated Menu during repeated AutoInit, preventing a retained Menu registry entry.
|
|
46
|
+
|
|
47
|
+
- **Card reveal accessibility.** Reveal panels now use a native `.card-reveal-trigger` button with a verified `aria-controls` relationship, runtime-owned `aria-expanded`, `inert` closed content, Escape handling, and focus return. AutoInit skips incomplete or disabled-only disclosures, and explicit initialization no longer hides a panel when it lacks a usable enabled `type="button"` trigger or exactly one identified direct panel. Closed-panel CSS is activated only after successful runtime setup, so rejected disclosures remain visible without JavaScript-managed state. This is a markup change: legacy `.activator` headings and separate heading-based close controls no longer toggle the panel.
|
|
48
|
+
- **App bar search dialog.** Closing a full-screen search view no longer reopens it when focus returns to the field.
|
|
49
|
+
- **Badge hosts.** Sibling badges on the stacked navigation bar and collapsed rail use logical inset and flip in RTL. Tabs nest a badge in the destination icon.
|
|
50
|
+
- **Card action semantics.** Material 3 card anatomy now uses `<div class="actions">` for command buttons instead of creating a false `<nav>` landmark.
|
|
51
|
+
- **Card layout and state parity.** Elevated, filled, and outlined cards now keep their Material variant elevations through hover, press, drag, and disabled states; dragged and picked-up elevation plus the 16% state layer take precedence over retained hover or active pointer state. Horizontal cards, including directly actionable cards and fixed-size helpers, keep intrinsic or specified heights when expanded and stack below 600px with fixed heights reset to content. Reveal content is no longer capped in browsers without intrinsic-size interpolation.
|
|
52
|
+
- **Nested card disclosure isolation.** Reveal triggers and Escape handling now belong to their closest card, so nested cards—including a complete card inside an outer reveal panel—retain their own disclosure ownership and cannot initialize, open, or close the outer card.
|
|
53
|
+
|
|
54
|
+
## [0.8.0] - 2026-08-27
|
|
55
|
+
|
|
56
|
+
Completes the HTML semantics sweep. Component markup is corrected so that
|
|
57
|
+
the element states what a component is, with the ARIA that element choice
|
|
58
|
+
implies. Scope is deliberately bounded: keyboard interaction, focus management
|
|
59
|
+
and colour contrast are **not** part of it.
|
|
60
|
+
|
|
61
|
+
The sweep is breaking, and lands in 0.8.0. Legacy markup covered by the
|
|
62
|
+
semantics sweep keeps rendering — nothing warns at runtime — so each
|
|
63
|
+
**Migration** entry below is the whole story for that component.
|
|
64
|
+
|
|
65
|
+
### Added
|
|
66
|
+
|
|
67
|
+
- **Split button** (`div.split-button`, the five button sizes), one of M3
|
|
68
|
+
Expressive's headline components. One action the user will take most of the
|
|
69
|
+
time, next to a menu of the ones they might take instead: a leading button
|
|
70
|
+
and a trailing `.menu-trigger`, 2dp apart, round on the outside, square-ish
|
|
71
|
+
on the seam between them.
|
|
72
|
+
|
|
73
|
+
M3 states this as **five token families**
|
|
74
|
+
(`md.comp.split-button.{xsmall…xlarge}`, DSP 34.0.21). They collapse to one
|
|
75
|
+
family here the way the button's own ladder does - the size class rewrites
|
|
76
|
+
the tokens and the two halves map them onto the button tokens once. The
|
|
77
|
+
height, the leading half's outer inset and the outline width are the button
|
|
78
|
+
ladder's own, so they are not restated; `between-space` is 2dp in all five
|
|
79
|
+
families, so it is written once.
|
|
80
|
+
|
|
81
|
+
**There is no new script.** The trailing half *is* a Menu trigger, so
|
|
82
|
+
`AutoInit()` starts it, `data-target` names the surface, and Menu's keyboard
|
|
83
|
+
model is unchanged. `aria-expanded` is Menu's to write (SEMANTICS rule 1),
|
|
84
|
+
which is what lets the expanded shape - the chevron turned over, the seam
|
|
85
|
+
swollen to fully round - be drawn from a selector alone. Never author it: the
|
|
86
|
+
shape is drawn from the attribute, so an authored `"true"` draws an open split
|
|
87
|
+
button over a closed menu.
|
|
88
|
+
|
|
89
|
+
**The halves are matched with `:where()`, not `:is()`.** `:is()` takes the
|
|
90
|
+
specificity of its heaviest argument, and `a.button` put the hover morph one
|
|
91
|
+
class above the expanded rule - so hovering an open split button collapsed the
|
|
92
|
+
seam Material says stays round for as long as the menu is up.
|
|
93
|
+
|
|
94
|
+
The container declares no role. `toolbar` is *rejected* rather than withheld:
|
|
95
|
+
the two halves are independent commands reached with Tab, so there is no
|
|
96
|
+
arrow-key model to write. The one composite widget here is the `<menu>`, and
|
|
97
|
+
Menu owns its contract.
|
|
98
|
+
|
|
99
|
+
- **Button groups** (`div.button-group`, `.connected`, the five button sizes),
|
|
100
|
+
one of M3 Expressive's headline components. A row of
|
|
101
|
+
related buttons that reads as one control without becoming one: standard
|
|
102
|
+
keeps each item's round shape and closes the gap as the buttons grow
|
|
103
|
+
(18/12/8/8/8dp), connected sits every item 2dp apart with round ends and
|
|
104
|
+
squared joins that square one step further while pressed.
|
|
105
|
+
|
|
106
|
+
M3 states this as **ten token families** (`md.comp.button-group.{standard,
|
|
107
|
+
connected}.{xsmall…xlarge}`, DSP 34.0.21). They collapse to one family here
|
|
108
|
+
the way the button's own ladder does — the size class rewrites the tokens, and
|
|
109
|
+
only the numbers that actually differ (the standard gap, the connected corners
|
|
110
|
+
at `large` and `xlarge`) are named twice.
|
|
111
|
+
|
|
112
|
+
**The size goes on the group, not on every button.** A group sets the button
|
|
113
|
+
tokens its items inherit, so `<div class="button-group medium">` sizes the row;
|
|
114
|
+
an item carrying its own size class still wins for itself. That is what moved
|
|
115
|
+
the size ladder out of `components/_buttons.scss` and into
|
|
116
|
+
`abstracts/_variables.scss` as `$button-sizes`, with `button-size()` beside the
|
|
117
|
+
other mixins: two partials read the table now, and a partial may not `@use`
|
|
118
|
+
another partial. No class, token or value changed.
|
|
119
|
+
|
|
120
|
+
**An icon-only item is `<button class="button circle">`, not an
|
|
121
|
+
`.icon-button`.** That component sets its own size ladder, insets and pressed
|
|
122
|
+
shape on the element itself, so a group can neither size it by handing its
|
|
123
|
+
items tokens nor reshape it without fighting the morph it already draws. A
|
|
124
|
+
`.circle` grows by its width on press, where a label item grows by its
|
|
125
|
+
insets - padding inside a stated width would squeeze the icon rather than
|
|
126
|
+
widen the button.
|
|
127
|
+
|
|
128
|
+
**The item contract is enforced, not just documented.** A group item is a
|
|
129
|
+
`<button>` or an `<a class="button" href>` - a bare `<a href>` passes for a
|
|
130
|
+
control and renders as a link, reaching none of the group's shape or sizing -
|
|
131
|
+
and an `.icon-button` is refused outright. Both are `semantics.json` rules, so
|
|
132
|
+
markup that would render wrong fails the suite rather than the eye.
|
|
133
|
+
|
|
134
|
+
**No selection, deliberately.** M3's connected group draws a selected state,
|
|
135
|
+
and holding one takes a control that remembers the answer — which is
|
|
136
|
+
`.segmented-button`, a `<fieldset>` of radios or checkboxes. A row of
|
|
137
|
+
`<button>`s has nothing to read it off, so the selected shape is the segmented
|
|
138
|
+
button's and the group stays a set of commands.
|
|
139
|
+
|
|
140
|
+
**No composite role, and it is rejected rather than withheld.** Toolbar's
|
|
141
|
+
`toolbar` waits on a keyboard model somebody still has to write; a button
|
|
142
|
+
group is not a composite widget at all — its items are independent commands
|
|
143
|
+
reached with Tab. A group that needs a name takes `role="group"` with an
|
|
144
|
+
`aria-label`, which promises no keyboard contract.
|
|
145
|
+
|
|
146
|
+
The press-time growth of a standard group's item is M3's 15%, taken from the
|
|
147
|
+
item's height rather than its width and with the items beside it sliding
|
|
148
|
+
rather than compressing: compression needs each item's measured width, which
|
|
149
|
+
is script. The token holds the figure as a ratio because CSS cannot multiply
|
|
150
|
+
a length by a percentage.
|
|
151
|
+
|
|
152
|
+
- **Scrim** (`--md-comp-scrim-color`), the wash a modal surface paints behind
|
|
153
|
+
itself, defined once. The dialog, the navigation drawer and the navigation
|
|
154
|
+
rail were each mixing `scrim` at 32% themselves — four rules across three
|
|
155
|
+
partials, none of which knew about the others. All four now read the token.
|
|
156
|
+
|
|
157
|
+
Both sheets are consumers but needed no edit: they never had a value of their
|
|
158
|
+
own, taking the modal wash from the `dialog::backdrop` rule they already
|
|
159
|
+
share and only suppressing it for the standard variant.
|
|
160
|
+
|
|
161
|
+
A **foundation**, not a component: there is no markup an author writes to get
|
|
162
|
+
one, so `semantics.json` carries it with `kind: "foundation"` and no rules,
|
|
163
|
+
the way `transitions` is carried. Its consumers already own the rows for the
|
|
164
|
+
markup that ends up wearing it.
|
|
165
|
+
|
|
166
|
+
It is declared on `:root, :host` rather than per surface because four of the
|
|
167
|
+
five consumers paint into `::backdrop`, which has no box of its own to hang a
|
|
168
|
+
token on — only the rail uses a `::before`. `::backdrop` inherits from the
|
|
169
|
+
element it belongs to, so setting `--md-comp-scrim-color` on one `<dialog>`
|
|
170
|
+
dims just that one, and setting it on the root rethemes every scrim on the
|
|
171
|
+
page. `:host` is there for the reason `tokens/_theme.scss` and
|
|
172
|
+
`tokens/_state.scss` carry it: a sheet loaded inside a shadow root cannot
|
|
173
|
+
reach its own host with `:root`, and an undefined token would take
|
|
174
|
+
`background-color` down with it and leave the surface with no scrim at all.
|
|
175
|
+
|
|
176
|
+
**One thing narrows, deliberately.** The mix resolves at `:root`, so
|
|
177
|
+
`--md-sys-color-scrim` is substituted there and overriding *that* token on a
|
|
178
|
+
subtree no longer reaches a scrim below it. Override it at the root to move
|
|
179
|
+
every scrim, or set `--md-comp-scrim-color` on a surface to move one. A scrim
|
|
180
|
+
is painted by a top-layer or fixed box covering the viewport, so there is no
|
|
181
|
+
meaningful subtree for it to have followed.
|
|
182
|
+
|
|
183
|
+
Blur is deliberately not folded in. Only the basic dialog frosts what is
|
|
184
|
+
behind it (`--md-comp-basic-dialog-scrim-blur`, unchanged); the drawer and the
|
|
185
|
+
rail slide over the page and would smear it. Nothing changes visually — all
|
|
186
|
+
four sites computed `oklab(0 0 0 / 0.32)` before and after.
|
|
187
|
+
- **Floating sheet** (`dialog.floating-sheet`), the third member of M3's sheet
|
|
188
|
+
family: secondary content on a surface detached from every window edge, where
|
|
189
|
+
a bottom sheet docks to the bottom and a side sheet to one side. `show()` is
|
|
190
|
+
standard (no scrim, the page stays interactive), `showModal()` is modal.
|
|
191
|
+
|
|
192
|
+
**No behavior module.** It does not drag, so the only dismissal it needs
|
|
193
|
+
beyond Escape and its action row is a scrim tap — which `Dialogs.Init()`
|
|
194
|
+
already gives every `<dialog>`. `BottomSheets` and `SideSheets` exist for the
|
|
195
|
+
drag gesture; a floating sheet has none, so adding a third module would have
|
|
196
|
+
been a parallel copy of light dismiss.
|
|
197
|
+
|
|
198
|
+
It is a `<dialog>` and reuses the dialog slots rather than restating them: the
|
|
199
|
+
partial re-points `--md-comp-basic-dialog-container-*` at its own tokens and
|
|
200
|
+
overrides placement, elevation and the standard variant's scrim. Its action
|
|
201
|
+
row keeps sheet buttons instead of the dialog's text buttons, like the other
|
|
202
|
+
two sheets.
|
|
203
|
+
|
|
204
|
+
Tokens are `md.comp.sheet.floating` (DSP 34.0.21), which publishes exactly
|
|
205
|
+
three values: `surface-container-low`, 28dp corners, elevation 1. **Placement
|
|
206
|
+
is not in the spec**, so the 24dp inset and 400dp cap are this framework's and
|
|
207
|
+
are custom properties (`--md-comp-floating-sheet-inset`,
|
|
208
|
+
`--md-comp-floating-sheet-container-max-width`) rather than fixed numbers.
|
|
209
|
+
|
|
210
|
+
**There are deliberately no edge modifier classes.** `.bottom` already selects
|
|
211
|
+
a bottom sheet and `.left` / `.right` a side sheet, so a floating sheet
|
|
212
|
+
borrowing those names would match two sheets' rules at once. Anchor it to a
|
|
213
|
+
corner with `inset` / `margin` instead.
|
|
214
|
+
|
|
215
|
+
- **Bottom app bar** (`.bottom-app-bar`), an 80dp row of the current screen's
|
|
216
|
+
commands at the bottom edge, with an optional FAB. CSS only.
|
|
217
|
+
|
|
218
|
+
**It is not the navigation bar, and the markup is where that is settled.** A
|
|
219
|
+
navigation bar holds destinations that do not change per screen and is a
|
|
220
|
+
`<nav>` landmark; this holds the actions of the screen the reader is already
|
|
221
|
+
on, so it is not a `<nav>` and takes no landmark (rule 4). It takes no
|
|
222
|
+
`role="toolbar"` either — that is a composite role promising arrow-key
|
|
223
|
+
navigation nothing here implements (rule 2), so it is *withheld*, the same
|
|
224
|
+
account the toolbar gives. `semantics.json` enforces both, plus a name on
|
|
225
|
+
every icon-only action.
|
|
226
|
+
|
|
227
|
+
This component existed once before, keyed on `footer:has(> nav:only-child)`,
|
|
228
|
+
and was removed with the navigation bar's arrival because an element-driven
|
|
229
|
+
host claimed the landmark from the markup alone and left the two bars one slip
|
|
230
|
+
apart. The host is a class now, and the rule forbids every landmark element,
|
|
231
|
+
`<footer>` included — not just `<nav>`.
|
|
232
|
+
|
|
233
|
+
**Nothing in it collides with `nav.navigation-bar`.** Material says never show
|
|
234
|
+
both; a page that does anyway gets two intact bars rather than one broken one,
|
|
235
|
+
and `tests/bottom-app-bar.test.js` asserts no selector on either side reaches
|
|
236
|
+
the other.
|
|
237
|
+
|
|
238
|
+
Tokens are `md.comp.bottom-app-bar.*` (DSP 34.0.21): 80dp,
|
|
239
|
+
`surface-container`, elevation 2, square. One height with or without a FAB —
|
|
240
|
+
the 72dp `with-fab` height is deprecated. The 4dp inline padding is Compose's
|
|
241
|
+
`16.dp - 12.dp`: a 48dp action already insets its 24dp glyph by 12, so the
|
|
242
|
+
glyph lands 16dp from the container edge. The in-bar FAB is flat and
|
|
243
|
+
`secondary-container` per `BottomAppBarDefaults`, not the free-standing FAB's
|
|
244
|
+
lifted `primary-container`, and `margin-inline-start: auto` puts it at the end
|
|
245
|
+
with no spacer element.
|
|
246
|
+
|
|
247
|
+
M3 Expressive's docked toolbar (`.toolbar.docked`) is the newer 64dp answer to
|
|
248
|
+
the same problem and is unchanged. Both ship.
|
|
249
|
+
|
|
250
|
+
- **A bottom sheet's handle dismisses it when the handle is a `<button>`.** The
|
|
251
|
+
slot was pointer-only: `BottomSheets` bound `pointerdown`/`move`/`up`, so a
|
|
252
|
+
handle written as `<button aria-label="Dismiss">` — which the docs taught —
|
|
253
|
+
did nothing on Enter. The label promised an action no code performed, and
|
|
254
|
+
dragging is a pointer gesture, so the only keyboard route to the same outcome
|
|
255
|
+
was Escape, which the button did not advertise. Activation now closes the
|
|
256
|
+
sheet. A handle written as any other element stays decoration and stays inert.
|
|
257
|
+
|
|
258
|
+
**The click that ends a drag is told apart from a tap**, because a drag
|
|
259
|
+
finishes with a `click` on the element it started on and would otherwise
|
|
260
|
+
dismiss the sheet the drag had just declined to dismiss. Movement past 4px
|
|
261
|
+
marks the sequence as a drag, and the flag is cleared on `pointerdown` so an
|
|
262
|
+
abandoned sequence cannot leave it set. Keyboard activation does not consult
|
|
263
|
+
that flag at all — a `click` carrying `detail: 0` came from Enter or Space, so
|
|
264
|
+
a stale drag can never swallow a key press.
|
|
265
|
+
|
|
266
|
+
`tests/bottom-sheet.test.js` covers both directions: activation dismisses
|
|
267
|
+
through either spelling of the class, a snapped-back drag's trailing click
|
|
268
|
+
does not, a keyboard activation still lands after one, and a decorative handle
|
|
269
|
+
is not a control.
|
|
270
|
+
|
|
271
|
+
- **Drag handle** (`.drag-handle`), the affordance that makes something legible
|
|
272
|
+
as draggable. CSS only, no plugin, no registry entry.
|
|
273
|
+
|
|
274
|
+
**Which element you write is the whole semantic.** A
|
|
275
|
+
`<span class="drag-handle" aria-hidden="true">` is decoration, which is the
|
|
276
|
+
usual case — the handle draws a bar and holds no text, so exposed it arrives
|
|
277
|
+
in the reading order as an unlabelled blank. A `<button>` with an `aria-label`
|
|
278
|
+
is a control, and is written only when activating it does something.
|
|
279
|
+
`semantics.json` enforces both directions, including the trap in the middle:
|
|
280
|
+
`aria-hidden` on a button hides it from assistive technology without taking it
|
|
281
|
+
out of the tab order. The hover, focus and pressed states key off
|
|
282
|
+
`button.drag-handle` for that reason, so a decorative handle stays inert under
|
|
283
|
+
a pointer merely crossing it — and that scoping is only correct *because* the
|
|
284
|
+
rule holds every other element to `aria-hidden`.
|
|
285
|
+
|
|
286
|
+
**Nothing here drags, and the docs say so rather than implying otherwise.**
|
|
287
|
+
Dragging is a pointer gesture, so an outcome reachable only by dragging is
|
|
288
|
+
unreachable without a pointer. No reordering behaviour ships to hang off this
|
|
289
|
+
handle; a page that builds one owes it a keyboard path of its own, and
|
|
290
|
+
`llm.md`, `m3-guidelines.md` §6.4 and the docs page each state that plainly.
|
|
291
|
+
|
|
292
|
+
**`md.comp.drag-handle` is the *vertical* handle**, which is easy to mistake
|
|
293
|
+
for the bottom sheet's grabber and is not it. Material tokenised the bar that
|
|
294
|
+
sits in the gutter between two panes — Compose's `VerticalDragHandle` — 4×48dp
|
|
295
|
+
in a 24dp hit target, swelling to 12×52dp on a 12dp corner while held. The
|
|
296
|
+
sheet's grabber is a different component with its own token set,
|
|
297
|
+
`md.comp.sheet-bottom.docked.drag-handle`, horizontal and 32×4dp at 40%
|
|
298
|
+
`on-surface-variant`. So one class serves both hosts and neither one's values
|
|
299
|
+
are invented: inside a sheet, `_bottom-sheet.scss` supplies the geometry and
|
|
300
|
+
suppresses the vertical bar this partial draws in `::before`, which would
|
|
301
|
+
otherwise print across the sheet's own.
|
|
302
|
+
|
|
303
|
+
The bar is a pseudo-element rather than the box because the box is the hit
|
|
304
|
+
target — 4dp of anything is not something a pointer can be asked to find — and
|
|
305
|
+
because the state layer needs a surface the bar's own shape does not clip. The
|
|
306
|
+
container is sized to the *pressed* bar, so holding the handle moves nothing
|
|
307
|
+
beside it.
|
|
308
|
+
|
|
309
|
+
**`.handle` is the pre-1.0 name for the bottom sheet's slot and still works.**
|
|
310
|
+
The rename is additive in the usual way and `tests/m3-naming.test.js` asserts
|
|
311
|
+
it: every rule reaching `.handle` reaches `.drag-handle` too.
|
|
312
|
+
|
|
313
|
+
- **Banners** (`.banner`, `.banner.rich`), Material 3 Expressive's persistent
|
|
314
|
+
in-flow message. A `<div class="banner">` holding a `<p>`, an optional leading
|
|
315
|
+
icon, an optional `.actions` row of text buttons and an optional close icon
|
|
316
|
+
button; `rich` adds a heading and takes an 80dp `<img>` in place of the icon.
|
|
317
|
+
Two colour variants (`vibrant` for `primary-container`) and one shape
|
|
318
|
+
modifier (`square`). CSS only — closing a banner is removing it from the page,
|
|
319
|
+
so there is nothing to initialize and nothing to tear down.
|
|
320
|
+
|
|
321
|
+
**It is the third way of talking back, and the docs say which of the three to
|
|
322
|
+
reach for.** Snackbar is transient and floats; banner is persistent and sits
|
|
323
|
+
in the flow; dialog blocks. A message nobody needs to dismiss is a snackbar,
|
|
324
|
+
and a message the user is allowed to ignore is not a dialog — `llm.md`,
|
|
325
|
+
`m3-guidelines.md` §3.3 and §7.5, and the docs page all state the trade the
|
|
326
|
+
same way, because shipping the third one without that guidance is how
|
|
327
|
+
component sprawl starts.
|
|
328
|
+
|
|
329
|
+
A banner is **not** `role="banner"`, which is the page header landmark; a page
|
|
330
|
+
has one of those and may have several banners. `semantics.json` blocks both
|
|
331
|
+
spellings of the confusion — `<header class="banner">` and the role on the
|
|
332
|
+
container. Its action row is `.actions` and never a `<nav>` (rule 4).
|
|
333
|
+
|
|
334
|
+
The basic and rich layouts are scoped `:not(.rich)` and `.rich` rather than
|
|
335
|
+
left to source order, so they tie on specificity and neither reaches into the
|
|
336
|
+
other. The rich layout is a named grid, so the five parts can be written in
|
|
337
|
+
any order and a missing one collapses its track. Values are
|
|
338
|
+
`md.comp.banners.*`, `md.comp.banners.basic.*` and `md.comp.banners.rich.*` at
|
|
339
|
+
DSP 34.0.21; the four the sheet does not spend — the `two-lines` height, both
|
|
340
|
+
`body-text.trailing-space` close-button reserves, and
|
|
341
|
+
`rich.leading-element.trailing-space` — are named in the partial with the
|
|
342
|
+
reason each is unspendable in CSS.
|
|
343
|
+
|
|
344
|
+
- **Loading indicator** (`.loading-indicator`), M3 Expressive's indicator for a
|
|
345
|
+
wait under about five seconds: a shape that morphs through a circle and four
|
|
346
|
+
cookie shapes while it rotates. One empty `<span>`, CSS-only, no plugin.
|
|
347
|
+
`contained` puts it on a `secondary-container` circle for backgrounds the
|
|
348
|
+
plain indicator would be lost against.
|
|
349
|
+
|
|
350
|
+
**It supersedes the indeterminate circular case of `.progress`** — write it
|
|
351
|
+
anywhere you would have written `<span class="progress circular">` with no
|
|
352
|
+
value. `.progress` is unchanged and keeps both linear bars and every
|
|
353
|
+
determinate case, including the determinate circular one. Neither is
|
|
354
|
+
deprecated; the docs, `llm.md` and `m3-guidelines.md` all state which is the
|
|
355
|
+
default for which wait.
|
|
356
|
+
|
|
357
|
+
The morph is `clip-path` keyframes, which interpolate vertex by vertex and so
|
|
358
|
+
need every shape to have the same vertex count. Each is 24 points on a polar
|
|
359
|
+
curve, generated by `_petal-shape()` in the partial: 24 divides 4, 6, 8 and
|
|
360
|
+
12, so every cusp lands on a vertex. `prefers-reduced-motion: reduce` stops
|
|
361
|
+
both animations and leaves the circle. The element reports nothing on its own,
|
|
362
|
+
so the author gives it `role="status"` and a name — not `role="progressbar"`,
|
|
363
|
+
which promises an `aria-valuenow` an indeterminate indicator does not have.
|
|
364
|
+
|
|
365
|
+
- **The vibrant emphasis axis is a foundation, not a component variant.** The
|
|
366
|
+
`vibrant` attribute remaps the surface family of `--md-sys-color-*` roles for
|
|
367
|
+
its whole subtree onto an accent container, so a component needs no vibrant
|
|
368
|
+
code of its own to sit on one — the same inheritance trick `tokens/_theme.scss`
|
|
369
|
+
uses for `light-dark()`. It points at the live role names rather than the
|
|
370
|
+
`-light` / `-dark` pair, so a runtime theme switch reaches it. There is one
|
|
371
|
+
ramp, tertiary, which is what Material uses for the axis: accent and outline
|
|
372
|
+
roles are deliberately left alone, since pointing the surfaces at primary or
|
|
373
|
+
secondary would erase every component whose own fill is that container, and
|
|
374
|
+
deriving outline from the text colour would turn every checkbox, radio and
|
|
375
|
+
switch border in the subtree translucent. Nothing is restyled by default.
|
|
376
|
+
`:host([vibrant])` is matched alongside `[vibrant]`, so a shadow-DOM host can
|
|
377
|
+
carry it — a sheet loaded in a shadow root cannot reach its own host with a
|
|
378
|
+
descendant selector. The elevation ladder collapses inside a vibrant subtree
|
|
379
|
+
— every `surface-container` rung resolves to the one container colour, which
|
|
380
|
+
is what a vibrant surface is in Material, and `on-surface-variant` collapses
|
|
381
|
+
onto `on-surface` with it — so it belongs on a component, not a page.
|
|
382
|
+
|
|
383
|
+
The `.vibrant` classes on Menu and Toolbar keep the colours Material's own
|
|
384
|
+
per-component token sets give each one, and Toolbar's is untouched. Menu's
|
|
385
|
+
now also answers to `[vibrant]`: a selected menu item is filled with
|
|
386
|
+
`tertiary-container`, the very colour the attribute paints the menu with, so
|
|
387
|
+
selection would otherwise disappear into its own surface. Material's
|
|
388
|
+
`menus-vibrant` moves selection to solid `tertiary`, which the class already
|
|
389
|
+
did, and FormSelect's option fill reads the same token so it follows.
|
|
390
|
+
- **The extended FAB took Material 3's size and colour axes.** `extend medium`
|
|
391
|
+
is 80dp with a 28dp icon, 20dp corners and a `title-large` label; `extend
|
|
392
|
+
large` is 96dp with a 36dp icon, 28dp corners and a `headline-small` label.
|
|
393
|
+
The container colour is a role rather than a value — `primary-container` is
|
|
394
|
+
the default, `secondary-container` and `tertiary-container` are the other
|
|
395
|
+
two — and each sets `--md-comp-extended-fab-container-color` and
|
|
396
|
+
`--md-comp-extended-fab-label-text-color` together, so the hover and focus
|
|
397
|
+
state layers, which mix one into the other, follow the role with it. The
|
|
398
|
+
three colour utilities that name those roles step aside for `.extend`
|
|
399
|
+
(`.primary-container:not(.extend)`, and the same for the other two):
|
|
400
|
+
utilities win by layer rather than weight, so a bare role class on an
|
|
401
|
+
extended FAB would have repainted the container and left the label colour
|
|
402
|
+
and both state layers behind. The other 46 roles are untouched — a role the
|
|
403
|
+
component does not name has nothing to lose.
|
|
404
|
+
|
|
405
|
+
**Migration.** `extend small` carries the `title-medium` label Material
|
|
406
|
+
gives it, where it used to keep `label-large` from the sizeless `extend`.
|
|
407
|
+
A sizeless `extend` is unchanged.
|
|
408
|
+
- **Search bar and search view.** `search.search-bar` is Material 3's search
|
|
409
|
+
bar — 56dp on a full corner, `surface-container-high` at elevation 3, holding
|
|
410
|
+
a bare `<input type="search">` with none of the text-field chrome. Values are
|
|
411
|
+
`md.comp.search-bar.*` / `md.comp.search-view.*` at DSP 34.0.21. End spacing
|
|
412
|
+
reads the bar's own children: 16dp beside a bare glyph, 4dp beside an icon
|
|
413
|
+
button, which already insets its glyph by 12dp. The `<search>` element is the
|
|
414
|
+
landmark, so there is no `role="search"` to author.
|
|
415
|
+
|
|
416
|
+
`.search-view` is the surface the bar expands into, and it is two different
|
|
417
|
+
native elements because the variants want two different behaviours. Docked,
|
|
418
|
+
it is a plain element inside the bar, toggled with `hidden`: a modeless
|
|
419
|
+
`<dialog>.show()` moves focus into the dialog and would take the caret out of
|
|
420
|
+
the input being typed in. Full-screen, it is a
|
|
421
|
+
`dialog.search-view.full-screen` opened with `showModal()`, so Escape and
|
|
422
|
+
light dismiss come from the platform and `behaviors/dialogs.ts`. Its header
|
|
423
|
+
is another `.search-bar` with the pill and shadow taken off — the same markup
|
|
424
|
+
either way. No JavaScript component ships with it.
|
|
425
|
+
|
|
426
|
+
**Suggestions stay Autocomplete's.** `class="autocomplete"` on the bar's
|
|
427
|
+
input gives the combobox, its listbox and the keyboard contract already
|
|
428
|
+
implemented and tested there, so nothing here duplicates them. The search
|
|
429
|
+
view itself *rejects* `listbox` rather than owing it: its contents are links
|
|
430
|
+
and buttons reached with Tab, and a second suggestion listbox here would be
|
|
431
|
+
the same promise with no keyboard model behind it. `semantics.json` records
|
|
432
|
+
the rejection.
|
|
433
|
+
|
|
434
|
+
**Migration.** `.searchbar`, the pre-1.0 Materialize navbar search field,
|
|
435
|
+
is now an alias of `.search-bar` and moves out of `forms/_input-fields.scss`
|
|
436
|
+
into `components/_search.scss`. Old markup keeps working and picks up the M3
|
|
437
|
+
container; `tests/m3-naming.test.js` holds the alias to every rule.
|
|
438
|
+
- **FAB menu.** `fab-menu` is Material 3 Expressive's FAB menu: a FAB that
|
|
439
|
+
expands into a list of labelled pills — 56dp tall, full corner, 24dp insets,
|
|
440
|
+
a `title-medium` label — and morphs into the close button while it is open.
|
|
441
|
+
The colour axis is the same three roles as the extended FAB, and one class
|
|
442
|
+
moves both halves: the actions take `<role>-container` and the close button
|
|
443
|
+
the solid `<role>`, which is the pairing Material specifies. Expanded state
|
|
444
|
+
is the framework's — the constructor stamps `aria-expanded` and `open()` /
|
|
445
|
+
`close()` move it with the `active` class, and `semantics.json` forbids
|
|
446
|
+
authoring either.
|
|
447
|
+
|
|
448
|
+
It runs on the existing `FloatingActionButton` rather than a second class,
|
|
449
|
+
so `AutoInit()`, `open()`, `close()`, `isOpen` and `destroy()` all work
|
|
450
|
+
unchanged; `direction` and `hoverEnabled` are inert on it, because a FAB
|
|
451
|
+
menu opens upward and on click and both are decided in CSS. It does not
|
|
452
|
+
touch the `.fab` speed dial or the `.fab.toolbar` transition: `.fab` is a
|
|
453
|
+
whole class name and never matches `.fab-menu`.
|
|
454
|
+
- **Segmented buttons.** A `fieldset.segmented-button` of `<input>` and
|
|
455
|
+
`label.segment` pairs is Material 3's outlined segmented button — 40dp on
|
|
456
|
+
a full corner, equal columns filling the width it is given, a 1dp outline
|
|
457
|
+
with dividers between segments, 12dp insets, an
|
|
458
|
+
optional 18dp icon, and `secondary-container` / `on-secondary-container` for
|
|
459
|
+
the chosen one. Radios make the group single-select and checkboxes make it
|
|
460
|
+
multi-select; the input type is the entire difference between the two
|
|
461
|
+
variants.
|
|
462
|
+
|
|
463
|
+
There is no plugin. The control is the label's sibling and not its child,
|
|
464
|
+
the way a filter chip is written: a `<label>` wrapping a radio or a checkbox
|
|
465
|
+
*is* one to `forms/_radio-buttons` and `forms/_checkboxes`, which would paint
|
|
466
|
+
a 20dp ring on the control and stretch the label to a 48dp row. Selection is
|
|
467
|
+
read off the control with `input:checked + .segment`, so `checked` states
|
|
468
|
+
the initial value and the browser owns it from there — there is no class to
|
|
469
|
+
keep in sync and no ARIA copy to go stale, and `semantics.json` refuses
|
|
470
|
+
`aria-checked`,
|
|
471
|
+
`aria-selected` and `aria-pressed` on a segment. The composite role is
|
|
472
|
+
*rejected* rather than withheld, which is Carousel's case and not Tabs':
|
|
473
|
+
rule 2 asks for an implemented keyboard contract before a component claims a
|
|
474
|
+
composite role, and a `<fieldset>` of radios already has one the browser
|
|
475
|
+
implements — so `radiogroup` is declined because the element already carries
|
|
476
|
+
it, not deferred. `forbid-composite-roles` keeps that and the other nine out
|
|
477
|
+
of the markup. What the rules enforce instead is that the markup stays
|
|
478
|
+
native: a `<div>` root, a `<button>` segment or a radio without the shared
|
|
479
|
+
`name` takes the group name, the arrow keys and the checked state with it. The `<legend>` names the group and is
|
|
480
|
+
hidden, since Material's anatomy has no visible group label. Nothing shows
|
|
481
|
+
it: the root is both the fieldset and the grid of segments, so a legend back
|
|
482
|
+
in flow is another column beside them, and a group that needs a visible label
|
|
483
|
+
writes one before the fieldset.
|
|
484
|
+
- **Carousel can advance on its own, with a pause contract that cannot be
|
|
485
|
+
switched off.** An `interval` (milliseconds, `0` and off by default) cycles
|
|
486
|
+
the track; `pause()` and `start()` stop and resume it. The gap follows each
|
|
487
|
+
transition rather than containing it, so a full cycle takes
|
|
488
|
+
`duration + interval`; each rest is armed by the move before it rather than
|
|
489
|
+
running on a fixed phase, so a rest can never end on a move still animating
|
|
490
|
+
nor be cut to the remainder of one that did. Auto-advancing content
|
|
491
|
+
is WCAG 2.2.2 territory, so unlike the Slideshow it replaces, Carousel offers
|
|
492
|
+
no `pauseOnHover` / `pauseOnFocus` booleans to turn off — an interval always
|
|
493
|
+
pauses while the pointer is over the carousel, while focus is inside it, and
|
|
494
|
+
while the tab is hidden, and `prefers-reduced-motion: reduce` suppresses
|
|
495
|
+
auto-advance entirely.
|
|
496
|
+
- Carousel takes a fixed `height` in pixels, reproducing the Slideshow layout:
|
|
497
|
+
the height sizes the track and the indicators take their own row below it
|
|
498
|
+
(`--md-comp-carousel-indicator-allowance`, 40px) instead of covering the
|
|
499
|
+
media. Markup can ask for the same layout on its own, by carrying
|
|
500
|
+
`.fixed-height` and setting `--carousel-height`. An explicit `noWrap: true`
|
|
501
|
+
stops auto-advance after one pass rather than looping.
|
|
502
|
+
|
|
503
|
+
### Changed
|
|
504
|
+
|
|
505
|
+
- **`.button.circle` sizes its glyph from the button ladder.** It used to pin
|
|
506
|
+
24px whatever size class it wore, so it got the size's *box* and a fixed
|
|
507
|
+
glyph in it: 24px in a 32dp `xsmall` disc, and — the visible one — 24px
|
|
508
|
+
adrift in a 136dp `xlarge` disc. The glyph is now
|
|
509
|
+
`--md-comp-filled-button-icon-size` like every other button's, which is
|
|
510
|
+
20 / 20 / 24 / 32 / 40dp across the five rungs. `.circle.extra` and
|
|
511
|
+
`.circle.large` are the FAB and are unaffected: they read
|
|
512
|
+
`--md-comp-fab-icon-size` and always did.
|
|
513
|
+
|
|
514
|
+
**Migration.** The default `<button class="button circle">` carries no size
|
|
515
|
+
class, so it is the `small` rung and its glyph moves **24px → 20px**. That is
|
|
516
|
+
a visible change on every page using one. 24px in a 40dp disc is the *icon
|
|
517
|
+
button's* number (`md.comp.icon-button.small.icon-size`), not the common
|
|
518
|
+
button's, so the control that keeps it is `.icon-button` — which is the real
|
|
519
|
+
M3 icon button, with the whole ladder and its own colours:
|
|
520
|
+
|
|
521
|
+
```html
|
|
522
|
+
<!-- 40dp box, 20dp glyph (the button ladder) -->
|
|
523
|
+
<button class="button circle" aria-label="Add">…</button>
|
|
524
|
+
<!-- 40dp box, 24dp glyph (the icon-button ladder) -->
|
|
525
|
+
<button class="icon-button" aria-label="Add">…</button>
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
Or state the rung you meant: `.circle.medium` is 56dp with a 24dp glyph.
|
|
529
|
+
|
|
530
|
+
One host is worth checking by eye: the **top app bar** sizes its own
|
|
531
|
+
icon-only actions at 24dp but steps aside for anything that styles itself,
|
|
532
|
+
`.circle` included — so a `.button.circle` in a bar now shows a 20dp glyph
|
|
533
|
+
beside the bar's 24dp ones. The bar's own spelling is a bare
|
|
534
|
+
`<button class="navigation-drawer-trigger">` with no `.button`, which the bar
|
|
535
|
+
sizes itself; `.icon-button` is the other 24dp answer.
|
|
536
|
+
|
|
537
|
+
This subsumes the button-group-local override shipped with button groups,
|
|
538
|
+
which handed a group's `.circle` items the token so a sized group would not
|
|
539
|
+
get the wrong glyph. That rule and its test are gone; the fix is general now.
|
|
540
|
+
|
|
541
|
+
- **Toolbars are the four M3 Expressive variants, from two independent axes.**
|
|
542
|
+
A *shape* — floating or docked — and a *colour style* — standard or vibrant —
|
|
543
|
+
now compose freely, and their values come from the 34.0.21 sets
|
|
544
|
+
`md.comp.toolbar.{standard, vibrant, docked, floating, floating.fab}` rather
|
|
545
|
+
than from the prose the component was first written against. Nothing in the
|
|
546
|
+
markup contract moves: the bar is still not a `<nav>`, and it still withholds
|
|
547
|
+
`role="toolbar"` (rule 2), which `semantics.json` still enforces.
|
|
548
|
+
|
|
549
|
+
Both defaults are written as the *absence* of the other side rather than as
|
|
550
|
+
a selector of their own, which is what makes `.floating` and `.standard`
|
|
551
|
+
writable without four rules or two inert ones — a bar naming either still
|
|
552
|
+
matches the default arm. `.filled` and `.max` keep their old meanings.
|
|
553
|
+
|
|
554
|
+
The corrected values: a floating bar lifts to elevation **3** (was 2) and
|
|
555
|
+
takes **8dp** end insets (was 12dp), 16dp from the viewport edge and 24dp
|
|
556
|
+
when vertical. A docked bar takes **16dp** end insets and spaces its actions
|
|
557
|
+
between 4dp and 32dp apart as it widens, instead of `space-around`, which
|
|
558
|
+
honoured neither bound. Action icons and labels are **`on-surface-variant`**
|
|
559
|
+
(was `on-surface`), and a vibrant bar's selected action is
|
|
560
|
+
**`surface-container`** (was `surface`).
|
|
561
|
+
|
|
562
|
+
`.vibrant` stays M3's own toolbar-vibrant set and deliberately does *not*
|
|
563
|
+
also answer to the `vibrant` attribute. That attribute is the emphasis
|
|
564
|
+
foundation, whose one ramp is tertiary; a bar inside it is repainted by the
|
|
565
|
+
foundation alone, with no toolbar-specific code, because every colour the
|
|
566
|
+
component draws is now a `--md-comp-toolbar-*` token pointed at a
|
|
567
|
+
`--md-sys-color-*` role. Matching the attribute here as well would render
|
|
568
|
+
one variant two ways and would contradict `tokens/_vibrant.scss`.
|
|
569
|
+
|
|
570
|
+
The companion FAB in `.toolbar-group` takes `md.comp.toolbar.floating.fab`:
|
|
571
|
+
`secondary-container` beside a standard bar, `tertiary-container` beside a
|
|
572
|
+
vibrant one, at elevation 1 where the bar is at 3. Mark only the bar. Docs
|
|
573
|
+
that hand-applied colour utilities to that FAB no longer need to.
|
|
574
|
+
|
|
575
|
+
- **The FAB no longer claims to be a menu.** `FloatingActionButton` used to set
|
|
576
|
+
`role="menu"` on the action list and `aria-haspopup="menu"` on the trigger.
|
|
577
|
+
A menu is a composite widget whose items are reached with the arrow keys, and
|
|
578
|
+
this component has no such keyboard model, so both were a promise it does not
|
|
579
|
+
keep (SEMANTICS rule 2). They are gone; `aria-expanded` stays, which is what a
|
|
580
|
+
disclosure actually is. `semantics.json` records `menu` as withheld pending
|
|
581
|
+
the keyboard model rather than leaving the gap unaccounted for.
|
|
582
|
+
|
|
583
|
+
**Migration.** A script or stylesheet selecting `.fab [role="menu"]` needs a
|
|
584
|
+
different selector. Nothing about the markup an author writes changes.
|
|
585
|
+
- **Buttons took Material 3 Expressive's two modifier axes.** Five styles
|
|
586
|
+
(`filled`, `tonal`, `outlined`, `elevated`, `text`) and five sizes
|
|
587
|
+
(`xsmall`, `small`, `medium`, `large`, `xlarge`) are now independent classes
|
|
588
|
+
on `.button`, and any style combines with any size — nine classes, not
|
|
589
|
+
twenty-five, because the size rules set tokens and the style rules set
|
|
590
|
+
colour. `filled` is new, and is what the styleless default already was.
|
|
591
|
+
|
|
592
|
+
**Migration.** Three things behave differently:
|
|
593
|
+
|
|
594
|
+
- `small` is 40dp now, not 32dp — M3's small *is* the default size, so
|
|
595
|
+
writing it changes nothing. The old 32dp button is `xsmall`.
|
|
596
|
+
- `large` is 96dp, not 56dp. The old 56dp button is `medium`, and `extra`
|
|
597
|
+
still resolves to it, so `class="button extra"` is unchanged. `circle
|
|
598
|
+
extra`, `circle large`, `extend` and every FAB size are untouched.
|
|
599
|
+
- Spacing is symmetric at every size. Expressive dropped the old "24dp
|
|
600
|
+
inset, but 16dp next to an icon" asymmetry, so a button with an icon is
|
|
601
|
+
no longer inset differently on the icon's side, and the default inset is
|
|
602
|
+
16dp rather than 24dp. The default icon is 20dp (was 18dp) and corners
|
|
603
|
+
are fully round at every size.
|
|
604
|
+
|
|
605
|
+
An `outlined` button's border also thickens with the size — 1dp up to
|
|
606
|
+
medium, 2dp large, 3dp extra large — from the new
|
|
607
|
+
`--md-comp-filled-button-outline-width` token.
|
|
608
|
+
|
|
609
|
+
- Pane layouts now use M3 spacing: 16dp inline margins on Compact, then
|
|
610
|
+
24dp margins and 24dp spacers on Medium through Extra-large.
|
|
611
|
+
- **Responsive breakpoints now match Material 3 exactly.** Compact is below
|
|
612
|
+
600px, Medium is 600–839px, Expanded is 840–1199px, Large is
|
|
613
|
+
1200–1599px, and Extra-large starts at 1600px. The Sass keys are now
|
|
614
|
+
`compact`, `medium`, `expanded`, `large`, and `extra-large`; consumers that
|
|
615
|
+
used `small`, `xlarge`, or `xxlarge` must migrate, and code that used
|
|
616
|
+
`bp-up("large")` for the old desktop boundary should use
|
|
617
|
+
`bp-up("expanded")`. Grid markup keeps `.s` / `.m` / `.l` / `.xl` /
|
|
618
|
+
`.xxl`, mapped to those five classes in order. Visibility helpers now name
|
|
619
|
+
and isolate the same M3 ranges; legacy `small`, `med`, and `xxl` spellings
|
|
620
|
+
remain aliases where their meaning is unambiguous.
|
|
621
|
+
- **Components took the names Material 3 uses for them.** Every rename is
|
|
622
|
+
additive — the old class stays in the selector list, the old export stays as
|
|
623
|
+
an alias — with one exception, called out below.
|
|
624
|
+
|
|
625
|
+
| M3 | was | now |
|
|
626
|
+
| --- | --- | --- |
|
|
627
|
+
| Slider | `.range` / `Range` | `.slider` / `Slider` |
|
|
628
|
+
| Navigation drawer | `.sidenav` / `Sidenav` | `.navigation-drawer` / `NavigationDrawer` |
|
|
629
|
+
| FAB | `.fixed-action-btn` | `.fab` |
|
|
630
|
+
| Progress indicators | `.preloader` | `.progress.circular` |
|
|
631
|
+
| Date / Time pickers | `.datepicker` / `.timepicker` | `.date-picker` / `.time-picker` |
|
|
632
|
+
|
|
633
|
+
- **`Slider` changed meaning, and that one is a break.** It was the image
|
|
634
|
+
slideshow; it is now the range control, because that is what M3 calls a
|
|
635
|
+
slider. Aliasing it would have defeated the rename. Before 0.8.0, `.slider`
|
|
636
|
+
markup and `Expressive.Slider.init()` started the slideshow; both now name the
|
|
637
|
+
range control. Migrate slideshow markup and script to Carousel using the
|
|
638
|
+
removal guide below. `Expressive.Range` still resolves to the renamed
|
|
639
|
+
`Slider`.
|
|
640
|
+
- **`.field` deliberately did not become `.text-field`.** M3 names the
|
|
641
|
+
component "Text fields", but the same container wraps `<select>`,
|
|
642
|
+
autocomplete and file inputs, so the M3 name would be wrong for most of what
|
|
643
|
+
it does.
|
|
644
|
+
- Sass partials moved with their components: `_sidenav` → `_navigation-drawer`,
|
|
645
|
+
`forms/_range` → `forms/_slider`, and `_preloader` → `_progress`. The old
|
|
646
|
+
slideshow partial, `_slider`, was deleted with that component.
|
|
647
|
+
- **The semantics roster stopped counting things that are not components.** A
|
|
648
|
+
component is a part of the framework an author writes markup for, and four of
|
|
649
|
+
the 45 rows in `semantics.json` were not one. Three now state a `kind`
|
|
650
|
+
instead: `transitions` is a foundation, `docked-display` and
|
|
651
|
+
`character-counter` are behaviors. `table_of_contents` left the file — the
|
|
652
|
+
list it styles is Scrollspy's markup, so `scrollspy` took its rule, and the
|
|
653
|
+
partial is recorded under the new `notComponents` map, which is the only way
|
|
654
|
+
a partial leaves the roster and which requires a live partial and a stated
|
|
655
|
+
reason. The standard now reads **44 of 44 rows enforced, 41 of them
|
|
656
|
+
components**, and the exempt list is still empty.
|
|
657
|
+
|
|
658
|
+
Nothing was deleted to get there: no Sass, no script, no markup, and no rule
|
|
659
|
+
stopped running — a non-component row is checked exactly as a component's is.
|
|
660
|
+
The data file's `components` key is now `rows`, since it never held only
|
|
661
|
+
components.
|
|
662
|
+
|
|
663
|
+
### Removed
|
|
664
|
+
|
|
665
|
+
- **The retired Flask documentation pipeline is gone.** Astro is the sole
|
|
666
|
+
documentation generator, so the Flask application and Jinja templates,
|
|
667
|
+
Frozen-Flask configuration, Python environment, migration script, committed
|
|
668
|
+
`website/` snapshot and generated root `llms.txt` have been removed. The
|
|
669
|
+
documentation site now builds, verifies and publishes from a clean checkout
|
|
670
|
+
with Node alone; visual regression builds both revisions through that same
|
|
671
|
+
Astro production path.
|
|
672
|
+
|
|
673
|
+
- **Waves is gone, replaced by state layers.** Material 3 has no ink ripple; it
|
|
674
|
+
has a state layer, and the framework's is complete — a `md.sys.state` opacity
|
|
675
|
+
per state, documented on the State layers page, painted by the components
|
|
676
|
+
themselves. Keeping a second, script-driven feedback system beside it meant
|
|
677
|
+
two answers to one question, so the ripple is deleted rather than deprecated:
|
|
678
|
+
`behaviors/waves.ts`, the import-time `Waves.Init()`, the `Expressive.Waves`
|
|
679
|
+
export, and the Waves docs page. The bundle no longer attaches a delegated
|
|
680
|
+
`click` listener to `document.body`.
|
|
681
|
+
|
|
682
|
+
**Migration.** `.waves-effect`, `.waves-light` and `.waves-circle` never
|
|
683
|
+
matched a style rule — they were hooks the script read — so markup carrying
|
|
684
|
+
them still renders exactly as it does today, minus the ripple. **Delete
|
|
685
|
+
them**; nothing reads them any more. There is no replacement class to add,
|
|
686
|
+
because a state layer is not authored: an interactive component paints its
|
|
687
|
+
own. `Expressive.Waves.renderWaveEffect()` has no successor.
|
|
688
|
+
|
|
689
|
+
Five surfaces relied on Waves for their only press feedback and now carry a
|
|
690
|
+
pressed state layer of their own: the media-covering `.card-reveal-trigger`
|
|
691
|
+
and `.expanding-card-trigger` buttons, the actions inside a `.fab.toolbar`,
|
|
692
|
+
navigation drawer rows, and pagination items. The last two matter most on
|
|
693
|
+
touch, where hover never fires and both already suppressed the tap
|
|
694
|
+
highlight. The two card triggers read the card's own
|
|
695
|
+
`--md-comp-card-*-state-layer-*` tokens, so overriding a card still reaches
|
|
696
|
+
them; the other three have no component token and read `--md-sys-state-*`.
|
|
697
|
+
|
|
698
|
+
- **Slideshow is gone, and Carousel takes the case.** M3 has no slideshow — its
|
|
699
|
+
Carousel is the component for a band of media — and once Carousel could
|
|
700
|
+
advance on its own,
|
|
701
|
+
the two expressed one concept in 590 lines of duplicate. Deleted outright:
|
|
702
|
+
`components/_slideshow.scss`, `components/slideshow.ts`, the `slideshow`
|
|
703
|
+
`semantics.json` row, and the docs coverage on the Media page.
|
|
704
|
+
`Expressive.Slideshow` no longer resolves. **There is no alias**, because an
|
|
705
|
+
alias keeps both concepts alive while pretending one of them died.
|
|
706
|
+
|
|
707
|
+
**`.slider` is unambiguous again**, which is the bonus worth stating. The
|
|
708
|
+
intermediate content-discrimination selectors are gone with the component:
|
|
709
|
+
`.slider` is the range control and nothing else, and nothing in the sheet asks
|
|
710
|
+
what a `.slider` holds. The range control itself is otherwise untouched.
|
|
711
|
+
|
|
712
|
+
**Migration.** Carousel with a fixed `height` reproduces the layout — the
|
|
713
|
+
height sizes the track and the indicators take their own row beneath it:
|
|
714
|
+
|
|
715
|
+
```html
|
|
716
|
+
<!-- before -->
|
|
717
|
+
<div class="slider">
|
|
718
|
+
<ul class="slides">
|
|
719
|
+
<li><img src="1.jpg" alt="First"><div class="caption"><h3>One</h3></div></li>
|
|
720
|
+
<li><img src="2.jpg" alt="Second"><div class="caption"><h3>Two</h3></div></li>
|
|
721
|
+
</ul>
|
|
722
|
+
</div>
|
|
723
|
+
|
|
724
|
+
<!-- after -->
|
|
725
|
+
<div class="carousel">
|
|
726
|
+
<div class="carousel-item">
|
|
727
|
+
<img src="1.jpg" alt="First">
|
|
728
|
+
<div class="carousel-item-content"><h3>One</h3></div>
|
|
729
|
+
</div>
|
|
730
|
+
<div class="carousel-item">
|
|
731
|
+
<img src="2.jpg" alt="Second">
|
|
732
|
+
<div class="carousel-item-content"><h3>Two</h3></div>
|
|
733
|
+
</div>
|
|
734
|
+
</div>
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
`.carousel-item-content` is the caption slot and is not optional: the item
|
|
738
|
+
clips its overflow and the media fills its height, so a heading left as a
|
|
739
|
+
bare sibling is pushed out of the box rather than laid over the image the way
|
|
740
|
+
`.caption` was. It is also what the small and medium size roles hide and trim
|
|
741
|
+
against.
|
|
742
|
+
|
|
743
|
+
```js
|
|
744
|
+
// before
|
|
745
|
+
Expressive.Slider.init(el, { height: 400, indicators: true, interval: 6000 });
|
|
746
|
+
// after
|
|
747
|
+
Expressive.Carousel.init(el, { height: 400, indicators: true, interval: 6000, fullWidth: true });
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
Four options do not carry over cleanly.
|
|
751
|
+
|
|
752
|
+
- `duration` exists on both but means the transition, not the crossfade —
|
|
753
|
+
and Carousel's rest **follows** each transition, so a cycle takes
|
|
754
|
+
`duration + interval`. `interval: 6000` above therefore paces at 6.2s
|
|
755
|
+
rather than 6s; subtract `duration` to keep the old cadence exactly.
|
|
756
|
+
- `pauseOnHover` and `pauseOnFocus` have no equivalent and are not needed: an
|
|
757
|
+
auto-advancing carousel always pauses under the pointer, while focus is
|
|
758
|
+
inside it, and while the tab is hidden — WCAG 2.2.2 territory, so it is not
|
|
759
|
+
switchable.
|
|
760
|
+
- `indicatorLabelFunc` is replaced by two new `i18n` keys, `indicators` and
|
|
761
|
+
`slide`, which name the indicator row and prefix each dot. They are new in
|
|
762
|
+
this release: those were the last two strings Carousel generated in
|
|
763
|
+
hardcoded English, and SEMANTICS rule 5 wants every generated string on an
|
|
764
|
+
`i18n` option. The replacement is a pair of strings rather than a callback,
|
|
765
|
+
so a label that varied per index — "Go to slide 3 (Current)" — has no
|
|
766
|
+
equivalent; the current dot already carries `aria-current="true"`, which is
|
|
767
|
+
what that suffix was standing in for.
|
|
768
|
+
|
|
769
|
+
**`fullscreen` has no exact equivalent, and `.carousel.full-screen` is not
|
|
770
|
+
it.** A `fullscreen` slideshow filled its positioned ancestor and stayed
|
|
771
|
+
horizontal at every size. `.carousel.full-screen` is M3's immersive feed:
|
|
772
|
+
`100dvh`, square corners, and **vertical**, turning horizontal on its own in
|
|
773
|
+
landscape or at an expanded width (the component adds
|
|
774
|
+
`.full-screen-horizontal` from the rendered geometry, so setting that class
|
|
775
|
+
by hand does not stick). It also sizes itself, ignoring the `height` option.
|
|
776
|
+
Take it for an edge-to-edge feed; for a fixed horizontal band, keep the
|
|
777
|
+
`height` above and leave `full-screen` off.
|
|
778
|
+
|
|
779
|
+
- **The coverflow carousel layout is gone.** `.coverflow` was the 3D
|
|
780
|
+
perspective tween inherited from the vendored MaterializeCSS source, and
|
|
781
|
+
Material 3 defines it at no layout and no size. It hand-wrote a momentum
|
|
782
|
+
simulation the browser already performs — velocity smoothing over a
|
|
783
|
+
`Date.now()` delta, exponential-decay easing toward a target, modular index
|
|
784
|
+
wrapping, per-item opacity and z-translation recomputed every frame — and ran
|
|
785
|
+
a second set of pointer handlers on the same element as the snap track's, so
|
|
786
|
+
a reader of the component had to establish which of two drag models was live
|
|
787
|
+
before tracing a gesture. Deleted with it: the `.carousel.coverflow` Sass
|
|
788
|
+
block, and the `dist`, `shift`, `padding` and `numVisible` options.
|
|
789
|
+
|
|
790
|
+
Every `:not(.coverflow)` guard elsewhere in the partial is now unconditional
|
|
791
|
+
— the reduced-motion block, the fixed-height indicator row, the contained
|
|
792
|
+
edge fades and the grab cursor state their intent directly rather than by
|
|
793
|
+
exclusion. `.carousel` is the Material 3 carousel and nothing else.
|
|
794
|
+
|
|
795
|
+
**Migration.** Delete the class. The nearest Material 3 equivalent to
|
|
796
|
+
coverflow's centred, overlapping look is the **hero** layout, which shows one
|
|
797
|
+
large item flanked by small previews:
|
|
798
|
+
|
|
799
|
+
```html
|
|
800
|
+
<!-- before -->
|
|
801
|
+
<div class="carousel coverflow">
|
|
802
|
+
<a class="carousel-item" href="#one"><img src="1.jpg" alt="One"></a>
|
|
803
|
+
<a class="carousel-item" href="#two"><img src="2.jpg" alt="Two"></a>
|
|
804
|
+
</div>
|
|
805
|
+
|
|
806
|
+
<!-- after -->
|
|
807
|
+
<div class="carousel hero center-aligned">
|
|
808
|
+
<a class="carousel-item" href="#one"><img src="1.jpg" alt="One"></a>
|
|
809
|
+
<a class="carousel-item" href="#two"><img src="2.jpg" alt="Two"></a>
|
|
810
|
+
</div>
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
The four removed options are deleted rather than accepted and ignored, so
|
|
814
|
+
passing one is a type error instead of silence. There is no runtime warning
|
|
815
|
+
and no shim: a shim keeps both concepts alive while pretending one has died.
|
|
816
|
+
|
|
817
|
+
**`duration` and `noWrap` survive and mean what they always did for a snap
|
|
818
|
+
track.** `duration` bounds how long the component waits for `scrollend`
|
|
819
|
+
before assuming a programmatic scroll landed, and is added to `interval` to
|
|
820
|
+
time each auto-advance rest; `noWrap` stops auto-advance after one pass
|
|
821
|
+
instead of looping. Auto-advance — `interval`, `duration`, `noWrap` — behaves
|
|
822
|
+
exactly as it did.
|
|
823
|
+
|
|
824
|
+
- **Parallax and Pulse are gone**, with no successor. This removal is separate
|
|
825
|
+
from the semantics sweep above. Both were decorative
|
|
826
|
+
motion opinions of our own, and M3 Expressive ships a `MotionScheme` of
|
|
827
|
+
spring specs that says something different — the same reasoning that removed
|
|
828
|
+
the legacy palette. Deleted outright: the `.parallax` and `.pulse` styles,
|
|
829
|
+
the `Parallax` component and its registry entry (so `AutoInit()` no longer
|
|
830
|
+
claims `.parallax`, and `Expressive.Parallax` no longer resolves), and both
|
|
831
|
+
docs pages.
|
|
832
|
+
|
|
833
|
+
**Migration.** For a `.pulse` halo, write the animation yourself or use a
|
|
834
|
+
component that already reports what it means — a badge for a count, a
|
|
835
|
+
progress indicator for work in progress. For a `.parallax` hero, the markup
|
|
836
|
+
it wrapped is an image in a container: keep the `<img>` with
|
|
837
|
+
`object-fit: cover` and a fixed container height. Restoring the motion takes
|
|
838
|
+
more than one property — the container needs `view-timeline-name`, the image
|
|
839
|
+
needs to be taller than its clip (`height: calc(100% + 2 * travel)`) and to
|
|
840
|
+
run a `translateY` keyframe pair through `animation-timeline` with
|
|
841
|
+
`animation-range: cover`. Copy the deleted `_parallax.scss` out of the
|
|
842
|
+
history if you want it. A carousel is the M3 component for a scrolling band
|
|
843
|
+
of media.
|
|
844
|
+
|
|
845
|
+
### Fixed
|
|
846
|
+
|
|
847
|
+
- **The sheet is now usable as a shadow root's only stylesheet.** Token
|
|
848
|
+
declarations were split between `:root, :host` and `:root` alone — three
|
|
849
|
+
blocks paired, five not, among them the whole reference layer at 338
|
|
850
|
+
declarations. A sheet adopted only into a shadow root
|
|
851
|
+
(`shadowRoot.adoptedStyleSheets`, no document-level copy) therefore lost every
|
|
852
|
+
one of those tokens, and because `tokens/_theme.scss` resolves each role as
|
|
853
|
+
`light-dark(var(--md-sys-color-<role>-light), …)` out of the `:root`-only
|
|
854
|
+
pairs, it lost every colour in the sheet rather than only the missing tokens.
|
|
855
|
+
Every token-declaring rule now carries its `:host` twin, `[icon-style]`, the
|
|
856
|
+
three `[theme]` selectors and the grid's `--gap-size` on `body` included — so
|
|
857
|
+
a shadow tree can be pinned to a theme or an icon style the way a page can,
|
|
858
|
+
and a `.row` in one has a gutter instead of an invalid `gap`.
|
|
859
|
+
`adr/0002-shadow-only-stylesheet-adoption.md` records the commitment and
|
|
860
|
+
`tests/shadow-dom.test.js` enforces it. The ordinary setup — framework in the
|
|
861
|
+
document, also adopted into components — was never affected, because custom
|
|
862
|
+
properties inherit through shadow boundaries.
|
|
863
|
+
|
|
864
|
+
- **Runtime-created elements now stay in the tree that created them.** Six
|
|
865
|
+
places built an element and appended it to `document.body` — the tooltip
|
|
866
|
+
surface, the snackbar container, the lightbox caption, the drawer's edge drag
|
|
867
|
+
target, and the datepicker's two month/year menus. A sheet adopted into a
|
|
868
|
+
shadow root cannot match a node outside that root, so every one of them
|
|
869
|
+
arrived unstyled in the shadow-only setup the previous entry made possible.
|
|
870
|
+
`Utils.portalRoot(el)` returns `el.getRootNode()` when that is a shadow root
|
|
871
|
+
and `document.body` otherwise, and each site goes through it. The lightbox's
|
|
872
|
+
ancestor-overflow walk hops the shadow boundary through the host instead of
|
|
873
|
+
throwing on the root.
|
|
874
|
+
|
|
875
|
+
**This gives up an escape hatch on purpose.** A portal on `document.body`
|
|
876
|
+
escapes an ancestor's `overflow: hidden` and stacking context; inside a shadow
|
|
877
|
+
root it escapes neither, because the host's ancestors are still in the
|
|
878
|
+
flattened tree. Nothing recomputes coordinates, so a positioned ancestor above
|
|
879
|
+
the host re-anchors the `absolute`-positioned tooltip and a transformed one
|
|
880
|
+
re-anchors the three `fixed` portals. `adr/0002` states the bound; behaviour in
|
|
881
|
+
a plain document is unchanged.
|
|
882
|
+
|
|
883
|
+
Native `<dialog>` was never affected — a top-layer element is painted outside
|
|
884
|
+
the document's paint order but stays in its own tree.
|
|
885
|
+
|
|
886
|
+
**Migration:** `Snackbar` has no originating element, so it takes a new `root`
|
|
887
|
+
option — any element in the target tree. Without it the container still goes to
|
|
888
|
+
`document.body`, exactly as before. Because only one snackbar shows at a time,
|
|
889
|
+
the one shared container moves between roots rather than being duplicated.
|
|
890
|
+
|
|
891
|
+
- **A trigger and its target in the same shadow root can find each other.** The
|
|
892
|
+
mirror of the portal fix: four places resolved an id with
|
|
893
|
+
`document.getElementById` — the menu's target, the tooltip's rich content, the
|
|
894
|
+
snackbar's template and the drawer's trigger — and an id inside a shadow root
|
|
895
|
+
is invisible to the document, so every one of those lookups returned null.
|
|
896
|
+
`Utils.getElementById(el, id)` resolves against `el.getRootNode()`.
|
|
897
|
+
|
|
898
|
+
- **Two ancestor walks stepped onto a shadow root and threw.** `Menu` looks for
|
|
899
|
+
the nearest clipping ancestor with `getComputedStyle`, which rejects a
|
|
900
|
+
non-Element, so a menu whose container was a shadow root could not open;
|
|
901
|
+
`Lightbox` read `.style` on the same node. Both now step over the root to its
|
|
902
|
+
host, so the clipping ancestors above it are still found. `Lightbox` also put
|
|
903
|
+
every ancestor back to `''` rather than to the value it found, discarding an
|
|
904
|
+
author's inline `overflow` the first time a lightbox opened under it — it now
|
|
905
|
+
records and restores the original.
|
|
906
|
+
|
|
907
|
+
- **A menu opened from a trigger narrower than 112dp is sized to its own
|
|
908
|
+
content.** `constrainWidth` wrote the trigger's width onto the surface and the
|
|
909
|
+
surface's own `min-width: 112px` then overrode it, so any trigger under that
|
|
910
|
+
floor left the menu at exactly 112dp with every item inside it wrapped. An
|
|
911
|
+
icon button is 40dp and a split button's chevron is 48dp, so both were always
|
|
912
|
+
in that state. The constraint now applies only to a trigger wide enough to
|
|
913
|
+
mean something; a wider trigger still constrains as before.
|
|
914
|
+
|
|
915
|
+
- **The renames reached the styling and stopped short of the behaviour.** The
|
|
916
|
+
Sass alias made `.navigation-drawer-fixed` *look* docked while the component
|
|
917
|
+
still read only `sidenav-fixed`, so at the Expanded breakpoint a canonically
|
|
918
|
+
named drawer kept its drag target live and `open()` could turn it into a
|
|
919
|
+
modal. `_host()` in Slider knew only the legacy classes, so a dual-handle
|
|
920
|
+
control in a `.slider` host never clamped its handles and never tracked the
|
|
921
|
+
interval.
|
|
922
|
+
- **A circular indicator also matched the bare linear-progress rule**, drawing
|
|
923
|
+
a linear fill over the conic one — `<span class="progress circular">` is not
|
|
924
|
+
a `<progress>` and has no determinate child, so it fell through to the
|
|
925
|
+
fallback.
|
|
926
|
+
- **Four documented snippets kept the old names in script** — `.sidenav`
|
|
927
|
+
queries that matched nothing, and `Expressive.Slider.init` still being used
|
|
928
|
+
to start slideshows after `Slider` became the range control. A new check
|
|
929
|
+
reads every `Expressive.X` and every selector string in the docs and fails on
|
|
930
|
+
a name the bundle or the sheet does not have.
|
|
931
|
+
- **`.fab` was not excluded from the toolbar selector** the way
|
|
932
|
+
`.fixed-action-btn` is, so a FAB-to-toolbar transition written with the new
|
|
933
|
+
name picked up toolbar styling. Found by the rename test itself, which walks
|
|
934
|
+
every rule naming an old class and fails if the new one is missing from it.
|
|
935
|
+
- **`llm.md` documented a token that does not exist** — `--sidenav-width`. The
|
|
936
|
+
real one is `--md-comp-nav-drawer-width`.
|
|
937
|
+
- **The top app bar told an icon from a label by its element.** The 48dp
|
|
938
|
+
circular target was keyed on `<i>`, and the canonical icon is a
|
|
939
|
+
`<span class="material-symbols">` — so every bar written the way the docs
|
|
940
|
+
teach, this site's own header included, lost the rule outright. The leading
|
|
941
|
+
action fell through to the generic button pill (72×40, tonal fill), the
|
|
942
|
+
trailing icon links were swept up by the *text-destination* rule and drawn at
|
|
943
|
+
`label-large` with 12dp padding, and the headline sat 76px from the edge
|
|
944
|
+
instead of the spec's 56. Keyed on the shared `$icon` list now, the way
|
|
945
|
+
`_buttons` and `_toolbar` already were.
|
|
946
|
+
- **Five more tokens were declared or referenced but not both**, found by the
|
|
947
|
+
new check below. The time picker's AM/PM buttons referenced Materialize's
|
|
948
|
+
`--btn-padding` and `--btn-border-radius`, which no longer exist — the
|
|
949
|
+
declarations were invalid at computed-value time, which does *not* fall back
|
|
950
|
+
to the rule underneath, so the padding rendered as 0 while a base button rule
|
|
951
|
+
offered 24px. They now say the 0 they have always rendered as. The checkbox
|
|
952
|
+
and radio `state-layer-size` tokens were declared and read by nothing, with
|
|
953
|
+
the 40dp geometry written out as pre-computed `11px` / `10px` shadow spreads;
|
|
954
|
+
the spreads derive from the tokens now, to the same pixel.
|
|
955
|
+
|
|
956
|
+
- **The app bar's trailing icon token was declared and used by nothing.**
|
|
957
|
+
Both icon actions took the *leading* token, so the documented way to opt into
|
|
958
|
+
the spec's muted trailing icons changed nothing. The two are told apart the
|
|
959
|
+
way the rest of the bar is — by DOM order, leading before the headline and
|
|
960
|
+
trailing after it — and each token now colours its own side.
|
|
961
|
+
- **The medium and large app bars missed their spec insets**, and a
|
|
962
|
+
title-only one put the headline where the icons belong. The expanded
|
|
963
|
+
headline sat 20dp from the inline edge rather than 16, medium left 16dp
|
|
964
|
+
above the container bottom rather than 20, and the icon row sat 4dp high
|
|
965
|
+
instead of centred in the 64dp row the bar collapses to. Separately, a bar
|
|
966
|
+
with no icons has a single flex line, and `align-content: space-between`
|
|
967
|
+
packs one line to the *top* — so the headline rose to the icon row. The top
|
|
968
|
+
row now has a height of its own and the headline a basis that cannot share
|
|
969
|
+
a line with it.
|
|
970
|
+
- **Ten more components had the same defect as the app bar above.** The
|
|
971
|
+
canonical icon is `<span class="material-symbols">`, but the navigation bar,
|
|
972
|
+
dialog, panes, list, tabs, pagination, side sheet, toolbar, menu and
|
|
973
|
+
fieldset legend each still asked `> i`. Nothing warned; the rules simply stopped applying —
|
|
974
|
+
the navigation bar lost its active-indicator pill, a tab with an icon kept
|
|
975
|
+
the short container, menu icons rendered at 24dp instead of 20dp.
|
|
976
|
+
|
|
977
|
+
Three of them broke twice over, because the rule on the *other* side of the
|
|
978
|
+
question over-matched instead. `:has(> i:only-child)` failing meant the
|
|
979
|
+
dialog's close button fell into the `:not(:has(…))` **text button** branch
|
|
980
|
+
and rendered as an auto-width 40dp pill rather than a 48dp circle; in a
|
|
981
|
+
list, `> span { grid-column: 2 }` swallowed the leading icon
|
|
982
|
+
and drew it inside the text column. Icons now go through `$icon` and labels
|
|
983
|
+
through `$icon-label`, which is what those variables are for.
|
|
984
|
+
- **A labelled header action was squeezed into the icon-button circle.**
|
|
985
|
+
`:only-child` counts elements, not text nodes, so
|
|
986
|
+
`<button><span icon/> Save</button>` reads as icon-only and took the 48dp
|
|
987
|
+
circle with its label overflowing — while the complementary
|
|
988
|
+
`:not(:has(…))` branch excluded it from the text-button treatment, leaving
|
|
989
|
+
no way to write it at all. CSS cannot see the text node, so `.button` is now
|
|
990
|
+
the author's opt-out on dialog and pane headers, matching the escape hatch
|
|
991
|
+
the app bar already had. Writing the label as its own `<span>` works too, and
|
|
992
|
+
always did.
|
|
993
|
+
- **The navigation bar's active indicator was the navigation rail's.**
|
|
994
|
+
`md.comp.navigation-bar.active-indicator.width` is 64dp and
|
|
995
|
+
`md.comp.navigation-rail.active-indicator.width` is 56dp; both were 56 here.
|
|
996
|
+
The rail is unchanged; the bar is now 64×32.
|
|
997
|
+
- **The remaining four components, and the cascade bug hiding underneath
|
|
998
|
+
them.** `_buttons`, `_navigation-rail`, `_snackbar` and `forms/_slider` asked
|
|
999
|
+
`> i` the same way. Two failed harder than the rest: the navigation rail
|
|
1000
|
+
hides its FAB *label* with `> :not(i)`, so once the icon became a `<span>`
|
|
1001
|
+
that rule hid the icon instead and the FAB rendered as an empty 56dp box;
|
|
1002
|
+
and the FAB toolbar's collapsed actions kept `opacity: 0`.
|
|
1003
|
+
|
|
1004
|
+
Fixing `_buttons` exposed a second defect. A component icon rule and the base
|
|
1005
|
+
`.material-symbols` rule both carry a class, so they tie on specificity and
|
|
1006
|
+
source order alone decides — and `icons-material-design` was forwarded
|
|
1007
|
+
*after* `buttons`, `list` and `badges`. Those three silently lost the tie, so
|
|
1008
|
+
a button drew an 18px box around a 24px glyph. Icons now load first, which is
|
|
1009
|
+
the order every other component already had. **A standard button's icon is
|
|
1010
|
+
now 18dp** (`--md-comp-filled-button-icon-size`) rather than the 24dp font
|
|
1011
|
+
default; circle, FAB and extended buttons stay 24dp. `list` and `badges` are
|
|
1012
|
+
unchanged — their tokens already matched the default.
|
|
1013
|
+
|
|
1014
|
+
### Added
|
|
1015
|
+
|
|
1016
|
+
- **A token liveness check** (`tests/tokens.test.js`). Two failure modes, both
|
|
1017
|
+
silent — no syntax error, nothing in a browser console. A `var()` naming a
|
|
1018
|
+
token nothing declares makes its whole declaration invalid at computed-value
|
|
1019
|
+
time: it still wins the cascade and *then* resolves to unset, so the property
|
|
1020
|
+
falls to inherited-or-initial rather than to the rule underneath. And a token
|
|
1021
|
+
declared and read by nobody is a promise the sheet does not keep. Only
|
|
1022
|
+
`--md-comp-*` is checked for being read — `--md-sys-*` and `--md-ref-*` are
|
|
1023
|
+
the public palette, where 111 unread ramp entries are the point. Unread
|
|
1024
|
+
component tokens can be exempted with a stated reason, and the check fails on
|
|
1025
|
+
a stale exemption too, so the list can only shrink.
|
|
1026
|
+
|
|
1027
|
+
- **The sweep is finished — 45 of 45 components enforced.** The last pass takes
|
|
1028
|
+
the remaining 23: icons, badges, buttons, cards, toolbar, list, tooltip,
|
|
1029
|
+
preloader, dialog, panes, carousel, parallax, lightbox, and the ten that
|
|
1030
|
+
state no markup of their own and now say so in writing.
|
|
1031
|
+
- Two ways to account for a composite role a component keeps out of its markup.
|
|
1032
|
+
**Conformance debt** (`conformance`) is a promise deferred until the code can
|
|
1033
|
+
keep it — Tabs and Toolbar withhold `tablist` and `toolbar` pending a keyboard
|
|
1034
|
+
model. A **rejected role** (`rejects`) is a promise declined: Carousel's
|
|
1035
|
+
keyboard contract is implemented and tested, and it writes the ARIA carousel
|
|
1036
|
+
pattern instead, so `tablist` is not owed. Both link to the rule that enforces
|
|
1037
|
+
them, and the suite refuses either without the other in both directions, so a
|
|
1038
|
+
role cannot be withheld silently and debt cannot be recorded without teeth.
|
|
1039
|
+
- A fourth rule kind, **`forbid-composite-roles`**. The rule states the
|
|
1040
|
+
component's own selector and the checker expands it over the composite-role
|
|
1041
|
+
vocabulary, so the ten roles are named once rather than once per component and
|
|
1042
|
+
adding one tightens every such rule. It replaced two hand-written rules that
|
|
1043
|
+
each named a single role and left the other nine legal.
|
|
1044
|
+
- A third rule kind, **`require-accessible-name`**. It is the one question a
|
|
1045
|
+
selector cannot ask: whether a control ends up with a name depends on text
|
|
1046
|
+
*nodes*, and CSS cannot see them — `:has(> .icon:only-child)` counts
|
|
1047
|
+
elements, so it flags `<a><span icon/>Five</a>`, a link that is perfectly
|
|
1048
|
+
well named. The check reads the content instead.
|
|
1049
|
+
- **Navigation is the third sweep** — `landmarks`, `navbar`, `navigation-bar`,
|
|
1050
|
+
`navigation-rail`, `sidenav`, `breadcrumb`, `pagination`, `tabs`, `menu`,
|
|
1051
|
+
`table_of_contents`, `page-footer`. **22 of 45 components are now enforced;
|
|
1052
|
+
23 remain.**
|
|
1053
|
+
- **`m3-guidelines.md` is checked**, against `fragmentSafe` rules only. It
|
|
1054
|
+
states markup as inline code spans in prose rather than as fenced examples,
|
|
1055
|
+
so a rule may run against it only if it fires on a *wrong thing present*. A
|
|
1056
|
+
fragment cannot be wrong about what it omits — omitting is what a fragment
|
|
1057
|
+
is for.
|
|
1058
|
+
- `landmarks`, a component row for the cross-cutting rules that belong to no
|
|
1059
|
+
single component: every `<nav>` carries a name, and `<main>` does not nest.
|
|
1060
|
+
- **Forms are the second sweep** — `input-fields`, `fieldset`, `checkboxes`,
|
|
1061
|
+
`radio-buttons`, `switches`, `select`, `file-input`, `range`, plus
|
|
1062
|
+
`autocomplete` and `character-counter`. **11 of 44 components are now
|
|
1063
|
+
enforced; 33 remain.**
|
|
1064
|
+
- `Autocomplete` announces itself. The input is a `combobox` with
|
|
1065
|
+
`aria-autocomplete="list"` and `aria-controls`; the suggestion list is a
|
|
1066
|
+
`listbox`; every entry is an `option` with an id and `aria-selected`; and
|
|
1067
|
+
arrowing sets `aria-activedescendant`. It previously had **none** of these -
|
|
1068
|
+
the highlight moved with a class nothing but a sighted user could follow.
|
|
1069
|
+
Rule 2 permits the roles because the keyboard contract was already there.
|
|
1070
|
+
- `CharacterCounter` writes into a polite, atomic live region, so the remaining
|
|
1071
|
+
count is announced instead of silently running out.
|
|
1072
|
+
- A test fails any markup sample in `llm.md` fenced as ` ```text ` instead of
|
|
1073
|
+
` ```html `. The extractor keys on the tag, so 37 blocks - including every
|
|
1074
|
+
Fieldsets example - were markup no check could see.
|
|
1075
|
+
- **`SEMANTICS.md`**, the normative per-component markup standard, generated
|
|
1076
|
+
from `semantics.json` by `npm run build:semantics`. `CONTEXT.md` defines the
|
|
1077
|
+
vocabulary it uses.
|
|
1078
|
+
- **`tests/semantics.test.js`** enforces it against all three surfaces that
|
|
1079
|
+
state markup: `llm.md`, `docs/templates/**` and `tests/fixtures.js`. A
|
|
1080
|
+
component is `enforced` or `exempt`; the exempt list is the remaining backlog
|
|
1081
|
+
and only shrinks. A component cannot be added without a row, so anything new
|
|
1082
|
+
is checked from its first commit. A single example can opt out with a stated
|
|
1083
|
+
reason (` ```html ignore-semantics: why `, or `code(check=false, reason=…)`).
|
|
1084
|
+
- `Chips` gains an `i18n` option (`{ remove: 'Remove' }`) for the delete
|
|
1085
|
+
button's accessible name.
|
|
1086
|
+
|
|
1087
|
+
### Changed
|
|
1088
|
+
|
|
1089
|
+
- **An icon is a `<span>`, and it says whether it is decoration or an image.**
|
|
1090
|
+
465 `<i class="material-icons">` became
|
|
1091
|
+
`<span class="material-symbols" aria-hidden="true">`. `<i>` means idiomatic
|
|
1092
|
+
text and an icon is none of those; more to the point the glyph comes from a
|
|
1093
|
+
ligature, so the icon's content is real text and was being read out — every
|
|
1094
|
+
icon-only button announced itself twice, as "add, Add". An icon is now
|
|
1095
|
+
either `aria-hidden` with the control carrying the name, or `role="img"`
|
|
1096
|
+
with a label of its own.
|
|
1097
|
+
- **Hiding every icon exposed the controls that had nothing else.** 47
|
|
1098
|
+
icon-only links and buttons had no accessible name at all once the glyph
|
|
1099
|
+
stopped being read; they carry one now.
|
|
1100
|
+
- **A badge nested in an icon is hidden with it.** `aria-hidden` covers the
|
|
1101
|
+
subtree and `aria-hidden="false"` on a descendant does not undo that, so a
|
|
1102
|
+
count that lived only inside a decorative icon was a count nobody heard.
|
|
1103
|
+
Inside a control the name carries it (`aria-label="Inbox, 3 unread"`);
|
|
1104
|
+
standing alone the icon becomes the image.
|
|
1105
|
+
- A **toolbar is a `<div class="toolbar">`**, not a `<nav>` — it holds
|
|
1106
|
+
commands, not destinations — and not `role="toolbar"` either, which is a
|
|
1107
|
+
composite role promising arrow keys nothing here implements.
|
|
1108
|
+
- A **card's action row is a `<div class="actions">`**. A row of buttons is not
|
|
1109
|
+
a landmark, and one per card floods the landmark list.
|
|
1110
|
+
- A **`<dialog>` is named**, by `aria-labelledby` pointing at the heading it
|
|
1111
|
+
already has. Including the one `Sidenav` builds: it copies the drawer's name
|
|
1112
|
+
onto the dialog, because once that dialog opens modally the `<nav>` holding
|
|
1113
|
+
the label is outside it.
|
|
1114
|
+
- A **`<div class="progress">` reports progress** with `role="progressbar"`, and
|
|
1115
|
+
a determinate one reports `aria-valuenow` too — a progressbar with no value
|
|
1116
|
+
*is* an indeterminate one, which is a lie if the bar visibly shows 70%.
|
|
1117
|
+
`<progress>` reports itself; a div drawn with CSS reports nothing.
|
|
1118
|
+
- A **CSS-only `.tooltip` is referenced with `aria-describedby`.** Inside its
|
|
1119
|
+
control it was swallowed — with an `aria-label` present the label wins and
|
|
1120
|
+
the tooltip was never announced at all.
|
|
1121
|
+
- **`.lightboxed` images are operable.** They open an overlay on click, which
|
|
1122
|
+
makes them controls, and a bare `<img>` is not focusable. The keypress
|
|
1123
|
+
handler was already there; only `tabindex` and a role were missing.
|
|
1124
|
+
- `aria-selected` came off plain `<li>`, and a pane is a `<section>`, not a
|
|
1125
|
+
second `<main>`.
|
|
1126
|
+
- **Every `<nav>` now carries a name, and things that are not navigation
|
|
1127
|
+
stopped claiming to be.** A row of radios or checkboxes is a
|
|
1128
|
+
`<div class="inline">`, not a `<nav>` — it was a flex hook wearing a
|
|
1129
|
+
landmark. The icons page used `<nav>` to mean "any element that inherits";
|
|
1130
|
+
that is a `<div>`. A footer column with no links is a `<section>`. A top app
|
|
1131
|
+
bar that holds only a title and controls — as the docs chrome does — is a
|
|
1132
|
+
`.bar`, because an empty navigation landmark is worse than none.
|
|
1133
|
+
- **Breadcrumbs are an ordered list.** `<nav aria-label="Breadcrumb"><ol>`,
|
|
1134
|
+
with `aria-current="page"` on the last crumb. The flat run of
|
|
1135
|
+
`<a class="breadcrumb">` gave no count and no position. The Sass already
|
|
1136
|
+
supported the `<ol>` form; only the documentation was behind.
|
|
1137
|
+
- **Pagination lives in a labelled `<nav>`** with `<ol>`, `aria-current="page"`
|
|
1138
|
+
on the current link, and a `<span>` rather than an `<a href>` for a disabled
|
|
1139
|
+
prev/next — a disabled link is still focusable and still navigates.
|
|
1140
|
+
- Tabs, the navigation bar and the navigation rail mark the active destination
|
|
1141
|
+
with `aria-current="page"`, not just a class.
|
|
1142
|
+
- A `<menu>` separator is `<li class="divider" role="separator">`. `<menu>` is
|
|
1143
|
+
a list and its content model permits only `<li>`, so a bare `<hr>` between
|
|
1144
|
+
entries was invalid. It still renders.
|
|
1145
|
+
- The sidenav and the table of contents sit inside a labelled `<nav>`.
|
|
1146
|
+
- **Form markup now says what it is.** `.field` is the container; `.input-field`
|
|
1147
|
+
never was one (the only rule matching it is `.chips.input-field`). Field icons
|
|
1148
|
+
are `<span class="material-symbols" aria-hidden="true">` and name their side
|
|
1149
|
+
with `prefix` or `suffix`. Supporting text carries an `id` and the control
|
|
1150
|
+
points at it with `aria-describedby`, or it is never read out with the field.
|
|
1151
|
+
Radios live in a `<fieldset>` with a `<legend>` naming the question. A file
|
|
1152
|
+
input is a `<label>` pointing at the input rather than a `<button>` wrapping
|
|
1153
|
+
it, and the path field is `readonly`. Switch on/off captions are
|
|
1154
|
+
`aria-hidden`, because as bare text they made the switch announce itself as
|
|
1155
|
+
"Off On".
|
|
1156
|
+
- **Chips are the first component swept.** Each kind of chip is now written
|
|
1157
|
+
with the element that says what it is — the four Material 3 types plus a
|
|
1158
|
+
non-interactive display chip, across three root elements: display is
|
|
1159
|
+
`<span class="chip">`, assist and suggestion are
|
|
1160
|
+
`<button type="button" class="chip">`, filter is
|
|
1161
|
+
`<input type="checkbox" class="chip-input">` + `<label class="chip">`, and an
|
|
1162
|
+
input chip is a `<span class="chip">` holding a
|
|
1163
|
+
`<button type="button" class="close" aria-label>`. A filter chip's state is
|
|
1164
|
+
`:checked`, so it needs no JavaScript.
|
|
1165
|
+
- Selection is the `selected` class rather than `:focus`, which previously had
|
|
1166
|
+
to stand in for it. `active` still works.
|
|
1167
|
+
- Icons inside a chip are `<span class="material-symbols" aria-hidden="true">`.
|
|
1168
|
+
The ligature is real text and was being read out next to the label.
|
|
1169
|
+
- The icon modifiers `.left`, `.right`, `.tiny`, `.small`, `.medium` and
|
|
1170
|
+
`.large` no longer depend on the element being `<i>`. They were keyed on `i`
|
|
1171
|
+
in `base/_global.scss`, so writing an icon as a `<span>` silently dropped
|
|
1172
|
+
them; `.left` and `.right` moved onto the icon class list, where the sizes
|
|
1173
|
+
already were.
|
|
1174
|
+
|
|
1175
|
+
### Fixed
|
|
1176
|
+
|
|
1177
|
+
- **The footer's legal bar was unstyled whenever the footer used `.container`.**
|
|
1178
|
+
`.container` is a supported columns wrapper — the column rules already reach
|
|
1179
|
+
through it — but the copyright-bar selector only matched a direct
|
|
1180
|
+
`footer > small`, so the documented grid layout produced a bare copyright
|
|
1181
|
+
row. The docs' own chrome had it on every page.
|
|
1182
|
+
- **The docs taught four classes that do not exist.** `.page-footer` and
|
|
1183
|
+
`.footer-copyright` were described as aliases that "stay" — nothing in the
|
|
1184
|
+
sheet matches either; `carousel-slider` was said to still enable flat mode,
|
|
1185
|
+
but only `.flat` and `fullWidth` are read; and `btn-large`, removed three
|
|
1186
|
+
releases ago, was still used in a waves example. `.prev` / `.next` are
|
|
1187
|
+
accepted but inert, and the responsive-pagination prose still described the
|
|
1188
|
+
10%/80% split they used to drive, which `.nowrap` scrolling replaced.
|
|
1189
|
+
- **`m3-guidelines.md` declared `nav.toolbar` five times** — a shape
|
|
1190
|
+
`semantics.json` forbids — plus the toolbars page prose, which still called
|
|
1191
|
+
the bar a `<nav>` after the markup beside it had been changed. The fragment
|
|
1192
|
+
reader could not see any of it: it looks for `<tag …>` forms, and a
|
|
1193
|
+
selector-style mention in a sentence or an anatomy table has no angle
|
|
1194
|
+
brackets. A check now derives every `tag.class` shape from the fragment-safe
|
|
1195
|
+
forbid rules and fails when a document names one, allowing a deliberate
|
|
1196
|
+
negation so a rename can still be recorded.
|
|
1197
|
+
- **Icon anatomy prose omitted `aria-hidden`** on three pages while the next
|
|
1198
|
+
sentence claimed the icon was hidden — so a reader following it would build
|
|
1199
|
+
a button announced as "add Create".
|
|
1200
|
+
- **Prose lagged the sweeps.** The code blocks were rewritten mechanically and
|
|
1201
|
+
the sentences around them were not: a dozen places still called the icon an
|
|
1202
|
+
`<i>`, cards still described a `<nav>` action row, lists and menus still
|
|
1203
|
+
taught `aria-selected` on a plain `<li>`, and the pickers told you to wrap an
|
|
1204
|
+
input in `.input-field` — four sections after `llm.md` says that class is not
|
|
1205
|
+
the container and never was.
|
|
1206
|
+
- **A docs page could carry 31 landmarks called "Main."** Every app bar demo on
|
|
1207
|
+
a page is a specimen, and they all had the canonical label. The live demos
|
|
1208
|
+
are named by their section now; the samples a reader copies keep the plain
|
|
1209
|
+
name.
|
|
1210
|
+
- **Off-screen carousel slides were hidden but still focusable.** A slide is an
|
|
1211
|
+
`<a>`, and `aria-hidden` alone left the tab stop in place — focus landed on a
|
|
1212
|
+
link the user had no way to perceive. They are `inert` now, which removes
|
|
1213
|
+
them from the tab order, from assistive technology and from hit-testing.
|
|
1214
|
+
- **The drawer wrapper became a second app bar.** Wrapping the sidenav in a
|
|
1215
|
+
`<nav>` while it still lived inside `<header>` made it a direct child of an
|
|
1216
|
+
app bar host, so the navbar rules gave it `display: flex`, full width and a
|
|
1217
|
+
64dp minimum — an empty bar-sized band under the real one, on every docs
|
|
1218
|
+
page. The drawer is a sibling of the bar now, which is what
|
|
1219
|
+
`m3-guidelines.md` said all along: the trigger may live in the bar, the
|
|
1220
|
+
drawer must not.
|
|
1221
|
+
- **A `header.fixed` whose bar is a `.bar` kept its looks but lost its
|
|
1222
|
+
behaviour.** The fixed selector still required a `nav` child, so such a
|
|
1223
|
+
header got the app bar's appearance without `position: sticky`, its z-index
|
|
1224
|
+
or the scroll-fill.
|
|
1225
|
+
- **A row of checkboxes stacked vertically.** `_radio-buttons.scss` learned
|
|
1226
|
+
`.inline`; `_checkboxes.scss` did not, and each label is `display: flex`.
|
|
1227
|
+
- **Tabs announced the wrong tab as current.** `aria-current` was written into
|
|
1228
|
+
the markup and then never moved: the class changed on click, the attribute
|
|
1229
|
+
did not. Its value changes with interaction, so maintaining it is the
|
|
1230
|
+
component's job — `Tabs` now moves the class and the attribute together, on
|
|
1231
|
+
click and on a hash-selected init.
|
|
1232
|
+
- **`.bar` escaped its selector and styled every element that used it.** The
|
|
1233
|
+
new app bar host was interpolated as a bare comma-separated list, so
|
|
1234
|
+
`header.medium > #{$_bar}` expanded to `header.medium > nav:not(…), .bar` —
|
|
1235
|
+
a top-level `.bar` rule applying app bar layout page-wide. Wrapping the
|
|
1236
|
+
variable in `:is()` keeps it one compound selector, and the specificity
|
|
1237
|
+
where it was. The existing app bar tests caught it.
|
|
1238
|
+
- **A field icon written as a `<span>` inherited the floating label's
|
|
1239
|
+
position** - 16px from the edge with `pointer-events: none`, i.e. indented
|
|
1240
|
+
past its own padding. Five hand-copied "everything that is not the label"
|
|
1241
|
+
exclusion lists had already drifted from each other; they are now one
|
|
1242
|
+
`$_not-label` variable, which is what stops the sixth from being missed.
|
|
1243
|
+
- The field icon modifiers were keyed on `i` with `:first-of-type` /
|
|
1244
|
+
`:last-of-type`. Those count elements of the same *type*, so they could not
|
|
1245
|
+
be widened to spans at all - a field is full of other spans. The documented
|
|
1246
|
+
markup names the side instead; the positional `i` rules stay for old markup.
|
|
1247
|
+
- **The file input trigger is a `<label>` wrapping its control**, not a label
|
|
1248
|
+
pointing at a sibling. `label.button` joins the button selector list, and the
|
|
1249
|
+
floating-label rule now excludes it — without both, the trigger rendered as
|
|
1250
|
+
invisible, unclickable field furniture. Wrapping also scopes the invisible
|
|
1251
|
+
input to the button, so the picker no longer opens from a click anywhere in
|
|
1252
|
+
the field.
|
|
1253
|
+
- **The trailing field icon swallowed clicks.** Excluding the icons from the
|
|
1254
|
+
floating-label rule took `pointer-events: none` with it; `.prefix` restated
|
|
1255
|
+
it, `.suffix` did not.
|
|
1256
|
+
- **`aria-selected` no longer doubles as the highlight.** Active and selected
|
|
1257
|
+
are different things: folding the arrow-key highlight into `aria-selected`
|
|
1258
|
+
announced a committed multi-select choice as unselected the moment the user
|
|
1259
|
+
arrowed past it. The highlight is `aria-activedescendant` alone.
|
|
1260
|
+
- **`Autocomplete.destroy()` left a timer running.** `open()` defers
|
|
1261
|
+
`menu.open()` to `setTimeout(0)`; destroying in the same tick let it fire
|
|
1262
|
+
against a menu whose element was gone. It surfaced only as an uncaught
|
|
1263
|
+
error *after* the test had passed. Calling `open()` twice before the callback
|
|
1264
|
+
fired also orphaned the first timer, which `destroy()` could not then cancel.
|
|
1265
|
+
- **A rendered chip was a `<div tabindex="0">` with no role and no accessible
|
|
1266
|
+
name** — in the tab order, announcing nothing. The chip is no longer a
|
|
1267
|
+
control; its delete button is, and that button carries the name.
|
|
1268
|
+
- The chip delete affordance was an `<i class="close">`: not focusable, not
|
|
1269
|
+
labelled, and unreachable without a mouse. It is now a real
|
|
1270
|
+
`<button type="button">`.
|
|
1271
|
+
- `.chip:focus` set `outline: none`, which would have removed the focus
|
|
1272
|
+
indicator from the newly focusable chips. Focus now goes through the
|
|
1273
|
+
`focus-visible` mixin.
|
|
1274
|
+
- `closeIconClass` was documented with a default of `'material-icons'`; the
|
|
1275
|
+
code has always defaulted to `'material-symbols'`.
|
|
1276
|
+
|
|
1277
|
+
### Migration
|
|
1278
|
+
|
|
1279
|
+
- `<i class="material-icons">add</i>` →
|
|
1280
|
+
`<span class="material-symbols" aria-hidden="true">add</span>`, and the
|
|
1281
|
+
control around it needs an `aria-label` if the icon was its only content.
|
|
1282
|
+
- `<nav class="toolbar">` → `<div class="toolbar">`.
|
|
1283
|
+
- A card's `<nav>` action row → `<div class="actions">`.
|
|
1284
|
+
- `<dialog>` → add `aria-labelledby` pointing at its heading.
|
|
1285
|
+
- `<div class="progress">` → add `role="progressbar"`, plus `aria-valuenow`
|
|
1286
|
+
when it is determinate.
|
|
1287
|
+
- `<img class="lightboxed">` → add `tabindex="0" role="button"`.
|
|
1288
|
+
- `<li aria-selected="true">` → `<li class="selected">`.
|
|
1289
|
+
- Every `<nav>` needs `aria-label` (or `aria-labelledby`).
|
|
1290
|
+
- `<nav>` used to lay out radios, checkboxes or switches →
|
|
1291
|
+
`<div class="inline">`.
|
|
1292
|
+
- `<nav class="breadcrumb-wrapper">` with `<a class="breadcrumb">` →
|
|
1293
|
+
`<nav aria-label="Breadcrumb"><ol><li><a>`, last crumb `aria-current="page"`.
|
|
1294
|
+
- `<ul class="pagination">` → `<nav class="pagination" aria-label="Pagination"><ol>`.
|
|
1295
|
+
The bare list still renders.
|
|
1296
|
+
- `<hr>` inside a `<menu>` → `<li class="divider" role="separator"></li>`.
|
|
1297
|
+
- `<ul class="sidenav">` → wrap it in `<nav aria-label="…">`.
|
|
1298
|
+
- A top app bar with no destinations → `<div class="bar">` instead of `<nav>`.
|
|
1299
|
+
- `<div class="input-field">` → `<div class="field">`.
|
|
1300
|
+
- `<i class="material-icons">place</i>` in a field →
|
|
1301
|
+
`<span class="material-symbols prefix" aria-hidden="true">place</span>`. The
|
|
1302
|
+
side is now required.
|
|
1303
|
+
- `<small>help</small>` in a field → give it an `id` and add
|
|
1304
|
+
`aria-describedby` to the control.
|
|
1305
|
+
- Radios → wrap the group in `<fieldset><legend>`.
|
|
1306
|
+
- `<button><input type="file"></button>` →
|
|
1307
|
+
`<label class="button" for="x">File</label><input id="x" type="file">`, and
|
|
1308
|
+
add `readonly` to `input.file-path`.
|
|
1309
|
+
- `<div class="chip">` → `<span class="chip">`, or `<button type="button"
|
|
1310
|
+
class="chip">` if it is actionable. A filter chip becomes a checkbox and its
|
|
1311
|
+
label.
|
|
1312
|
+
- `<i class="close material-icons">close</i>` →
|
|
1313
|
+
`<button type="button" class="close" aria-label="Remove …"><span
|
|
1314
|
+
class="material-symbols" aria-hidden="true">close</span></button>`.
|
|
1315
|
+
- `.chip.active` still works; `.chip.selected` is the name going forward.
|
|
1316
|
+
- Old chip markup still renders. It is no longer documented.
|
|
1317
|
+
|
|
8
1318
|
## [0.7.0] - 2026-08-21
|
|
9
1319
|
|
|
10
1320
|
Promotes 0.7.0-rc.0 to `latest`. The framework itself is **unchanged** from
|
|
@@ -183,7 +1493,9 @@ are no compatibility aliases.
|
|
|
183
1493
|
- `.tabs` nested in a header or app bar as a secondary row. `.tabs` is a
|
|
184
1494
|
standalone component; the `.tabs.transparent` app bar variant is gone with it.
|
|
185
1495
|
|
|
186
|
-
[Unreleased]: https://github.com/BaezFJ/ExpressiveCSS/compare/v0.
|
|
1496
|
+
[Unreleased]: https://github.com/BaezFJ/ExpressiveCSS/compare/v0.9.0...HEAD
|
|
1497
|
+
[0.9.0]: https://github.com/BaezFJ/ExpressiveCSS/compare/v0.8.0...v0.9.0
|
|
1498
|
+
[0.8.0]: https://github.com/BaezFJ/ExpressiveCSS/compare/v0.7.0...v0.8.0
|
|
187
1499
|
[0.7.0]: https://github.com/BaezFJ/ExpressiveCSS/compare/v0.6.0...v0.7.0
|
|
188
1500
|
[0.7.0-rc.0]: https://github.com/BaezFJ/ExpressiveCSS/compare/v0.6.0...v0.7.0-rc.0
|
|
189
1501
|
[0.6.0]: https://github.com/BaezFJ/ExpressiveCSS/compare/v0.5.0...v0.6.0
|