@rcarls/rc-theme-material 0.6.0 → 0.7.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +544 -0
  2. package/DESIGN.md +1399 -0
  3. package/README.md +88 -5
  4. package/bridge.css +132 -55
  5. package/components/accordion.css +43 -157
  6. package/components/adaptive-menu.css +47 -86
  7. package/components/app-bar.css +4 -2
  8. package/components/bottom-sheet.css +2 -2
  9. package/components/button.css +2 -1
  10. package/components/card.css +2 -0
  11. package/components/chip.css +12 -1
  12. package/components/combobox.css +61 -14
  13. package/components/dialog.css +27 -26
  14. package/components/disclosure.css +128 -84
  15. package/components/field.css +566 -0
  16. package/components/icons.css +9 -0
  17. package/components/list-item.css +65 -3
  18. package/components/listbox.css +5 -4
  19. package/components/markdown-editor.css +62 -35
  20. package/components/menu-button.css +4 -1
  21. package/components/menu.css +18 -31
  22. package/components/menubar.css +4 -1
  23. package/components/modes.css +22 -5
  24. package/components/navigation-bar.css +3 -5
  25. package/components/navigation-rail.css +5 -7
  26. package/components/popover.css +50 -0
  27. package/components/progress.css +5 -0
  28. package/components/range-slider.css +3 -2
  29. package/components/search-bar.css +12 -2
  30. package/components/segmented-button.css +2 -28
  31. package/components/select.css +99 -14
  32. package/components/splitter.css +4 -1
  33. package/components/switch.css +9 -1
  34. package/components/textarea.css +7 -3
  35. package/components/toolbar.css +2 -1
  36. package/components/tooltip.css +12 -0
  37. package/components/transfer-list.css +5 -1
  38. package/components/virtual-scroller.css +7 -0
  39. package/components.css +6 -1
  40. package/css-components/button.css +66 -0
  41. package/css-components/card.css +14 -0
  42. package/css-components/chip-group.css +9 -0
  43. package/css-components/chip.css +80 -0
  44. package/css-components/disclosure.css +25 -0
  45. package/css-components/list.css +34 -0
  46. package/css-components/select.css +20 -0
  47. package/css-components/textarea.css +22 -0
  48. package/css-components.css +31 -0
  49. package/defaults.css +189 -3
  50. package/native.css +258 -0
  51. package/package.json +20 -6
  52. package/state-layer.css +5 -1
