@jasonshimmy/cer-material 0.1.2

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 (53) hide show
  1. package/README.md +989 -0
  2. package/dist/components/md-app-bar.d.ts +1 -0
  3. package/dist/components/md-badge.d.ts +1 -0
  4. package/dist/components/md-bottom-sheet.d.ts +1 -0
  5. package/dist/components/md-button-group.d.ts +1 -0
  6. package/dist/components/md-button.d.ts +1 -0
  7. package/dist/components/md-card.d.ts +1 -0
  8. package/dist/components/md-carousel.d.ts +1 -0
  9. package/dist/components/md-checkbox.d.ts +1 -0
  10. package/dist/components/md-chip.d.ts +1 -0
  11. package/dist/components/md-date-picker.d.ts +1 -0
  12. package/dist/components/md-dialog.d.ts +1 -0
  13. package/dist/components/md-divider.d.ts +1 -0
  14. package/dist/components/md-fab-menu.d.ts +1 -0
  15. package/dist/components/md-fab.d.ts +1 -0
  16. package/dist/components/md-icon-button.d.ts +1 -0
  17. package/dist/components/md-list.d.ts +1 -0
  18. package/dist/components/md-loading-indicator.d.ts +1 -0
  19. package/dist/components/md-menu.d.ts +1 -0
  20. package/dist/components/md-navigation-bar.d.ts +1 -0
  21. package/dist/components/md-navigation-drawer.d.ts +1 -0
  22. package/dist/components/md-navigation-rail.d.ts +1 -0
  23. package/dist/components/md-progress.d.ts +1 -0
  24. package/dist/components/md-radio.d.ts +1 -0
  25. package/dist/components/md-search.d.ts +1 -0
  26. package/dist/components/md-segmented-button.d.ts +1 -0
  27. package/dist/components/md-side-sheet.d.ts +1 -0
  28. package/dist/components/md-slider.d.ts +1 -0
  29. package/dist/components/md-snackbar.d.ts +1 -0
  30. package/dist/components/md-split-button.d.ts +1 -0
  31. package/dist/components/md-switch.d.ts +1 -0
  32. package/dist/components/md-tabs.d.ts +1 -0
  33. package/dist/components/md-text-field.d.ts +1 -0
  34. package/dist/components/md-time-picker.d.ts +1 -0
  35. package/dist/components/md-tooltip.d.ts +1 -0
  36. package/dist/composables/useControlledValue.d.ts +14 -0
  37. package/dist/composables/useEscapeKey.d.ts +16 -0
  38. package/dist/composables/useFocusReturn.d.ts +18 -0
  39. package/dist/composables/useFocusTrap.d.ts +31 -0
  40. package/dist/composables/useListKeyNav.d.ts +42 -0
  41. package/dist/composables/useScrollLock.d.ts +4 -0
  42. package/dist/index.cjs +5030 -0
  43. package/dist/index.cjs.map +1 -0
  44. package/dist/index.d.ts +7 -0
  45. package/dist/index.js +6250 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/theme.cjs +137 -0
  48. package/dist/theme.cjs.map +1 -0
  49. package/dist/theme.d.ts +14 -0
  50. package/dist/theme.js +148 -0
  51. package/dist/theme.js.map +1 -0
  52. package/dist/vite.svg +1 -0
  53. package/package.json +59 -0
