@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.
- package/CHANGELOG.md +544 -0
- package/DESIGN.md +1399 -0
- package/README.md +88 -5
- package/bridge.css +132 -55
- package/components/accordion.css +43 -157
- package/components/adaptive-menu.css +47 -86
- package/components/app-bar.css +4 -2
- package/components/bottom-sheet.css +2 -2
- package/components/button.css +2 -1
- package/components/card.css +2 -0
- package/components/chip.css +12 -1
- package/components/combobox.css +61 -14
- package/components/dialog.css +27 -26
- package/components/disclosure.css +128 -84
- package/components/field.css +566 -0
- package/components/icons.css +9 -0
- package/components/list-item.css +65 -3
- package/components/listbox.css +5 -4
- package/components/markdown-editor.css +62 -35
- package/components/menu-button.css +4 -1
- package/components/menu.css +18 -31
- package/components/menubar.css +4 -1
- package/components/modes.css +22 -5
- package/components/navigation-bar.css +3 -5
- package/components/navigation-rail.css +5 -7
- package/components/popover.css +50 -0
- package/components/progress.css +5 -0
- package/components/range-slider.css +3 -2
- package/components/search-bar.css +12 -2
- package/components/segmented-button.css +2 -28
- package/components/select.css +99 -14
- package/components/splitter.css +4 -1
- package/components/switch.css +9 -1
- package/components/textarea.css +7 -3
- package/components/toolbar.css +2 -1
- package/components/tooltip.css +12 -0
- package/components/transfer-list.css +5 -1
- package/components/virtual-scroller.css +7 -0
- package/components.css +6 -1
- package/css-components/button.css +66 -0
- package/css-components/card.css +14 -0
- package/css-components/chip-group.css +9 -0
- package/css-components/chip.css +80 -0
- package/css-components/disclosure.css +25 -0
- package/css-components/list.css +34 -0
- package/css-components/select.css +20 -0
- package/css-components/textarea.css +22 -0
- package/css-components.css +31 -0
- package/defaults.css +189 -3
- package/native.css +258 -0
- package/package.json +20 -6
- 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`.
|