package/DESIGN.md ADDED
@@ -0,0 +1,1399 @@
1
+ ---
2
+ version: material-3-baseline
3
+ name: rc-theme-material
4
+ description: >
5
+ Material Design 3 composition guide for building Android-style progressive
6
+ web apps from rc-webcomponents with the rc-theme-material theme. Values below
7
+ include the M3 baseline tokens the theme vendors in defaults.css and
8
+ composition guidance for spacing; an app that brings its own scheme
9
+ overrides the matching --md-sys-* variables.
10
+
11
+ colors:
12
+ # Light / dark pairs resolve through light-dark() on --md-sys-color-*.
13
+ primary: '#6750a4' # --md-sys-color-primary
14
+ on-primary: '#ffffff'
15
+ primary-container: '#eaddff'
16
+ on-primary-container: '#21005d'
17
+ secondary: '#625b71'
18
+ on-secondary: '#ffffff'
19
+ secondary-container: '#e8def8' # selection indicators, tonal buttons
20
+ on-secondary-container: '#1d192b'
21
+ tertiary: '#7d5260'
22
+ on-tertiary: '#ffffff'
23
+ tertiary-container: '#ffd8e4'
24
+ on-tertiary-container: '#31111d'
25
+ error: '#b3261e'
26
+ on-error: '#ffffff'
27
+ error-container: '#f9dedc'
28
+ on-error-container: '#410e0b'
29
+ surface: '#fdf8fd' # page, app bar at rest, rail
30
+ surface-container-lowest: '#ffffff'
31
+ surface-container-low: '#f8f2f8' # cards, sheets, disclosures, chips
32
+ surface-container: '#f2ecf2' # navigation bar, scrolled app bar
33
+ surface-container-high: '#ece6ec' # dialogs
34
+ surface-container-highest: '#e6e1e5' # filled text fields
35
+ on-surface: '#1c1b1f'
36
+ on-surface-variant: '#49454f'
37
+ outline: '#79747e'
38
+ outline-variant: '#cac4d0'
39
+ inverse-surface: '#313033' # snackbar, tooltip
40
+ inverse-on-surface: '#f4eff4'
41
+ inverse-primary: '#d0bcff'
42
+ scrim: '#000000'
43
+
44
+ primary-dark: '#d0bcff'
45
+ on-primary-dark: '#381e72'
46
+ primary-container-dark: '#4f378b'
47
+ secondary-container-dark: '#4a4458'
48
+ error-dark: '#f2b8b5'
49
+ surface-dark: '#141317'
50
+ surface-container-low-dark: '#1c1b1f'
51
+ surface-container-dark: '#211f23'
52
+ surface-container-high-dark: '#2b292e'
53
+ surface-container-highest-dark: '#363438'
54
+ on-surface-dark: '#e6e1e5'
55
+ on-surface-variant-dark: '#cac4d0'
56
+ outline-dark: '#938f99'
57
+ outline-variant-dark: '#49454f'
58
+
59
+ typography:
60
+ # --md-sys-typescale-<role>-<size>-{font,weight,size,line-height,tracking}
61
+ display-large:
62
+ fontFamily: Roboto, system-ui, sans-serif
63
+ fontSize: 57px
64
+ fontWeight: 400
65
+ lineHeight: 64px
66
+ letterSpacing: -0.25px
67
+ display-medium:
68
+ fontFamily: Roboto, system-ui, sans-serif
69
+ fontSize: 45px
70
+ fontWeight: 400
71
+ lineHeight: 52px
72
+ letterSpacing: 0
73
+ display-small:
74
+ fontFamily: Roboto, system-ui, sans-serif
75
+ fontSize: 36px
76
+ fontWeight: 400
77
+ lineHeight: 44px
78
+ letterSpacing: 0
79
+ headline-large:
80
+ fontFamily: Roboto, system-ui, sans-serif
81
+ fontSize: 32px
82
+ fontWeight: 400
83
+ lineHeight: 40px
84
+ letterSpacing: 0
85
+ headline-medium:
86
+ fontFamily: Roboto, system-ui, sans-serif
87
+ fontSize: 28px
88
+ fontWeight: 400
89
+ lineHeight: 36px
90
+ letterSpacing: 0
91
+ headline-small:
92
+ fontFamily: Roboto, system-ui, sans-serif
93
+ fontSize: 24px
94
+ fontWeight: 400
95
+ lineHeight: 32px
96
+ letterSpacing: 0
97
+ title-large:
98
+ fontFamily: Roboto, system-ui, sans-serif
99
+ fontSize: 22px
100
+ fontWeight: 400
101
+ lineHeight: 28px
102
+ letterSpacing: 0
103
+ title-medium:
104
+ fontFamily: Roboto, system-ui, sans-serif
105
+ fontSize: 16px
106
+ fontWeight: 500
107
+ lineHeight: 24px
108
+ letterSpacing: 0.15px
109
+ title-small:
110
+ fontFamily: Roboto, system-ui, sans-serif
111
+ fontSize: 14px
112
+ fontWeight: 500
113
+ lineHeight: 20px
114
+ letterSpacing: 0.1px
115
+ body-large:
116
+ fontFamily: Roboto, system-ui, sans-serif
117
+ fontSize: 16px
118
+ fontWeight: 400
119
+ lineHeight: 24px
120
+ letterSpacing: 0.5px
121
+ body-medium:
122
+ fontFamily: Roboto, system-ui, sans-serif
123
+ fontSize: 14px
124
+ fontWeight: 400
125
+ lineHeight: 20px
126
+ letterSpacing: 0.25px
127
+ body-small:
128
+ fontFamily: Roboto, system-ui, sans-serif
129
+ fontSize: 12px
130
+ fontWeight: 400
131
+ lineHeight: 16px
132
+ letterSpacing: 0.4px
133
+ label-large:
134
+ fontFamily: Roboto, system-ui, sans-serif
135
+ fontSize: 14px
136
+ fontWeight: 500
137
+ lineHeight: 20px
138
+ letterSpacing: 0.1px
139
+ label-medium:
140
+ fontFamily: Roboto, system-ui, sans-serif
141
+ fontSize: 12px
142
+ fontWeight: 500
143
+ lineHeight: 16px
144
+ letterSpacing: 0.5px
145
+ label-small:
146
+ fontFamily: Roboto, system-ui, sans-serif
147
+ fontSize: 11px
148
+ fontWeight: 500
149
+ lineHeight: 16px
150
+ letterSpacing: 0.5px
151
+
152
+ rounded:
153
+ # --md-sys-shape-corner-*
154
+ none: 0px
155
+ extra-small: 4px # snackbar, tooltip, inner corners of segmented lists and accordions
156
+ small: 8px # chips, menus
157
+ medium: 12px # cards
158
+ large: 16px # standalone disclosures, FAB
159
+ extra-large: 28px # dialogs, bottom sheet top corners, segmented group ends
160
+ full: 999px # buttons, icon buttons, navigation indicators
161
+
162
+ spacing:
163
+ # 4dp grid. Window margins follow the width class.
164
+ '1': 4px
165
+ '2': 8px
166
+ '3': 12px
167
+ '4': 16px # compact window margin, list inline padding
168
+ '6': 24px # medium+ window margin, dialog padding
169
+ pane-gap: 24px # between panes in list-detail and supporting-pane layouts
170
+ touch-target: 48px # minimum activation area (components reserve 3rem)
171
+
172
+ components:
173
+ button-filled:
174
+ backgroundColor: '{colors.primary}'
175
+ textColor: '{colors.on-primary}'
176
+ typography: '{typography.label-large}'
177
+ rounded: '{rounded.full}'
178
+ height: 40px
179
+ button-tonal:
180
+ backgroundColor: '{colors.secondary-container}'
181
+ textColor: '{colors.on-secondary-container}'
182
+ rounded: '{rounded.full}'
183
+ button-outlined:
184
+ textColor: '{colors.primary}'
185
+ borderColor: '{colors.outline-variant}'
186
+ rounded: '{rounded.full}'
187
+ button-text:
188
+ textColor: '{colors.primary}'
189
+ rounded: '{rounded.full}'
190
+ icon-button:
191
+ size: 40px
192
+ iconSize: 24px
193
+ rounded: '{rounded.full}'
194
+ fab:
195
+ backgroundColor: '{colors.primary-container}'
196
+ textColor: '{colors.on-primary-container}'
197
+ size: 56px
198
+ rounded: '{rounded.large}'
199
+ top-app-bar:
200
+ backgroundColor: '{colors.surface}'
201
+ scrolledBackgroundColor: '{colors.surface-container}'
202
+ typography: '{typography.title-large}'
203
+ height: 64px
204
+ navigation-bar:
205
+ backgroundColor: '{colors.surface-container}'
206
+ indicatorColor: '{colors.secondary-container}'
207
+ typography: '{typography.label-medium}'
208
+ navigation-rail:
209
+ backgroundColor: '{colors.surface}'
210
+ indicatorColor: '{colors.secondary-container}'
211
+ card:
212
+ backgroundColor: '{colors.surface-container-low}'
213
+ rounded: '{rounded.medium}'
214
+ padding: 16px
215
+ list-item:
216
+ minHeight: 56px
217
+ paddingInline: 16px
218
+ typography: '{typography.body-large}'
219
+ segmented-list:
220
+ itemBackgroundColor: '{colors.surface-container-low}'
221
+ gap: 2px
222
+ outerRadius: '{rounded.extra-large}'
223
+ innerRadius: '{rounded.extra-small}'
224
+ dialog:
225
+ backgroundColor: '{colors.surface-container-high}'
226
+ rounded: '{rounded.extra-large}'
227
+ padding: 24px
228
+ minWidth: 280px
229
+ maxWidth: 560px
230
+ headline: '{typography.headline-small}'
231
+ full-screen-dialog:
232
+ backgroundColor: '{colors.surface}'
233
+ rounded: '{rounded.none}'
234
+ header: top-app-bar
235
+ bottom-sheet:
236
+ backgroundColor: '{colors.surface-container-low}'
237
+ rounded: '{rounded.extra-large}'
238
+ handle: 32x4px
239
+ side-sheet:
240
+ backgroundColor: '{colors.surface-container-low}'
241
+ rounded: '{rounded.large}'
242
+ maxWidth: 400px
243
+ chip:
244
+ backgroundColor: '{colors.surface-container-low}'
245
+ selectedBackgroundColor: '{colors.secondary-container}'
246
+ typography: '{typography.label-large}'
247
+ rounded: '{rounded.small}'
248
+ height: 32px
249
+ text-field:
250
+ backgroundColor: '{colors.surface-container-highest}'
251
+ indicatorColor: '{colors.on-surface-variant}'
252
+ focusColor: '{colors.primary}'
253
+ typography: '{typography.body-large}'
254
+ snackbar:
255
+ backgroundColor: '{colors.inverse-surface}'
256
+ textColor: '{colors.inverse-on-surface}'
257
+ actionColor: '{colors.inverse-primary}'
258
+ rounded: '{rounded.extra-small}'
259
+ tooltip:
260
+ backgroundColor: '{colors.inverse-surface}'
261
+ textColor: '{colors.inverse-on-surface}'
262
+ rounded: '{rounded.extra-small}'
263
+ rich-tooltip:
264
+ backgroundColor: '{colors.surface-container}'
265
+ textColor: '{colors.on-surface-variant}'
266
+ rounded: '{rounded.medium}'
267
+ ---
268
+
269
+ # rc-theme-material design guide
270
+
271
+ This guide is for AI coding agents (and the people working with them)
272
+ building an app from rc-webcomponents with `@rcarls/rc-theme-material`. It
273
+ covers what importing the theme does not: which component fills which
274
+ Material 3 role, how components combine into an Android-style app, and how a
275
+ layout adapts across window sizes.
276
+
277
+ It is written from the Material 3 guidelines and Android's adaptive-app
278
+ guidance. Where the theme deliberately deviates, or does not yet cover a
279
+ pattern, the section says so. Attribute and token reference lives on the
280
+ [docs site](https://richardcarls.github.io/rc-webcomponents/); this guide
281
+ names only what composition needs.
282
+
283
+ ## Overview
284
+
285
+ Three owners, kept apart:
286
+
287
+ | Owner | Owns | Never owns |
288
+ | ----------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------- |
289
+ | rc-webcomponents | Semantics, ARIA, keyboard support, focus, events, structural layout | Color, elevation, decorative shape |
290
+ | rc-theme-material | Material geometry, color roles, typography, elevation, motion, state layers | Behavior, ARIA, which actions exist |
291
+ | Your app | Information architecture, routes, state, which component is used where, responsive mode switches | Component internals (`--_rc-*`, shadow DOM) |
292
+
293
+ Rules that follow from this:
294
+
295
+ - **Build on native children where required.** Components such as `rc-button`,
296
+ `rc-dialog`, `rc-disclosure` and `rc-field` wrap an authored `<button>`,
297
+ `<dialog>`, `<details>` or `<input>`. Keep required native children in the DOM
298
+ and label them; they carry the accessibility and form behavior. Containers
299
+ such as `rc-app-bar` and `rc-accordion` have different child contracts.
300
+ - **Style through tokens.** Adapt with `--md-sys-*` (system) and
301
+ `--rc-<component>-*` (component) properties, theme classes and documented
302
+ `data-*` hooks. Never target shadow internals or `--_rc-*` private tokens.
303
+ - **Switch modes from app state.** Window-size changes that alter behavior
304
+ (navigation bar to rail, full-screen to basic dialog, docked search to search
305
+ view) are attribute or property writes from your code, not CSS.
306
+
307
+ ## Setup beyond import
308
+
309
+ ### Entry points
310
+
311
+ ```css
312
+ /* Standalone: baseline light and dark scheme, bridge, component styles. */
313
+ @import '@rcarls/rc-theme-material/theme.css';
314
+ ```
315
+
316
+ ```css
317
+ /* Your app already defines --md-sys-* (for example from Material Theme Builder). */
318
+ @import '@rcarls/rc-theme-material/bridge.css';
319
+ @import '@rcarls/rc-theme-material/components.css';
320
+ ```
321
+
322
+ ```css
323
+ /* Only what you use. */
324
+ @import '@rcarls/rc-theme-material/bridge.css';
325
+ @import '@rcarls/rc-theme-material/components/button.css';
326
+ @import '@rcarls/rc-theme-material/components/dialog.css';
327
+ @import '@rcarls/rc-theme-material/state-layer.css';
328
+ ```
329
+
330
+ Native HTML can use the Material presentation without registering an RC
331
+ element. These are independent opt-ins and are not included by `theme.css`:
332
+
333
+ ```css
334
+ @import '@rcarls/rc-theme-material/defaults.css';
335
+ @import '@rcarls/rc-theme-material/bridge.css';
336
+ @import '@rcarls/rc-theme-material/native.css';
337
+ @import '@rcarls/rc-theme-material/css-components.css';
338
+ ```
339
+
340
+ `native.css` is a convenience layer for giving ordinary buttons, text fields,
341
+ selects, textareas, checkboxes, radios, ranges, and progress controls a Material
342
+ treatment throughout the theme scope. Component recipes style native children
343
+ when their presentation belongs to a themed menu, toolbar, or other component
344
+ context. Add `data-rc-native="off"` to opt an element and its descendants out of
345
+ the native layer. `css-components.css` provides
346
+ appearance and CSS-visible states for `.rc-button`, `.rc-chip`,
347
+ `.rc-chip-group`, `.rc-select`, `.rc-textarea`, `.rc-card`, `.rc-list`, and
348
+ `details.rc-disclosure`; each recipe is also exported selectively. These
349
+ recipes do not add behavior, events, popup replacement, or measured overflow.
350
+
351
+ To bring a brand scheme, generate one with Material Theme Builder and declare
352
+ its `--md-sys-color-*` values on the theme scope, after the import. Keep the
353
+ roles and their pairings; change only the values.
354
+
355
+ ### Scope and color scheme
356
+
357
+ The theme applies inside `.rc-theme-material`. Put it on `<html>` so dialogs,
358
+ sheets and popovers rendered in the top layer stay in scope:
359
+
360
+ ```html
361
+ <html class="rc-theme-material" lang="en">
362
+ <head>
363
+ <meta name="color-scheme" content="light dark" />
364
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
365
+ <meta name="theme-color" content="#fdf8fd" media="(prefers-color-scheme: light)" />
366
+ <meta name="theme-color" content="#141317" media="(prefers-color-scheme: dark)" />
367
+ </head>
368
+ </html>
369
+ ```
370
+
371
+ Colors resolve through `light-dark()`, so `color-scheme` picks light or dark.
372
+ For a user override, set `color-scheme: light` or `dark` on the scope; do not
373
+ swap stylesheets.
374
+
375
+ ### Cascade layers
376
+
377
+ The theme declares `rc-base`, then its `defaults`, `bridge` and `components`
378
+ layers. App CSS outside any layer outranks every layer, so app rules win without
379
+ `!important`. If your app uses layers, declare yours after the theme's.
380
+
381
+ ### Fonts and icons
382
+
383
+ Neither ships with the theme. Load Roboto (or your brand face, set through
384
+ `--md-sys-typescale-*-font` and `--md-ref-typeface-*`) and an icon font, then
385
+ give every icon marker the font in one rule:
386
+
387
+ ```css
388
+ :is(
389
+ [data-rc-icon],
390
+ [data-rc-button-icon],
391
+ [data-rc-button-selected-icon],
392
+ [data-rc-chip-icon],
393
+ [data-rc-list-leading],
394
+ [data-rc-menu-icon],
395
+ [data-rc-menu-leading],
396
+ [data-rc-navigation-icon],
397
+ [data-rc-navigation-collapse-icon],
398
+ [data-rc-navigation-expand-icon],
399
+ [data-rc-segmented-button-selected-icon]
400
+ ) {
401
+ font-family: 'Material Symbols Outlined';
402
+ }
403
+ ```
404
+
405
+ Without it, markers render their ligature text (`menu`, `close`). The rule
406
+ includes the list and menu leading markers, which hold icons in list rows and
407
+ in actions that `rc-adaptive-menu` promotes or overflows. Three
408
+ markers get no sizing from the theme, so give them a `font-size` as well:
409
+ `data-rc-navigation-collapse-icon`, `data-rc-navigation-expand-icon` and
410
+ `data-rc-segmented-button-selected-icon`. The rest follow
411
+ `--rc-icon-font-size` and `--rc-icon-font-line-height`.
412
+
413
+ Mark decorative icons `aria-hidden="true"` and put the accessible name on the
414
+ control.
415
+
416
+ ## Colors
417
+
418
+ - **Pair roles.** Text and icons on a role use its `on-*` partner:
419
+ `on-primary` on `primary`, `on-secondary-container` on
420
+ `secondary-container`. Never put `primary` text on `primary-container`.
421
+ - **One accent per view.** `primary` marks the single most important action
422
+ or state. `secondary-container` marks selection: navigation indicators,
423
+ selected chips, tonal buttons, pressed toolbar buttons. `tertiary` is for
424
+ contrasting accents, not a second primary.
425
+ - **Surfaces climb the container ladder** instead of casting shadows:
426
+
427
+ | Role | Use |
428
+ | --------------------------- | ------------------------------------------------------------------------- |
429
+ | `surface` | Page background, top app bar at rest, navigation rail, full-screen dialog |
430
+ | `surface-container-low` | Cards, bottom and side sheets, disclosures, segmented list items, chips |
431
+ | `surface-container` | Navigation bar, scrolled app bar, toolbars, menus' listbox |
432
+ | `surface-container-high` | Basic dialogs |
433
+ | `surface-container-highest` | Filled text fields |
434
+
435
+ Content inside a container uses a step **below** it, so a card on a dialog
436
+ is `surface-container-low`, never the dialog's own step (it would vanish).
437
+
438
+ - **Error is a tone, not a component.** Add `data-tone="danger"` to a button
439
+ variant for destructive actions; `rc-field` shows error state from native
440
+ validity or `invalid`.
441
+ - **Color never carries meaning alone.** Pair it with text, an icon or shape.
442
+ - **Forced colors** are handled by the theme with system colors. Do not
443
+ re-add custom colors inside `@media (forced-colors: active)`.
444
+
445
+ ## Typography
446
+
447
+ Use the type scale through `--md-sys-typescale-<role>-<size>-*`. Common
448
+ assignments:
449
+
450
+ | Element | Role |
451
+ | ------------------------------------ | --------------------------------------------------------- |
452
+ | Top app bar title | title-large (medium/large bars: headline-small/medium) |
453
+ | Dialog headline | headline-small |
454
+ | Section header above a list or group | title-small, `primary` or `on-surface-variant` |
455
+ | List item headline / supporting text | body-large / body-medium in `on-surface-variant` |
456
+ | Card title / subtitle | title-medium / body-medium |
457
+ | Buttons, chips, tabs | label-large |
458
+ | Navigation labels | label-medium |
459
+ | Body copy | body-large on compact, body-medium in dense desktop panes |
460
+
461
+ ```css
462
+ .section-heading {
463
+ margin: 0;
464
+ font: var(--md-sys-typescale-title-small-weight) var(--md-sys-typescale-title-small-size) /
465
+ var(--md-sys-typescale-title-small-line-height) var(--md-sys-typescale-title-small-font);
466
+ letter-spacing: var(--md-sys-typescale-title-small-tracking);
467
+ color: var(--md-sys-color-primary);
468
+ }
469
+ ```
470
+
471
+ Keep reading measure near 40–75 characters; widen panes, not lines.
472
+
473
+ For a reading region, add `data-rc-type-scale` and set a positive
474
+ `--rc-type-scale` factor. With `theme.css`, the Material type sizes and line
475
+ heights and the RC text tokens derived from them scale inside the region.
476
+ With `bridge.css` alone, RC text tokens scale from the application's Material
477
+ values while those Material tokens remain unchanged. A nested marked region
478
+ uses its own factor. The front matter above records the unscaled bundled
479
+ `*-value` sources; update those source tokens alongside branded final values.
480
+
481
+ ```html
482
+ <main data-rc-type-scale style="--rc-type-scale: 1.25">…</main>
483
+ ```
484
+
485
+ ## Layout and window size classes
486
+
487
+ ### Size classes
488
+
489
+ Use Android's window size classes, on both axes. They describe the window
490
+ the app has, not the device: split screen, resizable windows and folding
491
+ change them while the app runs, so never derive a device type from them.
492
+ Read them from the viewport in app code (`matchMedia`), expose them as state,
493
+ and publish them on the root element for CSS (see the snippet below):
494
+
495
+ | Width class | Range | Navigation | Typical layout |
496
+ | ----------- | ----------- | ------------------------------------- | ----------------------------------------------------------------- |
497
+ | Compact | < 600px | Navigation bar | Single pane, full-screen dialogs, bottom sheets |
498
+ | Medium | 600–839px | Navigation rail | Single pane, wider margins, basic dialogs |
499
+ | Expanded | 840–1199px | Navigation rail (optionally expanded) | List-detail, supporting pane |
500
+ | Large | 1200–1599px | Expanded rail | A fixed list pane, a bounded detail, an optional third pane |
501
+ | Extra-large | ≥ 1600px | Expanded rail | Three panes or more grid columns, capped measure, TV-safe margins |
502
+
503
+ | Height class | Range | Effect |
504
+ | ------------ | --------- | --------------------------------------------------------------------------------------------------------------- |
505
+ | Compact | < 480px | Landscape phone: prefer the navigation bar, move bottom actions to the inline end, dock panes instead of sheets |
506
+ | Medium | 480–899px | Standard |
507
+ | Expanded | ≥ 900px | More vertical room; do not stretch reading measure |
508
+
509
+ The window classes choose the shell; everything inside a pane adapts to the
510
+ pane through container queries (see
511
+ [Choosing the adaptive mechanism](#choosing-the-adaptive-mechanism)). Combine
512
+ the two axes like this:
513
+
514
+ | Shell decision | Rule |
515
+ | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
516
+ | Navigation | Bar when width **or** height is compact; rail otherwise; the expanded rail is offered from expanded |
517
+ | Primary route panes | One route page or fragment at compact width or compact height; list-detail begins at expanded width with medium height; a third may appear from large |
518
+ | App bar | Hides on scroll at compact height; a medium or large title starts compact there |
519
+ | FAB | In the rail's header when a rail shows; otherwise at the bottom inline end |
520
+
521
+ List-detail needs height as well as width: a phone in landscape is often
522
+ expanded width (915px and up) but has no room for the parent list beside the
523
+ detail, so it shows one route page or fragment with Back. A supporting content
524
+ column may still share that page when it can scroll with the primary content.
525
+
526
+ Android's default navigation suite shows a navigation bar when the width
527
+ **or** height is compact, and a rail otherwise
528
+ (`NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo` in the `androidx`
529
+ `material3-adaptive-navigation-suite` library). Its newer `navigationSuiteType`
530
+ uses a short bar with each label beside its icon at compact height.
531
+
532
+ At compact height, list and reading screens follow these patterns:
533
+
534
+ - **App bars** hide on scroll and give their space back; see
535
+ [Components and composition patterns](#components-and-composition-patterns)
536
+ for the layout recipe.
537
+ - **Sheets** leave the screen. Open transient controls, such as filters or an
538
+ ingredients list, in a full-screen dialog at any width. Content about the
539
+ selection, such as nutrition, may become a second column that scrolls with
540
+ the page: a pane pinned at the inline end would push the FAB toward the
541
+ middle of the screen, while a scrolling column lets it keep its corner.
542
+ See [Sheets and the FAB](#sheets-and-the-fab).
543
+ - **Dialogs** for multi-field tasks are full-screen.
544
+ - **Tall media** is capped by the pane's height. Make the scroll region a
545
+ `container-type: size` container, which is safe because layout already
546
+ gives it a definite size, and query its height or use `cqb` units: a
547
+ window media query cannot see how much the bars and panes have taken.
548
+ - **Sticky chip rows** stay on one row (`rc-chip-group max-rows="1"`).
549
+
550
+ ```js
551
+ const widthClass = (w) =>
552
+ w >= 1600
553
+ ? 'xlarge'
554
+ : w >= 1200
555
+ ? 'large'
556
+ : w >= 840
557
+ ? 'expanded'
558
+ : w >= 600
559
+ ? 'medium'
560
+ : 'compact';
561
+ const heightClass = (h) => (h >= 900 ? 'expanded' : h >= 480 ? 'medium' : 'compact');
562
+
563
+ const publish = () => {
564
+ document.documentElement.dataset.widthClass = widthClass(innerWidth);
565
+ document.documentElement.dataset.heightClass = heightClass(innerHeight);
566
+ };
567
+ ```
568
+
569
+ Measure when your app shell mounts and on resize, not once at module load.
570
+ Shell CSS can then select on `:root[data-height-class='compact']` without a
571
+ script gate of its own.
572
+
573
+ ### Canonical layouts
574
+
575
+ - **List-detail.** Compact shows the list or the detail, routed. Expanded
576
+ shows both side by side; selection drives the detail. Use CSS grid with a
577
+ bounded list track (about 360px), or `rc-splitter` when the user should
578
+ resize. Keep the URL as the source of truth for the selection so back
579
+ navigation works at every size.
580
+ - **Supporting pane.** Primary content plus a secondary pane (filters,
581
+ details). On a sufficiently tall screen, compact width moves the pane into
582
+ a bottom sheet or below the content; medium and wider may dock it beside
583
+ the content. Content that stays relevant throughout the task belongs in a
584
+ persistent pane once the container is wide enough; a transient overlay is
585
+ for one-off tasks. Decide from the container's width and available height,
586
+ not the device type. At compact height, use a full-screen dialog for
587
+ transient controls and a scrolling column for supporting content that can
588
+ remain visible without narrowing the primary task.
589
+ - **Pane order.** When height permits docking, panes that shape the content,
590
+ such as filters and sort, dock at the inline start, after the navigation
591
+ rail: rail, filters, content. Panes about the current content, such as nutrition or details,
592
+ dock at the inline end; at compact height they scroll with the content
593
+ instead (see the compact-height list above). The FAB sits in the rail's header or floats at the
594
+ bottom inline end, so neither pane ever has to make room for it.
595
+ - **List-detail Back.** Show Back in the single-pane presentation; suppress it
596
+ only while the parent list is visible beside the detail. When the window
597
+ grows to two panes, keep the selected detail and mark it in the list; when
598
+ it shrinks, keep the detail and hide the list.
599
+ - **Pane sizes.** At medium or greater height, a fixed pane (a list, filters,
600
+ or supporting content) is 360px and the other pane is flexible. Separate
601
+ panes with a 24px spacer, which
602
+ can hold an `rc-splitter` handle when the user should resize. Margins are
603
+ 16px at compact width and 24px from medium.
604
+ - **Large windows.** From large width, add structure rather than stretch the
605
+ expanded layout: keep the list pane fixed, bound the reading measure (about
606
+ 44rem), and give a supporting pane, such as nutrition beside a recipe, its
607
+ own third column. Feeds add columns instead of widening cards.
608
+ - **Feed.** A grid of cards whose column count comes from container width:
609
+
610
+ ```css
611
+ .feed {
612
+ display: grid;
613
+ grid-template-columns: repeat(auto-fill, minmax(min(100%, 18rem), 1fr));
614
+ gap: 1rem;
615
+ }
616
+ ```
617
+
618
+ ### Choosing the adaptive mechanism
619
+
620
+ From narrowest to broadest:
621
+
622
+ 1. One geometry token, such as `--rc-carousel-slide-size`.
623
+ 1. A coordinated token set for tracks or placement, as with `rc-card` and
624
+ `rc-list-item` region tokens, often inside a container query.
625
+ 1. An attribute or property written from app state when the mode changes
626
+ ARIA, keyboard behavior, focus, popup placement or rendered controls:
627
+ `rc-dialog variant`, `rc-search-bar variant`, `rc-navigation-rail
628
+ expanded`, `rc-adaptive-menu max-shown`, `rc-splitter orientation`,
629
+ `rc-toolbar orientation`.
630
+ 1. A component's own `auto` mode, only where it measures a failure it owns,
631
+ such as `rc-chip-group layout="auto"` with `max-rows`.
632
+
633
+ Container queries style descendants; they cannot set attributes. Never hide
634
+ controls with CSS to "move" them; use `rc-adaptive-menu` so hidden actions
635
+ reach the overflow menu.
636
+
637
+ Split the work between window and container: window size classes decide the
638
+ shell (navigation type, primary route pane count), container queries decide everything
639
+ inside a pane, so a component works the same in a list pane, a feed or a
640
+ dialog. Inside a pane, prefer intrinsic layout that needs no breakpoint at
641
+ all: `auto-fill` grids with `minmax()`, wrapping rows with a flex basis, and
642
+ `clamp()` for space. Newer CSS fits as progressive enhancement:
643
+
644
+ - `:has()` lays out columns from which panes are mounted, and anchor
645
+ positioning keeps the FAB beside a sheet or pane.
646
+ - `@container scroll-state(stuck)` can raise a bar's elevation once it
647
+ sticks, without a scroll listener.
648
+ - Same-document view transitions animate panes opening and closing.
649
+ - `interpolate-size: allow-keywords` animates a surface to an `auto` size.
650
+
651
+ ### TV and other distant screens
652
+
653
+ Layout readiness, not a separate 10-foot mode:
654
+
655
+ - Keep essential controls inside a 5% safe margin (48px at the sides,
656
+ 27px top and bottom) on extra-large windows that may be a TV; backgrounds
657
+ still run to the edge.
658
+ - Every control must be reachable with arrow keys or a D-pad and show the
659
+ focus ring. Rows and grids suit arrow movement; a hover-only or swipe-only
660
+ affordance is never the only way in.
661
+ - Composite widgets (chip groups, tabs, toolbars) move focus with arrow keys
662
+ through roving focus; where the browser supports the `focusgroup`
663
+ attribute, it can replace that script.
664
+
665
+ ### Edge-to-edge
666
+
667
+ - Draw backgrounds behind system bars; keep controls inside
668
+ `env(safe-area-inset-*)`. The full-screen dialog variant pads with safe-area
669
+ insets already.
670
+ - Use `dvh` (or the dialog's visual-viewport tracking) for full-height
671
+ surfaces so the on-screen keyboard does not hide actions.
672
+ - Keep drag targets (sheet handles, splitters) away from system gesture
673
+ edges.
674
+ - Use logical properties (`padding-inline`, `inset-block-end`) in app CSS so
675
+ RTL and vertical writing modes work.
676
+
677
+ ### Directional icons and safe areas
678
+
679
+ Mirror icons that express movement along the reading direction in RTL: back and
680
+ forward arrows, disclosure chevrons, drawers opening from the inline start,
681
+ and move or import actions. Apply `:dir(rtl)` to the icon itself, where its
682
+ inherited direction is known. Keep clocks, media controls, checkmarks, and
683
+ brand marks in their original orientation. Prefer an icon with a semantic
684
+ back/forward meaning over one whose name promises a physical left/right edge.
685
+
686
+ Safe-area environment variables name physical edges. An app-owned FAB, app bar,
687
+ or full-screen surface placed at the inline end must map those insets to the
688
+ current inline edges before combining them with logical CSS properties. For
689
+ horizontal writing modes, a root mapping can be:
690
+
691
+ ```css
692
+ :root {
693
+ --app-safe-area-inline-start: env(safe-area-inset-left, 0px);
694
+ --app-safe-area-inline-end: env(safe-area-inset-right, 0px);
695
+ }
696
+
697
+ :root:dir(rtl) {
698
+ --app-safe-area-inline-start: env(safe-area-inset-right, 0px);
699
+ --app-safe-area-inline-end: env(safe-area-inset-left, 0px);
700
+ }
701
+ ```
702
+
703
+ For vertical writing modes, inline start and end map to physical top and
704
+ bottom instead; resolve those edges from the writing mode before using the
705
+ variables. The full-screen `rc-dialog` variant already pads its own content
706
+ for safe areas, so do not add the same inset a second time.
707
+
708
+ ## Elevation & Depth
709
+
710
+ - Material 3 expresses depth mostly through **tonal surface roles**; shadows
711
+ are reserved for floating elements (FAB, menus, dialogs, elevated cards).
712
+ - The theme maps shadows to `--rc-shadow-level0` through `--rc-shadow-level4`
713
+ and drops them where M3 does, such as a FAB in the navigation rail header.
714
+ - The app bar does not cast a shadow when content scrolls under it: it
715
+ changes to `surface-container`. Wire `scroll-target` so this happens.
716
+ - Scrims are `scrim` at 32% for modal dialogs and sheets; non-modal sheets
717
+ have none.
718
+
719
+ ## Shapes
720
+
721
+ | Component | Corner |
722
+ | -------------------------------------------- | ----------------------------------------------------------- |
723
+ | Buttons, icon buttons, navigation indicators | full |
724
+ | `rc-snackbar` | extra-small |
725
+ | `rc-tooltip` | extra-small |
726
+ | Chips, menus | small |
727
+ | `rc-popover` | medium |
728
+ | Cards | medium |
729
+ | FAB, standalone disclosure | large |
730
+ | Side sheet leading corners | large |
731
+ | Dialogs, bottom sheet top corners | extra-large |
732
+ | Full-screen dialog | none |
733
+ | Segmented list or accordion group | extra-large outer ends, extra-small inner corners, 2px gaps |
734
+
735
+ Do not round a single element inside a group; the theme assigns group
736
+ corners from position (`data-rc-list-position` on list items,
737
+ `:first-child`/`:last-child` in accordions).
738
+
739
+ ## Motion
740
+
741
+ The theme splits motion into **spatial** (position, size) and **effects**
742
+ (color, opacity) scales, bridged from Material durations and easing curves to
743
+ `--rc-motion-spatial-*` and `--rc-motion-effects-*`.
744
+
745
+ - Use standard easing for recurring, small changes (state layers, chips,
746
+ toggles). Reserve emphasized and expressive motion for prominent container
747
+ or route transitions.
748
+ - Effects motion never overshoots.
749
+ - Don't apply expressive motion to repeated progression, timers or
750
+ decorative loops.
751
+ - Under `prefers-reduced-motion`, the theme zeroes spatial durations and
752
+ shortens effects durations. Your app's own transitions should read the same
753
+ tokens so they follow suit:
754
+
755
+ ```css
756
+ .pane {
757
+ transition: translate var(--rc-motion-spatial-duration-default)
758
+ var(--rc-motion-spatial-easing-default, ease);
759
+ }
760
+ ```
761
+
762
+ See the [motion guide](https://richardcarls.github.io/rc-webcomponents/guide/motion).
763
+
764
+ ## Components and composition patterns
765
+
766
+ Each pattern names the Material role, the composition, and the theme hooks.
767
+
768
+ ### App shell
769
+
770
+ A standard screen is a top app bar, the content region it observes, one
771
+ navigation component chosen by size class, and at most one FAB.
772
+
773
+ ```html
774
+ <body>
775
+ <rc-app-bar scroll-target="#content">
776
+ <h1 class="app-title">Library</h1>
777
+ <rc-adaptive-menu slot="trailing" label="Library actions" max-shown="1">
778
+ <rc-button icon-only class="rc-icon-button--standard" data-priority="20">
779
+ <button type="button" aria-label="Search">
780
+ <span data-rc-menu-leading aria-hidden="true">search</span>
781
+ <span data-rc-menu-label>Search</span>
782
+ </button>
783
+ </rc-button>
784
+ <rc-button class="rc-button--text" slot="overflow">
785
+ <button type="button"><span data-rc-menu-label>Settings</span></button>
786
+ </rc-button>
787
+ </rc-adaptive-menu>
788
+ </rc-app-bar>
789
+
790
+ <nav aria-label="Main">
791
+ <rc-navigation-bar><!-- compact: links --></rc-navigation-bar>
792
+ </nav>
793
+
794
+ <main id="content">…</main>
795
+
796
+ <rc-fab>
797
+ <button type="button">
798
+ <span data-rc-icon aria-hidden="true">add</span>
799
+ New item
800
+ </button>
801
+ </rc-fab>
802
+ </body>
803
+ ```
804
+
805
+ ```css
806
+ .app-title {
807
+ margin: 0;
808
+ font-weight: inherit;
809
+ }
810
+ ```
811
+
812
+ - **Top app bar.** The default is the small bar. Add
813
+ `variant="expanded"` with `.rc-app-bar--medium` or `.rc-app-bar--large` for
814
+ headline-sized titles on top-level screens, plus `scroll-behavior="collapse"`
815
+ to shrink on scroll. `.rc-app-bar--center-aligned` is for a single centered
816
+ title on a root screen. `scroll-behavior="hide"` suits long reading views.
817
+ Always set `scroll-target` to the element that actually scrolls.
818
+ - **Compact height.** On a landscape phone, list and reading screens hide
819
+ the top app bar on scroll (`scroll-behavior="hide"`), and an expanded bar
820
+ starts compact. Stack the bar over its scroll container and reserve
821
+ `var(--rc-app-bar-block-size)` as the container's block-start padding and
822
+ scroll padding. Sticky offsets are measured inside that padding, so sticky
823
+ content below the bar pins at `calc(var(--rc-app-bar-visible-block-size) -
824
+ var(--rc-app-bar-block-size))`; both values are published on the bar's
825
+ scroll target. Hands-busy task bars and full-screen dialog bars stay pinned,
826
+ since they carry actions the task needs.
827
+ - **App bar title.** The title slot holds the screen's one page heading, so
828
+ the body doesn't repeat it. A collapsed or small bar shows one line with an
829
+ ellipsis; an expanded title may wrap to a few lines before clipping. Put
830
+ status (step, sync state) in a one-line subtitle under the title. Never put
831
+ a progress indicator in a top app bar: it is not part of the M3 anatomy.
832
+ - **App bar actions** are 24px icons centered in independent 48px targets.
833
+ Don't let margins meant for labeled buttons shrink or offset them.
834
+ - **App bar slots.** `leading` holds back or close (never a hamburger when a
835
+ rail is visible). `trailing` holds at most two or three icon actions; put
836
+ the rest in `rc-adaptive-menu` overflow. Author each action as an
837
+ `rc-button`: icon-only with `.rc-icon-button--standard` when promoted, and
838
+ `.rc-button--text` with `slot="overflow"` when it should always stay in the
839
+ menu. Larger `data-priority` values are promoted first, up to `max-shown`.
840
+ The component never measures space, so write `max-shown` from size-class
841
+ state. Utilities like Settings and Help belong in overflow, not in
842
+ navigation.
843
+ - **Navigation.** Three to five top-level destinations. Don't render a
844
+ navigation suite with a single destination, or with disabled or unreleased
845
+ ones; until there are two, the app bar title links home and utilities live
846
+ in overflow. Render
847
+ `rc-navigation-bar` on compact width or compact height, and
848
+ `rc-navigation-rail` otherwise; switch the element from app state, keeping
849
+ the same links and `aria-current="page"`. Put the screen's FAB in the rail's
850
+ `header` slot when the rail is shown (the theme removes its shadow there).
851
+ `expanded` on the rail shows labels beside icons on expanded and wider
852
+ windows.
853
+ - **FAB.** One per screen, for the screen's primary constructive action.
854
+ `.rc-fab--small` and `.rc-fab--large` change size; an icon plus label makes
855
+ an extended FAB. Anchor with `position` and keep it clear of the navigation
856
+ bar. `scroll-reveal` suits a back-to-top button, not the primary action. Use
857
+ `rc-fab-menu` when the action needs two to six choices.
858
+ - **Floating layer.** The FAB and other floating controls clear whatever is
859
+ docked at the bottom: the navigation bar, and a persistent sheet's peek by
860
+ 16px plus the safe-area inset.
861
+
862
+ ### Actions and emphasis
863
+
864
+ | Emphasis | Hook | Use |
865
+ | ----------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------ |
866
+ | High | `rc-button` (filled, default) | The one primary action in a view or dialog |
867
+ | Medium-high | `.rc-button--tonal` | Important but not primary; a secondary action next to filled |
868
+ | Medium | `.rc-button--outlined` | Alternatives that need a boundary |
869
+ | Low | `.rc-button--text` | Dialog dismiss, inline and tertiary actions |
870
+ | Icon only | `rc-button[icon-only]` with `.rc-icon-button--standard`, `--tonal` or `--outlined` | Toolbars, app bars, list trailing actions |
871
+
872
+ ```html
873
+ <rc-button class="rc-button--text"><button type="button">Cancel</button></rc-button>
874
+ <rc-button><button type="submit">Save</button></rc-button>
875
+
876
+ <rc-button icon-only class="rc-icon-button--standard">
877
+ <button type="button" aria-label="Edit">
878
+ <span data-rc-button-icon aria-hidden="true">edit</span>
879
+ </button>
880
+ </rc-button>
881
+
882
+ <rc-button class="rc-button--outlined" data-tone="danger">
883
+ <button type="button">Delete account</button>
884
+ </rc-button>
885
+ ```
886
+
887
+ - **One filled button per view.** If two compete, demote one.
888
+ - Icon buttons default to the small size (40px container). Use
889
+ `.rc-button--extra-small` to `.rc-button--extra-large`, plus `.rc-button--narrow`
890
+ or `.rc-button--wide`, for expressive sizes. Every size keeps a 48px
891
+ activation area.
892
+ - Toggle buttons use `rc-button toggle` with `data-rc-button-icon` and
893
+ `data-rc-button-selected-icon`; selection shows by shape and color, and is
894
+ announced through `aria-pressed`.
895
+ - Menus open from `rc-menu-button`; give an icon-only trigger an accessible
896
+ name. `.rc-menu-button--narrow` is for tight app-bar trailing groups.
897
+ A submenu row's trailing glyph goes in the nested `rc-menu-button`'s
898
+ `indicator` slot; `rc-menu` never generates one.
899
+ - Put an icon before the label, never after it.
900
+
901
+ ### Dialogs
902
+
903
+ Material has two dialog presentations. Choose by window size class, in both
904
+ dimensions, and write the choice from app state:
905
+
906
+ | | Basic dialog | Full-screen dialog |
907
+ | ------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------- |
908
+ | When | Medium and wider; short tasks and confirmations on any size | Compact width or compact height; multi-field tasks or long content |
909
+ | Surface | `surface-container-high`, extra-large corners, 280–560px wide | `surface`, no corners, fills the visual viewport |
910
+ | Header | Headline (optional icon above it) | Small top app bar: close (×) leading, title, confirm text button trailing |
911
+ | Actions | Footer, end-aligned: dismiss as text, confirm as filled (or text) | Confirm in the app bar; no footer |
912
+
913
+ ```html
914
+ <!-- Basic: confirmation -->
915
+ <rc-dialog id="discard">
916
+ <dialog aria-labelledby="discard-title">
917
+ <h2 id="discard-title">Discard draft?</h2>
918
+ <p>Your changes will be lost.</p>
919
+ <form method="dialog" class="dialog-actions">
920
+ <rc-button class="rc-button--text">
921
+ <button value="cancel">Keep editing</button>
922
+ </rc-button>
923
+ <rc-button class="rc-button--text" data-tone="danger">
924
+ <button value="discard">Discard</button>
925
+ </rc-button>
926
+ </form>
927
+ </dialog>
928
+ </rc-dialog>
929
+ ```
930
+
931
+ ```html
932
+ <!-- Full-screen on compact: an app bar carries both actions -->
933
+ <rc-dialog id="edit" variant="fullscreen">
934
+ <dialog aria-labelledby="edit-title">
935
+ <rc-app-bar scroll-target="#edit-body">
936
+ <button slot="leading" type="button" aria-label="Cancel">
937
+ <span data-rc-icon aria-hidden="true">close</span>
938
+ </button>
939
+ <h2 id="edit-title">Edit item</h2>
940
+ <rc-button slot="trailing" class="rc-button--text">
941
+ <button type="submit" form="edit-form">Save</button>
942
+ </rc-button>
943
+ </rc-app-bar>
944
+ <div id="edit-body" class="dialog-body">
945
+ <form id="edit-form" method="dialog">…</form>
946
+ <!-- Close button: call close() or requestClose() on the rc-dialog. -->
947
+ </div>
948
+ </dialog>
949
+ </rc-dialog>
950
+ ```
951
+
952
+ - **Adapt one dialog, don't fork it.** At the 600px width or 480px height
953
+ boundary, switch `variant` between `fullscreen` and `standard` and move the
954
+ two actions between the app bar and the footer. If a button had focus,
955
+ return focus to the same action
956
+ in its new position after the components finish rendering (await the app
957
+ bar's `updateComplete` before focusing a slotted button).
958
+ - **Reset heading styles** on an `<h2>` used as an app bar or dialog title
959
+ (`margin: 0; font: inherit`, then the title or headline type role); the
960
+ theme styles the slot, not your heading's UA defaults.
961
+ - **Scroll a body region**, never the dialog surface; the app bar's
962
+ `scroll-target` points at it.
963
+ - **Size task dialogs to their maximum.** A basic dialog fits its content, so
964
+ a dialog whose content is fluid (a search field, a list) shrinks to its
965
+ 280px minimum. Set `--rc-dialog-inline-size: 100%` on it to use the full
966
+ 560px.
967
+ - **Dialog actions are content-sized** and aligned to the inline end, dismiss
968
+ before confirm, each with a 48px target. They wrap only when text scaling or
969
+ width requires it and never stretch into full-width rows (Android's
970
+ `dismissButton`/`confirmButton` model).
971
+ - **Dismiss is never destructive.** Back, Escape and the scrim all cancel.
972
+ Set `closed-by="any"` only for dialogs whose dismissal loses nothing.
973
+ - **Single-choice dialogs** for enumerated settings use native radios in an
974
+ `rc-list selection="single"` and commit on confirm, or immediately on
975
+ selection when there is no confirm button.
976
+ - **Confirmations** state the consequence in the headline, put the
977
+ destructive action last with `data-tone="danger"`, and label buttons with
978
+ verbs rather than OK.
979
+ - Use `default-open` for an initially open uncontrolled dialog; `open` makes
980
+ the dialog controlled (see [Framework notes](#framework-notes)).
981
+
982
+ ### Bottom sheets
983
+
984
+ `rc-bottom-sheet` wraps a `<dialog>`. Use `showModal()` for a modal sheet
985
+ (scrim, focus trapped) and `show()` for a standard, non-modal sheet that
986
+ coexists with the page.
987
+
988
+ ```html
989
+ <rc-bottom-sheet id="filters" snap-points="50dvh 90dvh" swipe-dismiss>
990
+ <dialog aria-label="Filters">
991
+ <button
992
+ type="button"
993
+ data-rc-bottom-sheet-handle
994
+ data-rc-dialog-resize-axis="y"
995
+ data-rc-dialog-resize-origin="top"
996
+ aria-label="Resize sheet"
997
+ ></button>
998
+ …
999
+ </dialog>
1000
+ </rc-bottom-sheet>
1001
+ ```
1002
+
1003
+ - Use a modal sheet on compact width for filters, sharing and short option
1004
+ sets that would be a menu or side pane on wider windows. Expanded windows
1005
+ should show a docked supporting pane or a dialog instead.
1006
+ - Always include the drag handle as a real button so keyboard and switch
1007
+ users can resize. Keep a visible close or Done action as well; swipe is
1008
+ never the only way out.
1009
+ - Cap the top snap point below the status bar (`90dvh`); a sheet that fills
1010
+ the screen should be a full-screen dialog.
1011
+ - Do not stack sheets or open a dialog from a sheet's own content.
1012
+ - Keep the drag handle in its own band above the sheet content, as M3 places
1013
+ it. Draw keyboard focus inside the sheet's clipped corners, since an outline
1014
+ outside them is cut off.
1015
+ - A standard, non-modal sheet that stays open over the page uses its own
1016
+ container tone so it reads as a separate surface.
1017
+ - Don't add swipe paging between tabs inside a sheet. Horizontal swipes
1018
+ compete with vertical resizing, scrolling and the browser's edge-back
1019
+ gesture; switch tabs explicitly.
1020
+
1021
+ ### Sheets and the FAB
1022
+
1023
+ A floating action button and a sheet compete for the same edges: the FAB
1024
+ floats at the bottom inline end, a side sheet covers the inline end, and a
1025
+ bottom sheet covers the bottom. Moving the FAB out of a sheet's way is a sign
1026
+ the screen wants a different surface.
1027
+
1028
+ - Don't open a side sheet on a screen that has a FAB. Dock the same content as
1029
+ a pane in the layout (Material's standard side sheet, which stays visible
1030
+ while the content is used), or use a full-screen dialog for a short task.
1031
+ - At compact height, don't open a bottom sheet on a screen that has a FAB,
1032
+ modal or standard: what is left above it cannot hold both the content and
1033
+ the FAB. Use a full-screen dialog for transient controls at every width;
1034
+ supporting content may form a scrolling column or follow in the page.
1035
+ - On taller windows a bottom sheet may share a screen with a FAB; the FAB
1036
+ clears the sheet's peek (see the floating layer under App shell) and a modal
1037
+ sheet's scrim covers it while open.
1038
+ - When the navigation rail is shown, the FAB moves into the rail's `header`
1039
+ slot (Compose Material 3 `NavigationRail` and `WideNavigationRail` take the
1040
+ FAB in their header), so docked panes beside the rail never meet it.
1041
+
1042
+ ### Side sheets
1043
+
1044
+ `rc-dialog variant="side"` is the modal side sheet: docked to the inline end at
1045
+ full height, at most 400px wide, rounded on its leading edge, with the shared
1046
+ scrim. It fades rather than sliding, like every other surface here.
1047
+
1048
+ ```html
1049
+ <rc-dialog id="filters" variant="side" closed-by="any">
1050
+ <dialog aria-labelledby="filters-title">
1051
+ <h2 id="filters-title">Filters</h2>
1052
+ <div class="dialog-body">…</div>
1053
+ <form method="dialog" class="dialog-actions">
1054
+ <rc-button class="rc-button--text"><button value="cancel">Cancel</button></rc-button>
1055
+ <rc-button><button value="apply">Apply</button></rc-button>
1056
+ </form>
1057
+ </dialog>
1058
+ </rc-dialog>
1059
+ ```
1060
+
1061
+ - Use a side sheet for the same content a modal bottom sheet carries when the
1062
+ window is compact in height, such as a landscape phone, where a bottom
1063
+ sheet would leave only a strip of content, and only on a screen without a
1064
+ FAB; see [Sheets and the FAB](#sheets-and-the-fab). Write `variant` from app
1065
+ state; the sheet's content and actions stay the same.
1066
+ - Scroll a body region and keep the actions outside it, so they never leave
1067
+ the screen.
1068
+ - A side sheet that stays open beside content is a docked supporting pane,
1069
+ not a dialog. Lay it out in the page instead.
1070
+
1071
+ ### Lists and settings
1072
+
1073
+ `rc-list` and `rc-list-item` are Material lists. Slots map to the M3 list
1074
+ anatomy: `leading` (icon, avatar or native control, with `data-rc-list-leading`
1075
+ for icon geometry), the default headline and supporting text, `trailing`
1076
+ (metadata or an action), and `details` for expandable content.
1077
+
1078
+ ```html
1079
+ <h2 class="section-heading" id="display">Display</h2>
1080
+ <rc-list variant="segmented" aria-labelledby="display">
1081
+ <rc-list-item>
1082
+ <span slot="leading" data-rc-list-leading aria-hidden="true">dark_mode</span>
1083
+ <label for="dark">Dark theme</label>
1084
+ <rc-switch slot="trailing"><input id="dark" type="checkbox" /></rc-switch>
1085
+ </rc-list-item>
1086
+ <rc-list-item interactive action-target="text-size">
1087
+ <span slot="leading" data-rc-list-leading aria-hidden="true">text_fields</span>
1088
+ <a id="text-size" href="/settings/text-size">Text size</a>
1089
+ <small data-rc-list-supporting>Medium</small>
1090
+ </rc-list-item>
1091
+ </rc-list>
1092
+ ```
1093
+
1094
+ - **Settings screens** are groups of `variant="segmented"` lists under
1095
+ title-small section headings. A boolean is a switch in the trailing slot,
1096
+ labeled by the row headline. An enum with two to five short values
1097
+ is `rc-segmented-button`; longer enums open a single-choice dialog from a
1098
+ row showing the current value as supporting text. Navigation and
1099
+ dialog-opening rows use `interactive` with `action-target`, so the whole row
1100
+ forwards to one real link or button.
1101
+ - The theme does not restyle the action target itself, so reset a link used
1102
+ as a row headline:
1103
+
1104
+ ```css
1105
+ rc-list-item[interactive] > a[id] {
1106
+ color: inherit;
1107
+ text-decoration: none;
1108
+ }
1109
+ ```
1110
+
1111
+ - Segmented button rows span the row on compact and cap at about 30rem on
1112
+ wider panes. Drop the selected checkmark when the row is too narrow for
1113
+ three worded segments. `rc-list` sizes one shared trailing column for all
1114
+ its rows, so put segmented-button rows in a list without trailing content.
1115
+ - Settings on expanded windows become list-detail: the category list on the
1116
+ start side, the selected category on the end side.
1117
+ - Use the standard (non-segmented) list for content collections and dense
1118
+ results; use dividers sparingly.
1119
+ - `selection="single"` or `"multiple"` with native radios or checkboxes makes
1120
+ a selectable list. `reorderable` adds drag handles with keyboard support.
1121
+ - **Drag is never the only path.** Wherever items reorder by drag, also offer
1122
+ visible move-earlier and move-later actions, and announce the new position
1123
+ in a live region that exists before the move.
1124
+ - **Contextual selection.** For per-item actions in a grid or list, select
1125
+ first and then show the actions for the selected item, rather than a
1126
+ permanent action row on every item or a hidden long-press menu.
1127
+ - Keep one primary target per row. A trailing icon button is a second target
1128
+ only when the row itself does something else.
1129
+
1130
+ ### Disclosures and accordions
1131
+
1132
+ `rc-disclosure` around `<details>` is Material's expandable list item: a
1133
+ headline row with a trailing chevron that rotates when open. `rc-accordion`
1134
+ groups them as a segmented list, single-open by default (`multiple` allows
1135
+ several).
1136
+
1137
+ ```html
1138
+ <rc-accordion>
1139
+ <rc-disclosure default-open>
1140
+ <details>
1141
+ <summary>On your phone</summary>
1142
+ <p>…</p>
1143
+ </details>
1144
+ </rc-disclosure>
1145
+ <rc-disclosure>
1146
+ <details>
1147
+ <summary>On your computer</summary>
1148
+ <p>…</p>
1149
+ </details>
1150
+ </rc-disclosure>
1151
+ </rc-accordion>
1152
+ ```
1153
+
1154
+ - Seed the initial state with `default-open`. `open` is the **controlled**
1155
+ state: the disclosure then ignores its own summary, and the accordion's
1156
+ close arrives as an `rc-disclosure-toggle` request your code must honor.
1157
+ - Keep summaries to one line of title text; an optional leading icon remains
1158
+ before the trailing chevron. Put explanation in the panel.
1159
+ - Use disclosures for optional help and secondary detail, not for primary
1160
+ form fields or navigation.
1161
+
1162
+ ### Cards and feeds
1163
+
1164
+ `rc-card` defaults to the M3 **elevated** card (`surface-container-low`,
1165
+ level-1 shadow, medium corners).
1166
+
1167
+ ```html
1168
+ <rc-card interactive action-target="item-42">
1169
+ <img slot="media" src="/photo.jpg" alt="" />
1170
+ <a id="item-42" slot="title" href="/items/42">Item title</a>
1171
+ <span slot="subtitle">Supporting line</span>
1172
+ <p>Short summary.</p>
1173
+ </rc-card>
1174
+ ```
1175
+
1176
+ ```css
1177
+ /* M3 filled and outlined cards are token sets on the same element. */
1178
+ rc-card.card-filled {
1179
+ --rc-card-bg: var(--md-sys-color-surface-container-highest);
1180
+ --rc-card-shadow: none;
1181
+ }
1182
+
1183
+ rc-card.card-outlined {
1184
+ --rc-card-bg: var(--md-sys-color-surface);
1185
+ --rc-card-border: 1px solid var(--md-sys-color-outline-variant);
1186
+ --rc-card-shadow: none;
1187
+ }
1188
+ ```
1189
+
1190
+ - Use `interactive` and `action-target` for whole-card navigation: the card
1191
+ forwards clicks to one real link or button, which keeps a single tab stop
1192
+ and accessible name. Put additional actions in the `actions` slot.
1193
+ - `orientation="horizontal"` places media beside text; region tokens
1194
+ (`--rc-card-grid-template-columns` and friends) rearrange it inside a
1195
+ container query.
1196
+ - `selected` shows a selection outline for multi-select collections.
1197
+ - Use `rc-carousel` for a scrollable row of media, not for primary content
1198
+ the user must see.
1199
+ - Media needs a failure state. A missing or broken image renders a
1200
+ placeholder surface, never the browser's broken-image icon or the alt text
1201
+ as the visual. Card media is decorative (`alt=""`) when the title names the
1202
+ item.
1203
+
1204
+ ### Search
1205
+
1206
+ - Compact: a full-screen search view (`rc-search-bar variant="view"`) opened
1207
+ from an app bar action or a docked bar at the top of the screen.
1208
+ - Medium and wider: a docked bar (the default `variant`) in the app bar's
1209
+ `center` slot or at the top of the content pane.
1210
+ - The native `<input type="search">` is required and labeled. Debounce with
1211
+ `debounce`; commit the query to the URL so it survives reload and back.
1212
+
1213
+ ```html
1214
+ <search>
1215
+ <label for="q" class="visually-hidden">Search items</label>
1216
+ <rc-search-bar placeholder="Search items">
1217
+ <span slot="leading" aria-hidden="true" data-rc-icon>search</span>
1218
+ <input type="search" id="q" name="q" />
1219
+ </rc-search-bar>
1220
+ </search>
1221
+ ```
1222
+
1223
+ ### Forms and inputs
1224
+
1225
+ - `rc-field` is the M3 filled text field; add `.rc-field--outlined` for the
1226
+ outlined variant. Use one variant per form. Label, hint and error are slots
1227
+ around your native control; `counter` shows length against `maxlength`.
1228
+ - `rc-select` wraps a native `<select>`; `rc-combobox` adds filtering and free
1229
+ entry. `.rc-select--dense` and `.rc-combobox--dense` are for dense desktop
1230
+ panes, not touch layouts.
1231
+ - `rc-switch` for immediate on/off settings; a checkbox for choices committed
1232
+ with a form. `rc-slider` and `rc-range-slider` wrap `<input type="range">`.
1233
+ - Validate on submit and on blur, not per keystroke. Errors replace the hint
1234
+ and say how to fix the problem.
1235
+ - **Show saved data as content, not as read-only fields.** Detail views use
1236
+ lists, paragraphs and chips. A read-only field is a form state: use it only
1237
+ inside a form, for a value that can't change.
1238
+ - Full-width fields on compact; on wider windows, bound field width to the
1239
+ content (about 280–560px) instead of stretching across the pane.
1240
+
1241
+ ```html
1242
+ <rc-field>
1243
+ <label slot="label" for="title">Title</label>
1244
+ <input id="title" name="title" required />
1245
+ <small slot="hint">Shown at the top of the page.</small>
1246
+ <small slot="error">Enter a title.</small>
1247
+ </rc-field>
1248
+ ```
1249
+
1250
+ ### Chips
1251
+
1252
+ | Chip | Composition | Use |
1253
+ | ---------- | -------------------------------------------------------------- | ----------------------------------- |
1254
+ | Assist | `rc-chip` with a `<button>` | Contextual action (add to calendar) |
1255
+ | Filter | `rc-chip-group kind="filter"` with native radios or checkboxes | Narrow a collection |
1256
+ | Input | `rc-chip variant="input"` | Entered values (recipients, tags) |
1257
+ | Suggestion | `rc-chip variant="suggestion"` | Dynamic suggestions |
1258
+
1259
+ - Wrap filter chips in a `<fieldset>` with a legend; `selection` mirrors the
1260
+ native control type.
1261
+ - On compact, use `layout="scroll"` for a single horizontal row, or
1262
+ `layout="auto"` with `max-rows` to collapse behind a disclosure.
1263
+ - Chips are not navigation and not primary actions.
1264
+
1265
+ ### Feedback and progress
1266
+
1267
+ - **Transient messages.** `rc-snackbar.show({ message, actionLabel })` for brief,
1268
+ low-priority messages at the bottom; keep them clear of the navigation bar
1269
+ and FAB. At
1270
+ most one action, typically Undo for a reversible change; never use one
1271
+ for errors that need a decision (use a dialog) or for messages
1272
+ that must persist (use inline text). Choose a `queue-policy` deliberately.
1273
+ Undo restores the exact previous state. An action that changed nothing
1274
+ says so and offers no Undo.
1275
+ - **Progress.** `rc-progress` wraps `<progress>` as an M3 linear indicator.
1276
+ Determinate when you know the amount; `indeterminate` otherwise. Label it,
1277
+ and use `value-text` for a readable status. The theme has no circular
1278
+ indicator yet.
1279
+ - **Live regions.** Announce outcomes, such as Saved or 3 items imported, not
1280
+ every tick of progress.
1281
+ - **Empty states** use body text and one action, not an illustration alone.
1282
+
1283
+ - **Tooltips.** Use `rc-tooltip` for concise descriptive text attached to a
1284
+ focused or hovered trigger. Keep the content non-interactive, and use it for
1285
+ supplemental context rather than required instructions. The theme maps its
1286
+ surface to `inverse-surface` and its text to `inverse-on-surface`.
1287
+ - **Rich tooltips.** Use `rc-popover data-rc-material-rich-tooltip` for an
1288
+ explanatory panel with an optional action. Author a named non-modal dialog
1289
+ surface in the `content` slot with direct `h2`, `p`, and optional `footer`
1290
+ children. Set `open-on-hover` and `open-on-focus` only when users can move
1291
+ into the content without closing it. The theme supplies the Material surface,
1292
+ typography, spacing, and 320px maximum width; the authored action retains
1293
+ native button or link behavior.
1294
+
1295
+ ### Toolbars
1296
+
1297
+ - `rc-toolbar` groups related controls with roving focus. The theme draws
1298
+ it on `surface-container` with icon-button geometry and
1299
+ `secondary-container` for pressed toggles.
1300
+ - A bottom toolbar on compact holds contextual actions for the current
1301
+ screen (editing, selection mode). It is not navigation; never pair it with a
1302
+ navigation bar on the same screen.
1303
+ - In compact-height layouts, set `orientation="vertical"` and place the
1304
+ toolbar at the inline end.
1305
+
1306
+ ### Split panes
1307
+
1308
+ `rc-splitter` gives a resizable list-detail or supporting pane on expanded
1309
+ windows. Set `orientation` from app state when the region changes shape, and
1310
+ bound the primary pane with `min` and `max`. Do not use a splitter on compact
1311
+ widths; route between panes instead.
1312
+
1313
+ ## Accessibility and interaction states
1314
+
1315
+ - **State layers.** Interactive surfaces show hover (8%), focus (12%) and
1316
+ pressed (12%) layers of their foreground color. Components draw their
1317
+ own; add `.rc-state-layer` to custom interactive elements that already set a
1318
+ border radius.
1319
+ - **Focus.** Keep the visible focus ring. When a surface closes or a control
1320
+ moves, put focus somewhere sensible: the trigger, or the same action in its
1321
+ new position.
1322
+ - **Targets.** 48px minimum activation area for everything a finger can hit;
1323
+ the components reserve it even when the visual container is smaller.
1324
+ - **Keyboard.** The components implement the WAI-ARIA patterns (roving focus
1325
+ in toolbars and menus, arrow keys in accordions and lists, Escape in
1326
+ dialogs). Do not add competing key handlers.
1327
+ - **Names.** Every icon-only control has an `aria-label`; every landmark
1328
+ (`nav`, `search`, lists) has a label when there is more than one.
1329
+ - **Writing modes.** Use logical properties and `dir`-aware icons (back
1330
+ arrows mirror in RTL).
1331
+
1332
+ ## Framework notes
1333
+
1334
+ - **Attributes versus properties.** Many components distinguish controlled
1335
+ and uncontrolled state: `open` versus `default-open`, `value` versus
1336
+ `default-value`. Frameworks that set properties on custom elements (Solid,
1337
+ Lit, Vue) must use the default variant to seed state, such as Solid's
1338
+ `prop:defaultOpen={true}`. Binding `open` hands control to your code, and
1339
+ user interaction then only emits a request event.
1340
+ - **Keep native children mounted.** Do not conditionally render the inner
1341
+ `<dialog>`, `<details>` or `<input>`; toggle state instead.
1342
+ - **Render timing.** Slotted controls in a Lit-based component can't take
1343
+ focus until the component has rendered. Await `updateComplete` on the
1344
+ component before calling `focus()` on something you just slotted.
1345
+ - **Typings.** `@rcarls/rc-webcomponents/solid` provides JSX typings for
1346
+ attributes and Solid's `prop:` bindings. The `/react` typings cover
1347
+ attributes and events, but do not use `prop:` syntax.
1348
+
1349
+ ## Implementation guidance
1350
+
1351
+ ### Do
1352
+
1353
+ - Choose navigation, dialog presentation and search mode from window size
1354
+ class state.
1355
+ - Keep one filled button and at most one FAB per screen.
1356
+ - Use the surface-container ladder for depth and nest content one step down.
1357
+ - Put overflow actions in `rc-adaptive-menu` and utilities (Settings, Help)
1358
+ in overflow.
1359
+ - Use segmented lists with section headings for settings.
1360
+ - Seed initial open state with `default-open`.
1361
+ - Read motion tokens in your own transitions.
1362
+ - Use logical properties and safe-area insets.
1363
+
1364
+ ### Don't
1365
+
1366
+ - Don't use a full-screen dialog on medium or wider windows, or a basic dialog
1367
+ for long forms on compact.
1368
+ - Don't show a navigation bar and a rail together, or put utilities in
1369
+ navigation.
1370
+ - Don't hide controls with CSS media queries to adapt toolbars.
1371
+ - Don't style shadow internals or `--_rc-*` tokens, or use `!important`
1372
+ against the theme.
1373
+ - Don't use color alone for status, or `primary` for decoration.
1374
+ - Don't remove focus rings or shrink targets below 48px.
1375
+ - Don't put a card on a surface of the same container role.
1376
+ - Don't stack modal surfaces (a dialog over a sheet over a dialog).
1377
+
1378
+ ## References
1379
+
1380
+ - [Material 3](https://m3.material.io/): [layout and canonical layouts](https://m3.material.io/foundations/layout/canonical-layouts/overview),
1381
+ [color roles](https://m3.material.io/styles/color/roles),
1382
+ [type scale](https://m3.material.io/styles/typography/type-scale-tokens),
1383
+ [shape](https://m3.material.io/styles/shape/corner-radius-scale),
1384
+ [motion](https://m3.material.io/styles/motion/overview),
1385
+ [dialogs](https://m3.material.io/components/dialogs/guidelines),
1386
+ [bottom sheets](https://m3.material.io/components/bottom-sheets/guidelines),
1387
+ [lists](https://m3.material.io/components/lists/guidelines),
1388
+ [navigation bar](https://m3.material.io/components/navigation-bar/guidelines),
1389
+ [navigation rail](https://m3.material.io/components/navigation-rail/guidelines),
1390
+ [top app bar](https://m3.material.io/components/top-app-bar/guidelines).
1391
+ - Android: [window size classes](https://developer.android.com/develop/adaptive-apps/guides/use-window-size-classes),
1392
+ [canonical layouts](https://developer.android.com/develop/adaptive-apps/guides/canonical-layouts),
1393
+ [adaptive navigation](https://developer.android.com/develop/adaptive-apps/guides/build-adaptive-navigation),
1394
+ [edge-to-edge](https://developer.android.com/design/ui/mobile/guides/layout-and-content/edge-to-edge).
1395
+ - rc-webcomponents: [styling guide](https://richardcarls.github.io/rc-webcomponents/guide/styling),
1396
+ [motion](https://richardcarls.github.io/rc-webcomponents/guide/motion),
1397
+ [component reference](https://richardcarls.github.io/rc-webcomponents/).
1398
+ - Token values: [material-foundation/material-tokens](https://github.com/material-foundation/material-tokens),
1399
+ vendored in `defaults.css`.