package/README.md ADDED
@@ -0,0 +1,989 @@
1
+ # @jasonshimmy/cer-material
2
+
3
+ Material Design 3 web components built on [`@jasonshimmy/custom-elements-runtime`](https://github.com/jshimkoski/custom-elements).
4
+
5
+ All components are standard custom elements — framework-agnostic and usable in plain HTML, React, Vue, Angular, Svelte, or any other environment.
6
+
7
+ Learn more about the author at [jasonshimmy.com](https://jasonshimmy.com) and check out the [changelog](./CHANGELOG.md) for recent updates.
8
+
9
+ ---
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install @jasonshimmy/cer-material @jasonshimmy/custom-elements-runtime
15
+ ```
16
+
17
+ `@jasonshimmy/custom-elements-runtime` is a **peer dependency** and must be installed alongside this package.
18
+
19
+ ### Font setup
20
+
21
+ Components use Material Symbols and Roboto. Add both to your HTML `<head>`:
22
+
23
+ ```html
24
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
25
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
26
+ <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet" />
27
+ <link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20,400,0,0&display=swap" rel="stylesheet" />
28
+ ```
29
+
30
+ ---
31
+
32
+ ## Quick start
33
+
34
+ ```ts
35
+ // Registers all components and applies the MD3 design token theme
36
+ import '@jasonshimmy/cer-material';
37
+ ```
38
+
39
+ Then use any component tag directly in your HTML or templates:
40
+
41
+ ```html
42
+ <md-button variant="filled" label="Save"></md-button>
43
+ <md-text-field label="Email" type="email"></md-text-field>
44
+ ```
45
+
46
+ ### Theme only
47
+
48
+ If you need the MD3 CSS custom properties without registering components:
49
+
50
+ ```ts
51
+ import '@jasonshimmy/cer-material/theme';
52
+ ```
53
+
54
+ Or call it lazily:
55
+
56
+ ```ts
57
+ import { applyTheme } from '@jasonshimmy/cer-material/theme';
58
+ applyTheme();
59
+ ```
60
+
61
+ ---
62
+
63
+ ## Two-way bindings (`:model`)
64
+
65
+ All stateful components emit `update:*` events enabling concise two-way data binding with `:model` — no need to manually pair a `:prop` setter with a `@event` handler.
66
+
67
+ | Syntax | Syncs | Components |
68
+ |---|---|---|
69
+ | `:model="${ref}"` | `value` | `md-text-field`, `md-slider`, `md-search`, `md-date-picker`, `md-time-picker` |
70
+ | `:model:checked="${ref}"` | `checked` | `md-checkbox` |
71
+ | `:model:selected="${ref}"` | `selected` | `md-switch`, `md-chip` (filter), `md-icon-button` (toggle), `md-segmented-button` |
72
+ | `:model:activeTab="${ref}"` | active tab id | `md-tabs` |
73
+ | `:model:active="${ref}"` | active item id | `md-navigation-bar`, `md-navigation-rail`, `md-navigation-drawer` |
74
+ | `:model:open="${ref}"` | open / visible | `md-dialog`, `md-menu`, `md-snackbar`, `md-bottom-sheet`, `md-side-sheet`, `md-fab-menu`, `md-navigation-drawer`, `md-date-picker`, `md-time-picker` |
75
+
76
+ All original `change`, `close`, `tab-change`, and other events still fire for backward compatibility — `:model` is additive.
77
+
78
+ ```ts
79
+ // Verbose (still works)
80
+ <md-text-field :value="${email}" @change="${e => email = e.detail}"></md-text-field>
81
+
82
+ // Concise with :model
83
+ <md-text-field :model="${email}"></md-text-field>
84
+ ```
85
+
86
+ > **Radio groups**: `:model:checked` writes the radio's `value` string to the ref when selected, but does not derive the `checked` boolean. Use `:checked="${selected === radio.value}"` for the display state alongside `@change` for radio groups.
87
+
88
+ ---
89
+
90
+ ## Design tokens
91
+
92
+ The theme is injected as `document.adoptedStyleSheets` and exposes every MD3 system token as a CSS custom property. Override them on `:root` to customise the palette:
93
+
94
+ ```css
95
+ :root {
96
+ --md-sys-color-primary: #0057b8;
97
+ --md-sys-color-on-primary: #ffffff;
98
+ --md-sys-color-primary-container: #d6e4ff;
99
+ }
100
+ ```
101
+
102
+ Dark mode is handled automatically via `@media (prefers-color-scheme: dark)`.
103
+
104
+ **Available token groups:**
105
+
106
+ | Group | Example properties |
107
+ |---|---|
108
+ | Primary | `--md-sys-color-primary`, `--md-sys-color-on-primary`, `--md-sys-color-primary-container` |
109
+ | Secondary | `--md-sys-color-secondary`, `--md-sys-color-secondary-container` |
110
+ | Tertiary | `--md-sys-color-tertiary`, `--md-sys-color-tertiary-container` |
111
+ | Error | `--md-sys-color-error`, `--md-sys-color-error-container` |
112
+ | Surface | `--md-sys-color-surface`, `--md-sys-color-surface-variant`, `--md-sys-color-surface-container` |
113
+ | Outline | `--md-sys-color-outline`, `--md-sys-color-outline-variant` |
114
+ | Elevation | `--md-sys-elevation-1` … `--md-sys-elevation-4` |
115
+ | Typography | `--md-sys-typescale-font` |
116
+ | Shape | `--md-sys-shape-corner-none` … `--md-sys-shape-corner-full` |
117
+
118
+ ---
119
+
120
+ ## Components
121
+
122
+ ### `<md-app-bar>`
123
+
124
+ MD3 top app bar with four layout variants, a leading navigation icon, title area, and trailing action icons.
125
+
126
+ | Prop | Type | Default | Description |
127
+ |---|---|---|---|
128
+ | `variant` | `'small' \| 'medium' \| 'large' \| 'center'` | `'small'` | Layout and title size |
129
+ | `title` | `string` | `''` | Title text |
130
+ | `leading-icon` | `string` | `'menu'` | Material Symbol for the leading nav button |
131
+ | `trailing-icons` | `string[]` | `[]` | Array of Material Symbol names for trailing action buttons |
132
+ | `scrolled` | `boolean` | `false` | Applies elevated/tinted scroll state |
133
+
134
+ **Slots:** `title` — custom title content; `trailing` — completely custom trailing area.
135
+
136
+ **Events:** `nav` — leading icon clicked; `action` `(detail: string)` — the clicked icon name.
137
+
138
+ ```html
139
+ <md-app-bar
140
+ variant="small"
141
+ title="Inbox"
142
+ leading-icon="menu"
143
+ :bind="${{ trailingIcons: ['search', 'more_vert'] }}"
144
+ @nav="${openDrawer}"
145
+ @action="${handleAction}"
146
+ ></md-app-bar>
147
+ ```
148
+
149
+ ---
150
+
151
+ ### `<md-badge>`
152
+
153
+ Overlays a numeric label or a small dot indicator on top of slotted content.
154
+
155
+ | Prop | Type | Default | Description |
156
+ |---|---|---|---|
157
+ | `value` | `string \| number` | `''` | Badge text; empty renders a small dot |
158
+ | `small` | `boolean` | `false` | Forces the small dot style regardless of `value` |
159
+
160
+ **Slots:** default — the element the badge attaches to.
161
+
162
+ ```html
163
+ <md-badge value="3">
164
+ <md-icon-button icon="notifications"></md-icon-button>
165
+ </md-badge>
166
+ ```
167
+
168
+ ---
169
+
170
+ ### `<md-bottom-sheet>`
171
+
172
+ Slide-up bottom sheet with optional drag-to-dismiss, focus trap, and scroll lock.
173
+
174
+ | Prop | Type | Default | Description |
175
+ |---|---|---|---|
176
+ | `open` | `boolean` | `false` | Shows/hides the sheet |
177
+ | `headline` | `string` | `''` | Sheet header text |
178
+ | `show-handle` | `boolean` | `true` | Renders the drag handle pill |
179
+ | `variant` | `'standard' \| 'modal'` | `'standard'` | `modal` adds a scrim overlay |
180
+
181
+ **Slots:** default — sheet body content.
182
+
183
+ **Events:** `close` — sheet dismissed.
184
+
185
+ ```html
186
+ <md-bottom-sheet
187
+ :model:open="${isOpen}"
188
+ headline="Options"
189
+ variant="modal"
190
+ >
191
+ <p>Your content here</p>
192
+ </md-bottom-sheet>
193
+ ```
194
+
195
+ ---
196
+
197
+ ### `<md-button>`
198
+
199
+ MD3 button in five style variants with optional leading icon.
200
+
201
+ | Prop | Type | Default | Description |
202
+ |---|---|---|---|
203
+ | `variant` | `'filled' \| 'outlined' \| 'text' \| 'elevated' \| 'tonal'` | `'filled'` | Visual style |
204
+ | `label` | `string` | `''` | Button text |
205
+ | `icon` | `string` | `''` | Leading Material Symbol name |
206
+ | `type` | `'button' \| 'submit' \| 'reset'` | `'button'` | Native button type |
207
+ | `disabled` | `boolean` | `false` | Disables the button |
208
+
209
+ ```html
210
+ <md-button variant="filled" label="Save" icon="save"></md-button>
211
+ <md-button variant="outlined" label="Cancel"></md-button>
212
+ <md-button variant="text" label="Learn more"></md-button>
213
+ ```
214
+
215
+ ---
216
+
217
+ ### `<md-button-group>`
218
+
219
+ Horizontally connected group of buttons sharing a unified variant style.
220
+
221
+ | Prop | Type | Default | Description |
222
+ |---|---|---|---|
223
+ | `variant` | `'filled' \| 'outlined' \| 'tonal' \| 'text' \| 'elevated'` | `'outlined'` | Shared style for all items |
224
+ | `disabled` | `boolean` | `false` | Disables all buttons |
225
+ | `items` | `{ id: string; label: string; icon?: string; disabled?: boolean }[]` | `[]` | Button definitions |
226
+
227
+ **Events:** `click` `(detail: { id: string; index: number })` — a button was clicked.
228
+
229
+ ```html
230
+ <md-button-group
231
+ variant="outlined"
232
+ :bind="${{ items: [
233
+ { id: 'day', label: 'Day' },
234
+ { id: 'week', label: 'Week' },
235
+ { id: 'month', label: 'Month' },
236
+ ] }}"
237
+ @click="${handleGroupClick}"
238
+ ></md-button-group>
239
+ ```
240
+
241
+ ---
242
+
243
+ ### `<md-card>`
244
+
245
+ MD3 card container that optionally becomes an interactive button with a ripple state layer.
246
+
247
+ | Prop | Type | Default | Description |
248
+ |---|---|---|---|
249
+ | `variant` | `'elevated' \| 'filled' \| 'outlined'` | `'elevated'` | Visual style |
250
+ | `clickable` | `boolean` | `false` | Makes the whole card an accessible button |
251
+
252
+ **Slots:** default — card content.
253
+
254
+ **Events:** `click` — emitted when `clickable` is `true`.
255
+
256
+ ```html
257
+ <md-card variant="outlined" clickable @click="${openDetail}">
258
+ <h3>Card Title</h3>
259
+ <p>Supporting text goes here.</p>
260
+ </md-card>
261
+ ```
262
+
263
+ ---
264
+
265
+ ### `<md-carousel>`
266
+
267
+ Horizontal snap-scroll carousel rendering image or color-placeholder cards with overlay text.
268
+
269
+ | Prop | Type | Default | Description |
270
+ |---|---|---|---|
271
+ | `items` | `{ id: string; headline?: string; supportingText?: string; image?: string; color?: string }[]` | `[]` | Carousel item data |
272
+ | `variant` | `'multi-browse' \| 'uncontained' \| 'full-screen'` | `'multi-browse'` | Layout density |
273
+
274
+ **Events:** `select` `(detail: string)` — the `id` of the clicked card.
275
+
276
+ ```html
277
+ <md-carousel
278
+ variant="multi-browse"
279
+ :bind="${{ items: [
280
+ { id: '1', headline: 'Mountains', image: '/img/mountains.jpg' },
281
+ { id: '2', headline: 'Ocean', color: '#0077b6' },
282
+ ] }}"
283
+ @select="${handleSelect}"
284
+ ></md-carousel>
285
+ ```
286
+
287
+ ---
288
+
289
+ ### `<md-checkbox>`
290
+
291
+ MD3 checkbox with animated check/dash icon and an optional inline text label.
292
+
293
+ | Prop | Type | Default | Description |
294
+ |---|---|---|---|
295
+ | `checked` | `boolean` | `false` | Checked state |
296
+ | `indeterminate` | `boolean` | `false` | Indeterminate (dash) state |
297
+ | `disabled` | `boolean` | `false` | Disables interaction |
298
+ | `label` | `string` | `''` | Inline label text |
299
+
300
+ **Events:** `change` `(detail: boolean)` — new checked state.
301
+
302
+ ```html
303
+ <md-checkbox
304
+ label="Accept terms"
305
+ :model:checked="${accepted}"
306
+ ></md-checkbox>
307
+ ```
308
+
309
+ ---
310
+
311
+ ### `<md-chip>`
312
+
313
+ MD3 chip in four variants — assist, filter (toggle), input (removable), and suggestion.
314
+
315
+ | Prop | Type | Default | Description |
316
+ |---|---|---|---|
317
+ | `variant` | `'assist' \| 'filter' \| 'input' \| 'suggestion'` | `'assist'` | Chip type |
318
+ | `label` | `string` | `''` | Chip text |
319
+ | `icon` | `string` | `''` | Leading Material Symbol |
320
+ | `selected` | `boolean` | `false` | Active state (filter variant) |
321
+ | `disabled` | `boolean` | `false` | Disables interaction |
322
+
323
+ **Events:** `click`; `remove` — the × button was tapped (input variant only).
324
+
325
+ ```html
326
+ <md-chip variant="filter" label="Unread" :model:selected="${filterUnread}"></md-chip>
327
+ <md-chip variant="input" label="jason@example.com" @remove="${removeChip}"></md-chip>
328
+ ```
329
+
330
+ ---
331
+
332
+ ### `<md-date-picker>`
333
+
334
+ Full MD3 date picker with calendar grid, month/year navigation, optional date range, and min/max constraints.
335
+
336
+ | Prop | Type | Default | Description |
337
+ |---|---|---|---|
338
+ | `variant` | `'dialog' \| 'docked'` | `'dialog'` | Modal dialog or inline docked layout |
339
+ | `value` | `string` | `''` | Selected date (`YYYY-MM-DD`) |
340
+ | `range-start` | `string` | `''` | Range start date (`YYYY-MM-DD`) |
341
+ | `range-end` | `string` | `''` | Range end date (`YYYY-MM-DD`) |
342
+ | `min` | `string` | `''` | Minimum selectable date |
343
+ | `max` | `string` | `''` | Maximum selectable date |
344
+ | `open` | `boolean` | `false` | Controls visibility |
345
+ | `label` | `string` | `'Select date'` | Trigger field label |
346
+ | `aria-label` | `string` | `'Date picker'` | Accessible label |
347
+
348
+ **Events:** `change` `(detail: string)` — selected date in `YYYY-MM-DD` format; `close`.
349
+
350
+ ```html
351
+ <md-date-picker
352
+ label="Start date"
353
+ :model="${startDate}"
354
+ min="2024-01-01"
355
+ :model:open="${pickerOpen}"
356
+ ></md-date-picker>
357
+ ```
358
+
359
+ ---
360
+
361
+ ### `<md-dialog>`
362
+
363
+ MD3 modal dialog with scale-in animation, optional icon/headline, scrollable body, and an actions slot.
364
+
365
+ | Prop | Type | Default | Description |
366
+ |---|---|---|---|
367
+ | `open` | `boolean` | `false` | Shows/hides the dialog |
368
+ | `headline` | `string` | `''` | Dialog title |
369
+ | `icon` | `string` | `''` | Leading Material Symbol in the header |
370
+
371
+ **Slots:** default — scrollable body content; `actions` — action buttons row.
372
+
373
+ **Events:** `close` — dialog dismissed.
374
+
375
+ ```html
376
+ <md-dialog :model:open="${dialogOpen}" headline="Delete item?">
377
+ <p>This action cannot be undone.</p>
378
+ <div slot="actions">
379
+ <md-button variant="text" label="Cancel" @click="${() => dialogOpen = false}"></md-button>
380
+ <md-button variant="filled" label="Delete" @click="${confirmDelete}"></md-button>
381
+ </div>
382
+ </md-dialog>
383
+ ```
384
+
385
+ ---
386
+
387
+ ### `<md-divider>`
388
+
389
+ Thin horizontal or vertical separator rule with inset variants for list and layout use.
390
+
391
+ | Prop | Type | Default | Description |
392
+ |---|---|---|---|
393
+ | `inset` | `boolean` | `false` | Insets both ends |
394
+ | `inset-start` | `boolean` | `false` | Insets the leading end only |
395
+ | `inset-end` | `boolean` | `false` | Insets the trailing end only |
396
+ | `vertical` | `boolean` | `false` | Renders as a vertical rule |
397
+
398
+ ```html
399
+ <md-divider></md-divider>
400
+ <md-divider inset-start></md-divider>
401
+ <md-divider vertical></md-divider>
402
+ ```
403
+
404
+ ---
405
+
406
+ ### `<md-fab>`
407
+
408
+ MD3 Floating Action Button in four color variants, four sizes, and an extended (icon + label) pill form.
409
+
410
+ | Prop | Type | Default | Description |
411
+ |---|---|---|---|
412
+ | `variant` | `'primary' \| 'secondary' \| 'tertiary' \| 'surface'` | `'primary'` | Background color role |
413
+ | `size` | `'small' \| 'regular' \| 'medium' \| 'large'` | `'regular'` | Button size |
414
+ | `icon` | `string` | `'add'` | Material Symbol |
415
+ | `label` | `string` | `''` | Non-empty value makes it an extended FAB |
416
+ | `lowered` | `boolean` | `false` | Reduces elevation |
417
+ | `aria-label` | `string` | `''` | Accessible label |
418
+
419
+ **Events:** `click`
420
+
421
+ ```html
422
+ <md-fab icon="edit" variant="primary"></md-fab>
423
+ <md-fab icon="add" label="Compose" variant="secondary" size="regular"></md-fab>
424
+ ```
425
+
426
+ ---
427
+
428
+ ### `<md-fab-menu>`
429
+
430
+ FAB speed dial that expands upward to reveal labeled action items. Only one instance can be open at a time.
431
+
432
+ | Prop | Type | Default | Description |
433
+ |---|---|---|---|
434
+ | `icon` | `string` | `'add'` | Material Symbol for the collapsed state |
435
+ | `close-icon` | `string` | `'close'` | Material Symbol for the expanded state |
436
+ | `variant` | `'primary' \| 'secondary' \| 'tertiary'` | `'primary'` | FAB color role |
437
+ | `open` | `boolean` | `false` | Expanded state |
438
+ | `items` | `{ id: string; icon: string; label: string; disabled?: boolean }[]` | `[]` | Action item definitions |
439
+ | `aria-label` | `string` | `'Speed dial'` | Accessible label |
440
+
441
+ **Events:** `open`; `close`; `select` `(detail: { id: string })` — an item was chosen.
442
+
443
+ ```html
444
+ <md-fab-menu
445
+ :model:open="${fabOpen}"
446
+ :bind="${{ items: [
447
+ { id: 'share', icon: 'share', label: 'Share' },
448
+ { id: 'copy', icon: 'content_copy', label: 'Copy link' },
449
+ ] }}"
450
+ @select="${handleFabSelect}"
451
+ ></md-fab-menu>
452
+ ```
453
+
454
+ ---
455
+
456
+ ### `<md-icon-button>`
457
+
458
+ MD3 icon button with four style variants and optional toggle (pressed/selected) behaviour.
459
+
460
+ | Prop | Type | Default | Description |
461
+ |---|---|---|---|
462
+ | `variant` | `'standard' \| 'filled' \| 'tonal' \| 'outlined'` | `'standard'` | Visual style |
463
+ | `icon` | `string` | `'more_vert'` | Material Symbol |
464
+ | `selected` | `boolean` | `false` | Pressed/selected state (toggle mode) |
465
+ | `selected-icon` | `string` | `''` | Alternate symbol shown when selected |
466
+ | `toggle` | `boolean` | `false` | Enables toggle behaviour |
467
+ | `disabled` | `boolean` | `false` | Disables interaction |
468
+ | `aria-label` | `string` | `''` | Accessible label |
469
+
470
+ **Events:** `click`; `change` `(detail: boolean)` — new selected state (toggle mode).
471
+
472
+ ```html
473
+ <md-icon-button icon="favorite_border" selected-icon="favorite" toggle :model:selected="${isFav}"></md-icon-button>
474
+ <md-icon-button icon="delete" variant="outlined" @click="${onDelete}"></md-icon-button>
475
+ ```
476
+
477
+ ---
478
+
479
+ ### `<md-list>` / `<md-list-item>`
480
+
481
+ MD3 list container and individual list items with leading/trailing content, headlines, supporting text, and selected/disabled states.
482
+
483
+ #### `<md-list>`
484
+
485
+ | Prop | Type | Default | Description |
486
+ |---|---|---|---|
487
+ | `role` | `string` | `'list'` | ARIA role |
488
+
489
+ **Slots:** default — `<md-list-item>` children.
490
+
491
+ #### `<md-list-item>`
492
+
493
+ | Prop | Type | Default | Description |
494
+ |---|---|---|---|
495
+ | `headline` | `string` | `''` | Primary text |
496
+ | `supporting-text` | `string` | `''` | Secondary text |
497
+ | `leading-icon` | `string` | `''` | Leading Material Symbol |
498
+ | `trailing-icon` | `string` | `''` | Trailing Material Symbol |
499
+ | `trailing-supporting-text` | `string` | `''` | Trailing metadata text |
500
+ | `disabled` | `boolean` | `false` | Disables the item |
501
+ | `selected` | `boolean` | `false` | Highlights the item as active |
502
+ | `type` | `'text' \| 'link' \| 'checkbox' \| 'radio'` | `'text'` | Item interaction type |
503
+
504
+ **Slots:** `leading` — custom leading content; default — inline content after headline; `trailing` — custom trailing content.
505
+
506
+ **Events:** `click`
507
+
508
+ ```html
509
+ <md-list>
510
+ <md-list-item headline="Inbox" leading-icon="inbox" trailing-supporting-text="24"></md-list-item>
511
+ <md-list-item headline="Sent" leading-icon="send" selected></md-list-item>
512
+ <md-list-item headline="Trash" leading-icon="delete" disabled></md-list-item>
513
+ </md-list>
514
+ ```
515
+
516
+ ---
517
+
518
+ ### `<md-loading-indicator>`
519
+
520
+ MD3 four-color indeterminate circular spinner.
521
+
522
+ | Prop | Type | Default | Description |
523
+ |---|---|---|---|
524
+ | `size` | `'small' \| 'medium' \| 'large'` | `'medium'` | Spinner size |
525
+ | `aria-label` | `string` | `'Loading'` | Accessible announcement text |
526
+
527
+ ```html
528
+ <md-loading-indicator size="large"></md-loading-indicator>
529
+ ```
530
+
531
+ ---
532
+
533
+ ### `<md-menu>`
534
+
535
+ Contextual dropdown menu with four anchor positions, keyboard navigation, and divider support.
536
+
537
+ | Prop | Type | Default | Description |
538
+ |---|---|---|---|
539
+ | `open` | `boolean` | `false` | Shows/hides the menu |
540
+ | `items` | `{ id: string; label: string; icon?: string; disabled?: boolean; divider?: boolean }[]` | `[]` | Menu item definitions |
541
+ | `anchor` | `'bottom-start' \| 'bottom-end' \| 'top-start' \| 'top-end'` | `'bottom-start'` | Popup position relative to the trigger |
542
+
543
+ **Slots:** `trigger` — the element that opens the menu.
544
+
545
+ **Events:** `select` `(detail: string)` — the `id` of the chosen item; `close`.
546
+
547
+ ```html
548
+ <md-menu
549
+ :model:open="${menuOpen}"
550
+ :bind="${{ items: [
551
+ { id: 'edit', label: 'Edit', icon: 'edit' },
552
+ { id: 'dup', label: 'Duplicate', icon: 'content_copy' },
553
+ { id: 'del', label: 'Delete', icon: 'delete', divider: true },
554
+ ] }}"
555
+ @select="${handleMenuSelect}"
556
+ >
557
+ <md-icon-button slot="trigger" icon="more_vert" @click="${() => menuOpen = !menuOpen}"></md-icon-button>
558
+ </md-menu>
559
+ ```
560
+
561
+ ---
562
+
563
+ ### `<md-navigation-bar>`
564
+
565
+ MD3 bottom navigation bar for mobile with active pill indicator, filled icon for the active item, and badge support.
566
+
567
+ | Prop | Type | Default | Description |
568
+ |---|---|---|---|
569
+ | `items` | `{ id: string; label: string; icon: string; badge?: string }[]` | `[]` | Destination definitions |
570
+ | `active` | `string` | `''` | `id` of the active destination |
571
+
572
+ **Events:** `change` `(detail: string)` — the `id` of the selected destination.
573
+
574
+ ```html
575
+ <md-navigation-bar
576
+ :model:active="${activeTab}"
577
+ :bind="${{ items: [
578
+ { id: 'home', label: 'Home', icon: 'home' },
579
+ { id: 'search', label: 'Search', icon: 'search' },
580
+ { id: 'profile', label: 'Profile', icon: 'person', badge: '2' },
581
+ ] }}"
582
+ ></md-navigation-bar>
583
+ ```
584
+
585
+ ---
586
+
587
+ ### `<md-navigation-drawer>`
588
+
589
+ MD3 navigation drawer in standard (in-layout, width-animated) or modal (slide-in overlay) form with section headers and dividers.
590
+
591
+ | Prop | Type | Default | Description |
592
+ |---|---|---|---|
593
+ | `open` | `boolean` | `false` | Shows/hides the drawer |
594
+ | `headline` | `string` | `''` | Header text |
595
+ | `variant` | `'standard' \| 'modal'` | `'standard'` | `modal` adds a scrim |
596
+ | `items` | `{ id?: string; label?: string; icon?: string; section?: string; divider?: boolean; disabled?: boolean }[]` | `[]` | Navigation item definitions |
597
+ | `active` | `string` | `''` | `id` of the active item |
598
+
599
+ **Slots:** default — extra content below the item list.
600
+
601
+ **Events:** `close`; `change` `(detail: string)` — the `id` of the selected item.
602
+
603
+ ```html
604
+ <md-navigation-drawer
605
+ variant="modal"
606
+ headline="Mail"
607
+ :model:open="${drawerOpen}"
608
+ :model:active="${currentRoute}"
609
+ :bind="${{ items: navItems }}"
610
+ @change="${e => navigate(e.detail)}"
611
+ ></md-navigation-drawer>
612
+ ```
613
+
614
+ ---
615
+
616
+ ### `<md-navigation-rail>`
617
+
618
+ MD3 vertical navigation rail for tablet/desktop with optional top FAB and hamburger menu icon.
619
+
620
+ | Prop | Type | Default | Description |
621
+ |---|---|---|---|
622
+ | `items` | `{ id: string; label: string; icon: string; badge?: string }[]` | `[]` | Destination definitions |
623
+ | `active` | `string` | `''` | `id` of the active destination |
624
+ | `fab` | `boolean` | `false` | Shows a FAB at the top |
625
+ | `fab-icon` | `string` | `'add'` | FAB Material Symbol |
626
+ | `menu-icon` | `boolean` | `false` | Shows a hamburger menu icon |
627
+
628
+ **Events:** `change` `(detail: string)` — the `id` of the selected destination; `fab-click`; `menu-click`.
629
+
630
+ ```html
631
+ <md-navigation-rail
632
+ :model:active="${activeRoute}"
633
+ menu-icon
634
+ :bind="${{ items: [
635
+ { id: 'inbox', label: 'Inbox', icon: 'inbox' },
636
+ { id: 'sent', label: 'Sent', icon: 'send' },
637
+ ] }}"
638
+ @change="${e => navigate(e.detail)}"
639
+ ></md-navigation-rail>
640
+ ```
641
+
642
+ ---
643
+
644
+ ### `<md-progress>`
645
+
646
+ MD3 progress indicator — linear (with buffer track) or circular — in determinate or indeterminate mode.
647
+
648
+ | Prop | Type | Default | Description |
649
+ |---|---|---|---|
650
+ | `variant` | `'linear' \| 'circular'` | `'linear'` | Indicator shape |
651
+ | `value` | `number` | `0` | Progress value (0–100) |
652
+ | `indeterminate` | `boolean` | `false` | Animated looping mode |
653
+ | `buffer` | `number` | `100` | Buffer track value 0–100 (linear only) |
654
+ | `aria-label` | `string` | `'Loading'` | Accessible label |
655
+
656
+ ```html
657
+ <md-progress variant="linear" value="65"></md-progress>
658
+ <md-progress variant="circular" indeterminate></md-progress>
659
+ ```
660
+
661
+ ---
662
+
663
+ ### `<md-radio>`
664
+
665
+ MD3 radio button with animated inner circle, `name`/`value` for grouping, and an optional inline label.
666
+
667
+ | Prop | Type | Default | Description |
668
+ |---|---|---|---|
669
+ | `checked` | `boolean` | `false` | Selected state |
670
+ | `disabled` | `boolean` | `false` | Disables interaction |
671
+ | `name` | `string` | `''` | Radio group name |
672
+ | `value` | `string` | `''` | Form value |
673
+ | `label` | `string` | `''` | Inline label text |
674
+
675
+ **Events:** `change` `(detail: string)` — the `value` of the selected radio.
676
+
677
+ ```html
678
+ <md-radio name="size" value="s" label="Small"></md-radio>
679
+ <md-radio name="size" value="m" label="Medium" checked></md-radio>
680
+ <md-radio name="size" value="l" label="Large"></md-radio>
681
+ ```
682
+
683
+ ---
684
+
685
+ ### `<md-search>`
686
+
687
+ MD3 search bar with a leading icon, animated clear button, and optional avatar.
688
+
689
+ | Prop | Type | Default | Description |
690
+ |---|---|---|---|
691
+ | `value` | `string` | `''` | Current input value |
692
+ | `placeholder` | `string` | `'Search'` | Placeholder text |
693
+ | `leading-icon` | `string` | `'search'` | Material Symbol for the leading icon |
694
+ | `show-avatar` | `boolean` | `false` | Renders an avatar button on the trailing end |
695
+
696
+ **Events:** `clear`; `search` `(detail: string)` — the search query when Enter is pressed.
697
+
698
+ ```html
699
+ <md-search
700
+ placeholder="Search contacts"
701
+ :model="${query}"
702
+ @search="${e => runSearch(e.detail)}"
703
+ ></md-search>
704
+ ```
705
+
706
+ ---
707
+
708
+ ### `<md-segmented-button>`
709
+
710
+ MD3 segmented button group for single-select or multi-select toggle behaviour.
711
+
712
+ | Prop | Type | Default | Description |
713
+ |---|---|---|---|
714
+ | `segments` | `{ id: string; label?: string; icon?: string; disabled?: boolean }[]` | `[]` | Segment definitions |
715
+ | `selected` | `string \| string[]` | `''` | Selected segment `id` (or array for multi-select) |
716
+ | `multiselect` | `boolean` | `false` | Allows multiple selections |
717
+ | `aria-label` | `string` | `''` | Accessible group label |
718
+
719
+ **Events:** `change` `(detail: string | string[])` — selected segment `id` or array of `id`s (multiselect).
720
+
721
+ ```html
722
+ <md-segmented-button
723
+ :bind="${{ segments: [
724
+ { id: 'day', label: 'Day' },
725
+ { id: 'week', label: 'Week' },
726
+ { id: 'month', label: 'Month' },
727
+ ] }}"
728
+ :model:selected="${view}"
729
+ ></md-segmented-button>
730
+ ```
731
+
732
+ ---
733
+
734
+ ### `<md-side-sheet>`
735
+
736
+ MD3 side sheet — standard (in-layout, right-side panel) or modal (slide-in from the right with scrim).
737
+
738
+ | Prop | Type | Default | Description |
739
+ |---|---|---|---|
740
+ | `open` | `boolean` | `false` | Shows/hides the sheet |
741
+ | `headline` | `string` | `''` | Header title text |
742
+ | `variant` | `'standard' \| 'modal'` | `'standard'` | Layout mode |
743
+ | `divider` | `boolean` | `true` | Renders a left-edge border |
744
+
745
+ **Slots:** default — sheet body content.
746
+
747
+ **Events:** `close`; `back` — header back button clicked (modal variant).
748
+
749
+ ```html
750
+ <md-side-sheet variant="modal" headline="Details" :model:open="${sheetOpen}">
751
+ <p>Side panel content</p>
752
+ </md-side-sheet>
753
+ ```
754
+
755
+ ---
756
+
757
+ ### `<md-slider>`
758
+
759
+ MD3 range slider with a custom-styled track, optional floating value label, and optional tick marks.
760
+
761
+ | Prop | Type | Default | Description |
762
+ |---|---|---|---|
763
+ | `min` | `number` | `0` | Minimum value |
764
+ | `max` | `number` | `100` | Maximum value |
765
+ | `value` | `number` | `50` | Current value |
766
+ | `step` | `number` | `1` | Step increment |
767
+ | `disabled` | `boolean` | `false` | Disables the slider |
768
+ | `labeled` | `boolean` | `false` | Shows a floating value bubble on drag |
769
+ | `ticks` | `boolean` | `false` | Renders tick marks at each step |
770
+ | `aria-label` | `string` | `''` | Accessible label |
771
+
772
+ ```html
773
+ <md-slider min="0" max="50" step="5" labeled :model="${vol}"></md-slider>
774
+ ```
775
+
776
+ ---
777
+
778
+ ### `<md-snackbar>`
779
+
780
+ MD3 snackbar that slides up from the bottom with an optional action button.
781
+
782
+ | Prop | Type | Default | Description |
783
+ |---|---|---|---|
784
+ | `open` | `boolean` | `false` | Shows/hides the snackbar |
785
+ | `message` | `string` | `''` | Message text |
786
+ | `action-label` | `string` | `''` | Label for the optional action button; empty hides it |
787
+
788
+ **Events:** `action` — action button clicked; `close` — dismiss button clicked.
789
+
790
+ ```html
791
+ <md-snackbar
792
+ :model:open="${showSnack}"
793
+ message="Item deleted"
794
+ action-label="Undo"
795
+ @action="${undoDelete}"
796
+ ></md-snackbar>
797
+ ```
798
+
799
+ ---
800
+
801
+ ### `<md-split-button>`
802
+
803
+ MD3 split button combining a primary action and an arrow-toggle dropdown for secondary actions.
804
+
805
+ | Prop | Type | Default | Description |
806
+ |---|---|---|---|
807
+ | `label` | `string` | `'Action'` | Primary button label |
808
+ | `icon` | `string` | `''` | Leading Material Symbol on the primary button |
809
+ | `variant` | `'filled' \| 'outlined' \| 'tonal'` | `'filled'` | Visual style |
810
+ | `disabled` | `boolean` | `false` | Disables both sections |
811
+ | `items` | `{ id: string; label: string; icon?: string; disabled?: boolean }[]` | `[]` | Dropdown action definitions |
812
+
813
+ **Events:** `click` — primary button pressed; `select` `(detail: { id: string })` — dropdown item chosen.
814
+
815
+ ```html
816
+ <md-split-button
817
+ label="Save"
818
+ icon="save"
819
+ :bind="${{ items: [
820
+ { id: 'save-draft', label: 'Save as draft' },
821
+ { id: 'save-copy', label: 'Save a copy' },
822
+ ] }}"
823
+ @click="${save}"
824
+ @select="${handleSplitSelect}"
825
+ ></md-split-button>
826
+ ```
827
+
828
+ ---
829
+
830
+ ### `<md-switch>`
831
+
832
+ MD3 toggle switch with animated thumb, state-layer ripple, and optional check/close thumb icons.
833
+
834
+ | Prop | Type | Default | Description |
835
+ |---|---|---|---|
836
+ | `selected` | `boolean` | `false` | On/off state |
837
+ | `disabled` | `boolean` | `false` | Disables interaction |
838
+ | `icons` | `boolean` | `false` | Renders a check icon when on and a close icon when off |
839
+
840
+ **Events:** `change` `(detail: boolean)` — new selected state.
841
+
842
+ ```html
843
+ <md-switch :model:selected="${darkMode}" icons></md-switch>
844
+ ```
845
+
846
+ ---
847
+
848
+ ### `<md-tabs>`
849
+
850
+ MD3 tabbed navigation with an active indicator bar, icon and badge support, keyboard navigation, and a single panel slot.
851
+
852
+ | Prop | Type | Default | Description |
853
+ |---|---|---|---|
854
+ | `variant` | `'primary' \| 'secondary'` | `'primary'` | Tab style (`primary` has icons above labels; `secondary` is text-only) |
855
+ | `tabs` | `{ id: string; label: string; icon?: string; badge?: string }[]` | `[]` | Tab definitions |
856
+ | `active-tab` | `string` | `''` | `id` of the active tab |
857
+
858
+ **Slots:** default — the active tab panel content.
859
+
860
+ **Events:** `tab-change` `(detail: string)` — the `id` of the newly active tab.
861
+
862
+ ```html
863
+ <md-tabs
864
+ variant="primary"
865
+ :model:activeTab="${activeTab}"
866
+ :bind="${{ tabs: [
867
+ { id: 'flights', label: 'Flights', icon: 'flight' },
868
+ { id: 'hotels', label: 'Hotels', icon: 'hotel' },
869
+ ] }}"
870
+ >
871
+ <!-- render panel based on activeTab -->
872
+ </md-tabs>
873
+ ```
874
+
875
+ ---
876
+
877
+ ### `<md-text-field>`
878
+
879
+ MD3 text field (filled or outlined) with animated floating label, leading/trailing icons, error state, and supporting text.
880
+
881
+ | Prop | Type | Default | Description |
882
+ |---|---|---|---|
883
+ | `variant` | `'filled' \| 'outlined'` | `'filled'` | Visual style |
884
+ | `label` | `string` | `'Label'` | Floating label text |
885
+ | `value` | `string` | `''` | Input value |
886
+ | `type` | `string` | `'text'` | Native input type |
887
+ | `placeholder` | `string` | `''` | Placeholder text (shown when no label floats) |
888
+ | `disabled` | `boolean` | `false` | Disables the field |
889
+ | `error` | `boolean` | `false` | Applies error styling |
890
+ | `error-text` | `string` | `''` | Error helper text beneath the field |
891
+ | `supporting-text` | `string` | `''` | Helper text beneath the field (non-error) |
892
+ | `leading-icon` | `string` | `''` | Leading Material Symbol |
893
+ | `trailing-icon` | `string` | `''` | Trailing Material Symbol |
894
+ | `required` | `boolean` | `false` | Marks the field as required |
895
+ | `readonly` | `boolean` | `false` | Makes the field read-only |
896
+
897
+ ```html
898
+ <md-text-field
899
+ variant="outlined"
900
+ label="Email"
901
+ type="email"
902
+ leading-icon="mail"
903
+ :model="${email}"
904
+ :error="${!!emailError}"
905
+ :error-text="${emailError}"
906
+ ></md-text-field>
907
+ ```
908
+
909
+ ---
910
+
911
+ ### `<md-time-picker>`
912
+
913
+ MD3 time picker modal with an interactive clock dial or keyboard input mode, AM/PM toggle, and 12/24-hour support.
914
+
915
+ | Prop | Type | Default | Description |
916
+ |---|---|---|---|
917
+ | `variant` | `'dial' \| 'input'` | `'dial'` | Clock dial or text input mode |
918
+ | `value` | `string` | `''` | Selected time (`HH:MM` 24-hour) |
919
+ | `open` | `boolean` | `false` | Controls visibility |
920
+ | `hour24` | `boolean` | `false` | Uses 24-hour format (no AM/PM) |
921
+ | `aria-label` | `string` | `'Time picker'` | Accessible label |
922
+
923
+ **Events:** `change` `(detail: string)` — selected time in `HH:MM` 24-hour format; `close`.
924
+
925
+ ```html
926
+ <md-time-picker
927
+ :model="${meetingTime}"
928
+ :model:open="${pickerOpen}"
929
+ ></md-time-picker>
930
+ ```
931
+
932
+ ---
933
+
934
+ ### `<md-tooltip>`
935
+
936
+ MD3 tooltip shown on hover/focus — plain (small floating label) or rich (card with title, body, and optional action).
937
+
938
+ | Prop | Type | Default | Description |
939
+ |---|---|---|---|
940
+ | `text` | `string` | `''` | Tooltip body text |
941
+ | `variant` | `'plain' \| 'rich'` | `'plain'` | Plain label or rich card |
942
+ | `title` | `string` | `''` | Rich tooltip title |
943
+ | `action` | `string` | `''` | Rich tooltip action button label |
944
+
945
+ **Slots:** default — the anchor element that triggers the tooltip.
946
+
947
+ **Events:** `action` — rich tooltip action button clicked.
948
+
949
+ ```html
950
+ <!-- Plain tooltip -->
951
+ <md-tooltip text="Delete permanently">
952
+ <md-icon-button icon="delete"></md-icon-button>
953
+ </md-tooltip>
954
+
955
+ <!-- Rich tooltip -->
956
+ <md-tooltip variant="rich" title="Formatting" text="Adjust text size, weight, and alignment." action="Learn more" @action="${openDocs}">
957
+ <md-icon-button icon="format_size"></md-icon-button>
958
+ </md-tooltip>
959
+ ```
960
+
961
+ ---
962
+
963
+ ## Composables
964
+
965
+ Advanced consumers can re-use the internal composable utilities directly. All are exported from `cer-material`:
966
+
967
+ | Export | Description |
968
+ |---|---|
969
+ | `useControlledValue(getProp)` | Syncs an internal reactive value to a prop, returning a `ReactiveState`. |
970
+ | `useEscapeKey(guard, onEscape)` | Registers a global Escape key listener with an active-guard callback. Cleans up automatically. |
971
+ | `createFocusReturn()` | Captures the current focus target and restores it when `.return()` is called. |
972
+ | `createFocusTrap()` | Traps keyboard focus within a DOM element; call `.activate(el)` and `.deactivate()`. |
973
+ | `useListKeyNav(options)` | Adds keyboard arrow-key navigation for a list of DOM items. |
974
+ | `useScrollLock()` | Returns `{ lock(), unlock() }` to prevent `<body>` scrolling while an overlay is open. |
975
+
976
+ ---
977
+
978
+ ## Browser support
979
+
980
+ All evergreen browsers supporting:
981
+ - [Custom Elements v1](https://caniuse.com/custom-elementsv1)
982
+ - [Shadow DOM v1](https://caniuse.com/shadowdomv1)
983
+ - [`CSSStyleSheet.replaceSync`](https://caniuse.com/mdn-api_cssstylesheet_replacesync) (for theme tokens)
984
+
985
+ ---
986
+
987
+ ## License
988
+
989
+ MIT