material-inspired-component-library 7.0.2 → 8.0.1

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 (104) hide show
  1. package/.claude/settings.local.json +14 -0
  2. package/CLAUDE.md +53 -0
  3. package/README.md +6 -0
  4. package/components/accordion/README.md +6 -3
  5. package/components/alert/index.scss +5 -0
  6. package/components/appbar/index.scss +12 -0
  7. package/components/badge/index.scss +2 -0
  8. package/components/bottomsheet/index.scss +9 -0
  9. package/components/button/index.scss +33 -6
  10. package/components/card/README.md +4 -0
  11. package/components/card/index.scss +182 -150
  12. package/components/checkbox/index.scss +28 -6
  13. package/components/datepicker/index.scss +13 -0
  14. package/components/datepicker/index.ts +9 -9
  15. package/components/dialog/index.scss +21 -6
  16. package/components/iconbutton/index.scss +28 -6
  17. package/components/list/README.md +191 -32
  18. package/components/list/index.scss +281 -190
  19. package/components/list/index.ts +100 -100
  20. package/components/menu/README.md +199 -10
  21. package/components/menu/index.scss +242 -47
  22. package/components/menu/index.ts +74 -37
  23. package/components/navigationrail/index.scss +91 -68
  24. package/components/progressindicator/README.md +88 -0
  25. package/components/progressindicator/index.scss +225 -0
  26. package/components/progressindicator/index.ts +77 -0
  27. package/components/radio/index.scss +24 -6
  28. package/components/select/README.md +42 -5
  29. package/components/select/index.scss +45 -79
  30. package/components/shape/README.md +103 -0
  31. package/components/shape/_paths.generated.scss +64 -0
  32. package/components/shape/index.scss +66 -0
  33. package/components/shape/master.scss +28 -0
  34. package/components/sidesheet/index.scss +11 -0
  35. package/components/slider/index.scss +13 -0
  36. package/components/snackbar/index.scss +12 -0
  37. package/components/stepper/index.scss +3 -5
  38. package/components/switch/index.scss +9 -0
  39. package/components/textfield/index.scss +10 -1
  40. package/components/textfield/index.ts +2 -2
  41. package/components/timepicker/index.scss +16 -0
  42. package/dist/alert.css +1 -1
  43. package/dist/appbar.css +1 -1
  44. package/dist/badge.css +1 -1
  45. package/dist/bottomsheet.css +1 -1
  46. package/dist/button.css +1 -1
  47. package/dist/card.css +1 -1
  48. package/dist/checkbox.css +1 -1
  49. package/dist/components/list/index.d.ts +2 -2
  50. package/dist/components/progressindicator/index.d.ts +6 -0
  51. package/dist/datepicker.css +1 -1
  52. package/dist/dialog.css +1 -1
  53. package/dist/divider.css +1 -1
  54. package/dist/foundations/form/index.js +1 -0
  55. package/dist/foundations.css +1 -1
  56. package/dist/iconbutton.css +1 -1
  57. package/dist/layout.css +1 -1
  58. package/dist/list.css +1 -1
  59. package/dist/menu.css +1 -1
  60. package/dist/micl.css +1 -1
  61. package/dist/micl.js +1 -1
  62. package/dist/navigationrail.css +1 -1
  63. package/dist/progressindicator.css +1 -0
  64. package/dist/progressindicator.js +1 -0
  65. package/dist/radio.css +1 -1
  66. package/dist/select.css +1 -1
  67. package/dist/shape.css +1 -0
  68. package/dist/shape.js +1 -0
  69. package/dist/sidesheet.css +1 -1
  70. package/dist/slider.css +1 -1
  71. package/dist/snackbar.css +1 -1
  72. package/dist/stepper.css +1 -1
  73. package/dist/switch.css +1 -1
  74. package/dist/textfield.css +1 -1
  75. package/dist/timepicker.css +1 -1
  76. package/docs/accordion.html +24 -24
  77. package/docs/bottomsheet.html +1 -4
  78. package/docs/datepicker.html +21 -21
  79. package/docs/dialog.html +1 -1
  80. package/docs/index.html +5 -4
  81. package/docs/list.html +38 -22
  82. package/docs/menu.html +246 -41
  83. package/docs/micl.css +1 -1
  84. package/docs/micl.js +1 -1
  85. package/docs/progressindicator.html +288 -0
  86. package/docs/select.html +68 -19
  87. package/docs/shape.css +1 -0
  88. package/docs/shape.js +1 -0
  89. package/docs/shapes.html +150 -0
  90. package/foundations/index.scss +0 -1
  91. package/foundations/layout/README.md +1 -1
  92. package/foundations/layout/index.scss +3 -0
  93. package/micl.ts +8 -1
  94. package/package.json +6 -4
  95. package/styles/README.md +90 -12
  96. package/styles/elevation.scss +46 -13
  97. package/styles/motion.scss +51 -47
  98. package/styles/shapes.scss +41 -26
  99. package/styles/statelayer.scss +93 -36
  100. package/styles/typography.scss +120 -322
  101. package/styles.scss +10 -6
  102. package/tools/shapes/check.mjs +42 -0
  103. package/tools/shapes/generate.mjs +834 -0
  104. package/webpack.config.js +16 -1
@@ -27,6 +27,23 @@
27
27
  @use '../../styles/statelayer';
28
28
  @use '../../styles/typography';
29
29
 
30
+ @include elevation.level(0);
31
+ @include elevation.level(2);
32
+
33
+ @include shapes.corner('large');
34
+
35
+ @include statelayer.token('hover-state-layer-opacity');
36
+ @include statelayer.token('focus-state-layer-opacity');
37
+ @include statelayer.token('pressed-state-layer-opacity');
38
+ @include statelayer.token('backdrop-opacity');
39
+ @include statelayer.token('ripple-opacity-factor');
40
+ @include statelayer.token('ripple-duration');
41
+ @include statelayer.property;
42
+ @include statelayer.keyframes;
43
+
44
+ @include typography.scale('label-large');
45
+ @include typography.scale('label-medium');
46
+
30
47
  .micl-navigationrail {
31
48
  --md-comp-nav-rail-spring-buffer: 100px;
32
49
  --md-comp-nav-rail-divider-thickness: 0px;
@@ -36,19 +53,18 @@
36
53
  --md-comp-nav-rail-morph-duration: #{motion.$md-sys-motion-expressive-fast-spatial-duration};
37
54
  --md-comp-nav-rail-morph-duration-reverse: #{motion.$md-sys-motion-expressive-fast-spatial-duration};
38
55
 
39
- --_current-max-width: var(--md-comp-nav-rail-collapsed-container-width, 96px);
40
- --_current-min-width: var(--md-comp-nav-rail-collapsed-container-width, 96px);
41
- --_item-direction: column;
42
- --_item-width: var(--md-comp-nav-rail-item-short-container-height, 56px);
43
-
44
- --_item-target-height: var(--_item-base-height);
56
+ --_navigationrail-current-max-width: var(--md-comp-nav-rail-collapsed-container-width, 96px);
57
+ --_navigationrail-current-min-width: var(--md-comp-nav-rail-collapsed-container-width, 96px);
58
+ --_navigationrail-item-direction: column;
59
+ --_navigationrail-item-width: var(--md-comp-nav-rail-item-short-container-height, 56px);
60
+ --_navigationrail-item-target-height: var(--_navigationrail-item-base-height);
45
61
 
46
- --_content-gap: var(--md-comp-nav-rail-item-container-vertical-space, 6px);
47
- --_morph-speed: var(--md-comp-nav-rail-morph-duration-reverse);
48
- --_container-delay: calc(var(--_morph-speed) / 2);
49
- --_item-delay: 0ms;
50
- --_text-animation: none;
51
- --_expanded-text-margin: calc(var(--md-comp-nav-rail-item-active-indicator-icon-label-space, 8px) - 16px);
62
+ --_navigationrail-content-gap: var(--md-comp-nav-rail-item-container-vertical-space, 6px);
63
+ --_navigationrail-morph-speed: var(--md-comp-nav-rail-morph-duration-reverse);
64
+ --_navigationrail-container-delay: calc(var(--_navigationrail-morph-speed) / 2);
65
+ --_navigationrail-item-delay: 0ms;
66
+ --_navigationrail-text-animation: none;
67
+ --_navigationrail-expanded-text-margin: calc(var(--md-comp-nav-rail-item-active-indicator-icon-label-space, 8px) - 16px);
52
68
 
53
69
  box-sizing: border-box;
54
70
  display: flex;
@@ -70,11 +86,11 @@
70
86
  interpolate-size: allow-keywords;
71
87
 
72
88
  transition:
73
- min-inline-size var(--_morph-speed) var(--_container-delay) linear,
74
- max-inline-size var(--_morph-speed) var(--_container-delay) linear,
75
- padding-block-start var(--_morph-speed) var(--_container-delay) linear;
76
- min-inline-size: var(--_current-min-width);
77
- max-inline-size: var(--_current-max-width);
89
+ min-inline-size var(--_navigationrail-morph-speed) var(--_navigationrail-container-delay) linear,
90
+ max-inline-size var(--_navigationrail-morph-speed) var(--_navigationrail-container-delay) linear,
91
+ padding-block-start var(--_navigationrail-morph-speed) var(--_navigationrail-container-delay) linear;
92
+ min-inline-size: var(--_navigationrail-current-min-width);
93
+ max-inline-size: var(--_navigationrail-current-max-width);
78
94
 
79
95
  &> .micl-navigationrail__headline {
80
96
  padding-inline-start: 28px;
@@ -87,12 +103,12 @@
87
103
  flex: 1 1 auto;
88
104
  align-items: flex-start;
89
105
  inline-size: 100%;
90
- row-gap: var(--_content-gap);
106
+ row-gap: var(--_navigationrail-content-gap);
91
107
  padding-block: var(--md-comp-nav-rail-item-header-space-minimum, 40px) 16px;
92
108
  padding-inline: 20px 0;
93
109
 
94
110
  overflow: hidden auto;
95
- transition: row-gap var(--_morph-speed) linear;
111
+ transition: row-gap var(--_navigationrail-morph-speed) linear;
96
112
 
97
113
  &> a.micl-navigationrail__item:focus-visible {
98
114
  --statelayer-opacity: var(--md-sys-state-focus-state-layer-opacity, 10%);
@@ -101,9 +117,9 @@
101
117
  &> a.micl-navigationrail__item {
102
118
  --micl-ripple: 1;
103
119
  --statelayer-color: var(--md-sys-color-on-secondary-container);
104
- --_item-base-height: var(--md-comp-nav-rail-item-short-container-height, 56px);
105
- --_item-margin-bottom: 0px;
106
- --_item-padding-block: 12px;
120
+ --_navigationrail-item-base-height: var(--md-comp-nav-rail-item-short-container-height, 56px);
121
+ --_navigationrail-item-margin-bottom: 0px;
122
+ --_navigationrail-item-padding-block: 12px;
107
123
 
108
124
  box-sizing: border-box;
109
125
  display: flex;
@@ -117,20 +133,25 @@
117
133
  overflow: visible;
118
134
  background-color: transparent;
119
135
  background-image:
120
- radial-gradient(circle at var(--micl-x, center) var(--micl-y, center), transparent 0%, rgb(from var(--statelayer-color) r g b / var(--statelayer-opacity)) 10%, transparent 10%),
136
+ radial-gradient(
137
+ circle at var(--micl-x, center) var(--micl-y, center),
138
+ transparent 0%,
139
+ rgb(from var(--statelayer-color) r g b / calc(var(--statelayer-opacity) * var(--md-sys-state-ripple-opacity-factor))) 10%,
140
+ transparent 10%
141
+ ),
121
142
  linear-gradient(rgb(from var(--statelayer-color) r g b / var(--statelayer-opacity)));
122
143
  background-repeat: no-repeat;
123
- background-size: 10000%, 100%;
144
+ background-size: 0%, 100%;
124
145
 
125
146
  &:has(.micl-navigationrail__text) {
126
- --_item-base-height: var(--md-comp-nav-rail-item-vertical-active-indicator-height, 32px);
127
- --_item-margin-bottom: 40px;
128
- --_item-padding-block: 0px;
147
+ --_navigationrail-item-base-height: var(--md-comp-nav-rail-item-vertical-active-indicator-height, 32px);
148
+ --_navigationrail-item-margin-bottom: 40px;
149
+ --_navigationrail-item-padding-block: 0px;
129
150
  }
130
151
  &:not(:has(.micl-navigationrail__icon)) {
131
- --_item-base-height: var(--md-comp-nav-rail-item-short-container-height, 56px);
132
- --_item-margin-bottom: 0px;
133
- --_item-padding-block: 0px;
152
+ --_navigationrail-item-base-height: var(--md-comp-nav-rail-item-short-container-height, 56px);
153
+ --_navigationrail-item-margin-bottom: 0px;
154
+ --_navigationrail-item-padding-block: 0px;
134
155
 
135
156
  justify-content: center;
136
157
 
@@ -146,28 +167,28 @@
146
167
  animation: none !important;
147
168
 
148
169
  transition:
149
- padding-inline calc(var(--_morph-speed) / 2) var(--_item-delay) linear,
150
- margin-inline calc(var(--_morph-speed) / 2) var(--_item-delay) linear;
170
+ padding-inline calc(var(--_navigationrail-morph-speed) / 2) var(--_navigationrail-item-delay) linear,
171
+ margin-inline calc(var(--_navigationrail-morph-speed) / 2) var(--_navigationrail-item-delay) linear;
151
172
  }
152
173
  }
153
174
 
154
- inline-size: var(--_item-width);
155
- block-size: var(--_item-target-height, var(--_item-base-height));
156
- margin-block-end: var(--_item-target-margin, var(--_item-margin-bottom));
157
- padding-block: var(--_item-target-padding, var(--_item-padding-block));
158
- flex-direction: var(--_item-direction);
175
+ inline-size: var(--_navigationrail-item-width);
176
+ block-size: var(--_navigationrail-item-target-height, var(--_navigationrail-item-base-height));
177
+ margin-block-end: var(--_navigationrail-item-target-margin, var(--_navigationrail-item-margin-bottom));
178
+ padding-block: var(--_navigationrail-item-target-padding, var(--_navigationrail-item-padding-block));
179
+ flex-direction: var(--_navigationrail-item-direction);
159
180
 
160
- border-radius: calc(var(--_item-target-height, var(--_item-base-height)) / 2);
181
+ border-radius: calc(var(--_navigationrail-item-target-height, var(--_navigationrail-item-base-height)) / 2);
161
182
 
162
183
  transition:
163
- inline-size calc(var(--_morph-speed) / 2) var(--_item-delay) linear,
164
- block-size calc(var(--_morph-speed) / 2) var(--_item-delay) linear,
165
- margin-block-end calc(var(--_morph-speed) / 2) var(--_item-delay) linear,
166
- padding-block calc(var(--_morph-speed) / 2) var(--_item-delay) linear,
167
- border-radius calc(var(--_morph-speed) / 2) var(--_item-delay) linear,
168
- flex-direction 0s calc(var(--_morph-speed) / 2) linear allow-discrete,
169
- background-color calc(var(--_morph-speed) / 2) linear,
170
- background-size 3000ms linear,
184
+ inline-size calc(var(--_navigationrail-morph-speed) / 2) var(--_navigationrail-item-delay) linear,
185
+ block-size calc(var(--_navigationrail-morph-speed) / 2) var(--_navigationrail-item-delay) linear,
186
+ margin-block-end calc(var(--_navigationrail-morph-speed) / 2) var(--_navigationrail-item-delay) linear,
187
+ padding-block calc(var(--_navigationrail-morph-speed) / 2) var(--_navigationrail-item-delay) linear,
188
+ border-radius calc(var(--_navigationrail-morph-speed) / 2) var(--_navigationrail-item-delay) linear,
189
+ flex-direction 0s calc(var(--_navigationrail-morph-speed) / 2) linear allow-discrete,
190
+ background-color calc(var(--_navigationrail-morph-speed) / 2) linear,
191
+ background-size 0ms,
171
192
  --statelayer-opacity var(--md-comp-nav-rail-motion-duration) linear;
172
193
 
173
194
  .micl-navigationrail__icon {
@@ -185,7 +206,7 @@
185
206
  margin: 4px 0px 0px 0px;
186
207
  color: var(--md-sys-color-on-surface-variant);
187
208
  overflow: hidden;
188
- animation: var(--_text-animation);
209
+ animation: var(--_navigationrail-text-animation);
189
210
  }
190
211
 
191
212
  &:hover {
@@ -193,8 +214,10 @@
193
214
  }
194
215
  &:active {
195
216
  --statelayer-opacity: var(--md-sys-state-pressed-state-layer-opacity, 10%);
196
- background-size: 0%, 100%;
197
- transition-duration: 0ms, 0ms, 0ms, calc(var(--_morph-speed) / 2), 0ms, var(--md-comp-nav-rail-motion-duration);
217
+ transition-duration: 0ms, 0ms, 0ms, calc(var(--_navigationrail-morph-speed) / 2), 0ms, var(--md-comp-nav-rail-motion-duration);
218
+ }
219
+ &.micl-rippling {
220
+ animation: micl-ripple var(--md-sys-state-ripple-duration) motion.$md-sys-motion-easing-standard;
198
221
  }
199
222
  }
200
223
 
@@ -239,20 +262,20 @@
239
262
 
240
263
  dialog.micl-navigationrail,
241
264
  nav.micl-navigationrail:has(> .micl-navigationrail__headline .micl-button--toggle.micl-button--selected) {
242
- --_current-max-width: var(--md-comp-nav-rail-expanded-container-width-maximum, 360px);
243
- --_current-min-width: var(--md-comp-nav-rail-expanded-container-width-minimum, 220px);
244
- --_item-direction: row;
245
- --_item-width: fit-content;
265
+ --_navigationrail-current-max-width: var(--md-comp-nav-rail-expanded-container-width-maximum, 360px);
266
+ --_navigationrail-current-min-width: var(--md-comp-nav-rail-expanded-container-width-minimum, 220px);
267
+ --_navigationrail-item-direction: row;
268
+ --_navigationrail-item-width: fit-content;
246
269
 
247
- --_item-target-height: var(--md-comp-nav-rail-item-short-container-height, 56px);
248
- --_item-target-margin: 0px;
249
- --_item-target-padding: 16px;
270
+ --_navigationrail-item-target-height: var(--md-comp-nav-rail-item-short-container-height, 56px);
271
+ --_navigationrail-item-target-margin: 0px;
272
+ --_navigationrail-item-target-padding: 16px;
250
273
 
251
- --_content-gap: 0px;
252
- --_morph-speed: var(--md-comp-nav-rail-morph-duration);
253
- --_container-delay: 0ms;
254
- --_item-delay: calc(var(--_morph-speed) / 2);
255
- --_text-animation: var(--_morph-speed) linear forwards navigationrail-text-to-expanded;
274
+ --_navigationrail-content-gap: 0px;
275
+ --_navigationrail-morph-speed: var(--md-comp-nav-rail-morph-duration);
276
+ --_navigationrail-container-delay: 0ms;
277
+ --_navigationrail-item-delay: calc(var(--_navigationrail-morph-speed) / 2);
278
+ --_navigationrail-text-animation: var(--_navigationrail-morph-speed) linear forwards navigationrail-text-to-expanded;
256
279
 
257
280
  padding-block: var(--md-comp-nav-rail-expanded-top-space, 44px) 0;
258
281
  border-radius: var(--md-comp-nav-rail-expanded-container-shape, 0px);
@@ -269,7 +292,7 @@ nav.micl-navigationrail:has(> .micl-navigationrail__headline .micl-button--toggl
269
292
  }
270
293
  .micl-navigationrail__text {
271
294
  @include typography.label-large;
272
- margin-inline-start: var(--_expanded-text-margin);
295
+ margin-inline-start: var(--_navigationrail-expanded-text-margin);
273
296
  padding-inline-end: 16px;
274
297
  }
275
298
 
@@ -281,8 +304,8 @@ nav.micl-navigationrail:has(> .micl-navigationrail__headline .micl-button--toggl
281
304
  }
282
305
 
283
306
  nav.micl-navigationrail:has(> .micl-navigationrail__headline .micl-button--toggle.micl-button--toggled:not(.micl-button--selected)) {
284
- --_morph-speed: var(--md-comp-nav-rail-morph-duration-reverse);
285
- --_text-animation: var(--_morph-speed) linear forwards navigationrail-text-to-collapsed;
307
+ --_navigationrail-morph-speed: var(--md-comp-nav-rail-morph-duration-reverse);
308
+ --_navigationrail-text-animation: var(--_navigationrail-morph-speed) linear forwards navigationrail-text-to-collapsed;
286
309
  }
287
310
 
288
311
  nav.micl-navigationrail {
@@ -324,14 +347,14 @@ dialog.micl-navigationrail {
324
347
  0% {
325
348
  @include typography.label-large;
326
349
  margin-block-start: 0;
327
- margin-inline-start: var(--_expanded-text-margin);
350
+ margin-inline-start: var(--_navigationrail-expanded-text-margin);
328
351
  padding-inline-end: 16px;
329
352
  opacity: 100%
330
353
  }
331
354
  49.9% {
332
355
  @include typography.label-large;
333
356
  margin-block-start: 0;
334
- margin-inline-start: var(--_expanded-text-margin);
357
+ margin-inline-start: var(--_navigationrail-expanded-text-margin);
335
358
  padding-inline-end: 16px;
336
359
  opacity: 0%;
337
360
  }
@@ -373,14 +396,14 @@ dialog.micl-navigationrail {
373
396
  50.1% {
374
397
  @include typography.label-large;
375
398
  margin-block-start: 0px;
376
- margin-inline-start: var(--_expanded-text-margin);
399
+ margin-inline-start: var(--_navigationrail-expanded-text-margin);
377
400
  padding-inline-end: 16px;
378
401
  opacity: 0%;
379
402
  }
380
403
  100% {
381
404
  @include typography.label-large;
382
405
  margin-block-start: 0px;
383
- margin-inline-start: var(--_expanded-text-margin);
406
+ margin-inline-start: var(--_navigationrail-expanded-text-margin);
384
407
  padding-inline-end: 16px;
385
408
  opacity: 100%;
386
409
  }
@@ -0,0 +1,88 @@
1
+ # Progress Indicator
2
+ This component implements the [Material Design 3 Expressive Progress indicators](https://m3.material.io/components/progress-indicators/overview) design. Progress indicators show the status of a process in real time, either as a determinate value or as an indeterminate, looping animation.
3
+
4
+ ## Basic Usage
5
+
6
+ ### HTML
7
+ Progress indicators use the native `<progress>` element. Add the `micl-progress-linear` class for the linear (bar) variant or `micl-progress-circular` for the circular (ring) variant.
8
+
9
+ A **determinate** indicator has a known value. Provide the `value` and (optionally) `max` attributes:
10
+
11
+ ```HTML
12
+ <progress class="micl-progress-linear" value="0.6" max="1"></progress>
13
+ <progress class="micl-progress-circular" value="60" max="100"></progress>
14
+ ```
15
+
16
+ An **indeterminate** indicator has an unknown value. Omit the `value` attribute entirely:
17
+
18
+ ```HTML
19
+ <progress class="micl-progress-linear"></progress>
20
+ <progress class="micl-progress-circular"></progress>
21
+ ```
22
+
23
+ ### CSS
24
+ Import the progress indicator styles into your project:
25
+
26
+ ```CSS
27
+ @use "material-inspired-component-library/dist/progressindicator";
28
+ ```
29
+
30
+ Or import all MICL styles:
31
+ ```CSS
32
+ @use "material-inspired-component-library/styles";
33
+ ```
34
+
35
+ ### JavaScript
36
+ No custom JavaScript is required for indeterminate indicators — the looping animation is driven entirely by CSS via the native `:indeterminate` state.
37
+
38
+ For determinate indicators, the bundled handler mirrors the `value` and `max` attributes into the CSS custom properties that drive the fill, the active/track gap and the Expressive wave amplitude. It also flattens the wave into a straight line over the final 10% of progress.
39
+
40
+ > **Updating progress at runtime:** the handler observes the `value` and `max` *attributes*. When you update progress dynamically, set the attribute (`element.setAttribute('value', 0.7)`) rather than only the IDL property (`element.value = 0.7`), so the indicator stays in sync. Alternatively, you may set the `--md-comp-progress-fraction` custom property (a number between `0` and `1`) directly for pure-CSS control without the handler.
41
+
42
+ ### Live Demo
43
+ A live example of the [Progress Indicator component](https://henkpb.github.io/micl/progressindicator.html) is available to interact with.
44
+
45
+ ## Variants
46
+ The Progress Indicator component offers the following variants:
47
+
48
+ | CSS class | Description |
49
+ | --------- | ----------- |
50
+ | micl-progress-linear | A horizontal linear progress bar |
51
+ | micl-progress-circular | A circular progress ring |
52
+ | micl-progress-circular--s | Small circular ring (28px) |
53
+ | micl-progress-circular--m | Medium circular ring (48px, default) |
54
+ | micl-progress-circular--l | Large circular ring (64px) |
55
+
56
+ Each variant is **determinate** when a `value` attribute is present and **indeterminate** when it is omitted.
57
+
58
+ The signature Expressive *wavy active indicator* is applied to the linear variant as a static shape (rasterised once, so any number of determinate indicators cost nothing per frame). Its amplitude eases to zero as the indicator reaches completion, and setting `--md-comp-progress-wave-amplitude` to `0` produces a flat (pre-Expressive) active indicator. Only the transient loading states animate: the linear indeterminate comet, and the circular indeterminate spinner (a GPU-composited rotation). The circular variant renders a smooth ring.
59
+
60
+ The component respects the `dir` global attribute, automatically mirroring the linear sweep direction for right-to-left (RTL) languages when `dir="rtl"` is applied to an ancestor element. It also honours the user's `prefers-reduced-motion` setting by disabling the travelling wave and looping animations.
61
+
62
+ ## Customizations
63
+ You can customize the appearance of the Progress Indicator component by overriding its global CSS variables. Following the MICL convention, these `--md-comp-progress-*` variables can be set on `:root` or on any appropriate parent element to affect its child progress indicators. The colour roles default to the Material Design system colour tokens per the M3 progress-indicator specification.
64
+
65
+ | Variable name | Default Value | Description |
66
+ | ------------- | ------------- | ----------- |
67
+ | --md-comp-progress-active-color | var(--md-sys-color-primary) | Colour of the active (filled) indicator |
68
+ | --md-comp-progress-track-color | var(--md-sys-color-secondary-container) | Colour of the remaining track |
69
+ | --md-comp-progress-stop-color | var(--md-sys-color-primary) | Colour of the linear end stop indicator dot |
70
+ | --md-comp-progress-thickness | 4px | Thickness of the linear track and circular ring stroke |
71
+ | --md-comp-progress-track-gap | 4px | Gap between the active indicator and the remaining track (linear) |
72
+ | --md-comp-progress-stop-size | 4px | Diameter of the linear end stop indicator dot |
73
+ | --md-comp-progress-linear-width | 100% | Default width of the linear indicator |
74
+ | --md-comp-progress-wave-wavelength | 40px | Wavelength of the Expressive active wave |
75
+ | --md-comp-progress-wave-amplitude | 3px | Amplitude of the Expressive active wave (set to `0` to disable) |
76
+ | --md-comp-progress-indeterminate-duration | var(--md-sys-motion-duration-extra-long4) | Period of the indeterminate loop |
77
+ | --md-comp-progress-circular-size | 48px | Diameter of the circular indicator |
78
+ | --md-comp-progress-wave-image | (inline SVG) | The sine-ribbon mask used for the wavy active indicator |
79
+
80
+ **Example: A thicker linear indicator with a calmer wave**
81
+
82
+ ```HTML
83
+ <progress class="micl-progress-linear" value="0.4"
84
+ style="--md-comp-progress-thickness:8px;--md-comp-progress-wave-amplitude:2px"></progress>
85
+ ```
86
+
87
+ ## Compatibility
88
+ This component utilizes relative RGB color values, CSS `mask`, `conic-gradient` and registered `@property` custom properties, which may not be fully supported in your browser. Please check [Browser compatibility](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#browser_compatibility) for details.
@@ -0,0 +1,225 @@
1
+ //
2
+ // Copyright © 2025 Hermana AS
3
+ //
4
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ // of this software and associated documentation files (the "Software"), to deal
6
+ // in the Software without restriction, including without limitation the rights
7
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ // copies of the Software, and to permit persons to whom the Software is
9
+ // furnished to do so, subject to the following conditions:
10
+ //
11
+ // The above copyright notice and this permission notice shall be included in all
12
+ // copies or substantial portions of the Software.
13
+ //
14
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ // SOFTWARE.
21
+
22
+ @use '../../foundations';
23
+ @use '../../styles/motion';
24
+
25
+ @include motion.duration('long2');
26
+ @include motion.duration('extra-long4');
27
+
28
+ // Registered so determinate updates from the JS handler interpolate smoothly.
29
+ // Never keyframe-animated.
30
+ @property --md-comp-progress-fraction {
31
+ syntax: '<number>';
32
+ initial-value: 0;
33
+ inherits: true;
34
+ }
35
+
36
+ // Non-themed defaults. Declared on :root so they can be overridden on :root or
37
+ // any parent element (consistent with the other MICL components). The colour
38
+ // roles are resolved per the M3 progress-indicator spec at the point of use
39
+ // (see $active/$track/$stop) because the theme palette is scoped to the
40
+ // .light/.dark window class and is therefore NOT in scope at :root.
41
+ :root {
42
+ --md-comp-progress-thickness: 4px;
43
+ --md-comp-progress-track-gap: 4px;
44
+ --md-comp-progress-stop-size: 4px;
45
+ --md-comp-progress-linear-width: 100%;
46
+ --md-comp-progress-wave-wavelength: 40px;
47
+ --md-comp-progress-wave-amplitude: 3px;
48
+ --md-comp-progress-indeterminate-duration: var(--md-sys-motion-duration-extra-long4);
49
+ --md-comp-progress-circular-size: 48px;
50
+ --md-comp-progress-amplitude-scale: 1;
51
+
52
+ // One wavelength of a sine ribbon, stroked at a constant width, used as a
53
+ // static mask so the active indicator takes a wavy shape. Painted once —
54
+ // no animation, no per-frame cost. The stroke is WHITE so it reveals
55
+ // correctly whether the browser treats the SVG mask as alpha (white =
56
+ // opaque) or luminance (white = 1).
57
+ --md-comp-progress-wave-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 10' preserveAspectRatio='none'%3E%3Cpath d='M0 5 Q10 -1 20 5 T40 5' fill='none' stroke='white' stroke-width='4' stroke-linecap='round'/%3E%3C/svg%3E");
58
+ }
59
+
60
+ // M3 progress-indicator colour mapping. Resolved on the component element so
61
+ // the system palette (scoped to the themed window ancestor) is in scope; a
62
+ // consumer-supplied --md-comp-progress-*-color on any ancestor still wins.
63
+ $active: var(--md-comp-progress-active-color, var(--md-sys-color-primary));
64
+ $track: var(--md-comp-progress-track-color, var(--md-sys-color-secondary-container));
65
+ $stop: var(--md-comp-progress-stop-color, var(--md-sys-color-primary));
66
+
67
+ %progress-reset {
68
+ -webkit-appearance: none;
69
+ appearance: none;
70
+ border: none;
71
+ margin: 0;
72
+ background-color: transparent;
73
+ color: $active;
74
+ vertical-align: middle;
75
+ }
76
+
77
+ // Static wavy active indicator. No animation: the mask is rasterised once and
78
+ // never sampled again, so a screen full of determinate indicators costs
79
+ // nothing per frame.
80
+ @mixin wave {
81
+ background-color: $active;
82
+ -webkit-mask-image: var(--md-comp-progress-wave-image);
83
+ mask-image: var(--md-comp-progress-wave-image);
84
+ -webkit-mask-repeat: repeat-x;
85
+ mask-repeat: repeat-x;
86
+ -webkit-mask-position: 0 center;
87
+ mask-position: 0 center;
88
+ -webkit-mask-size: var(--md-comp-progress-wave-wavelength)
89
+ calc(var(--md-comp-progress-thickness) + 2 * var(--md-comp-progress-wave-amplitude) * var(--md-comp-progress-amplitude-scale, 1));
90
+ mask-size: var(--md-comp-progress-wave-wavelength)
91
+ calc(var(--md-comp-progress-thickness) + 2 * var(--md-comp-progress-wave-amplitude) * var(--md-comp-progress-amplitude-scale, 1));
92
+ }
93
+
94
+ //
95
+ // Linear progress indicator
96
+ //
97
+ progress.micl-progress-linear {
98
+ @extend %progress-reset;
99
+
100
+ display: inline-block;
101
+ inline-size: var(--md-comp-progress-linear-width);
102
+ block-size: calc(var(--md-comp-progress-thickness) + 2 * var(--md-comp-progress-wave-amplitude));
103
+
104
+ // Track (remaining portion) + end stop indicator. Starts one track-gap
105
+ // after the active indicator, clipped to --thickness tall and centred.
106
+ @mixin track {
107
+ background-color: transparent;
108
+ background-image:
109
+ radial-gradient(circle, #{$stop} 0 50%, transparent 50%),
110
+ linear-gradient(90deg,
111
+ transparent 0 calc(var(--md-comp-progress-fraction, 0) * 100% + var(--md-comp-progress-track-gap)),
112
+ #{$track} calc(var(--md-comp-progress-fraction, 0) * 100% + var(--md-comp-progress-track-gap)) 100%);
113
+ background-repeat: no-repeat;
114
+ background-position: right center, left center;
115
+ background-size:
116
+ var(--md-comp-progress-stop-size) var(--md-comp-progress-stop-size),
117
+ 100% var(--md-comp-progress-thickness);
118
+ border-radius: calc(var(--md-comp-progress-thickness) / 2);
119
+ }
120
+
121
+ &::-webkit-progress-bar { @include track; }
122
+ &::-webkit-progress-value { @include wave; }
123
+ &::-moz-progress-bar { @include wave; }
124
+
125
+ // Firefox renders the track on the element itself.
126
+ @-moz-document url-prefix() {
127
+ @include track;
128
+ }
129
+
130
+ // Indeterminate: a comet sweeping a transparent track. Only this transient
131
+ // state animates; the move is a single small repainted strip.
132
+ &:indeterminate {
133
+ background-image:
134
+ linear-gradient(90deg, transparent, #{$active}, transparent),
135
+ linear-gradient(#{$track}, #{$track});
136
+ background-repeat: no-repeat;
137
+ background-position: -40% center, left center;
138
+ background-size: 40% var(--md-comp-progress-thickness), 100% var(--md-comp-progress-thickness);
139
+ border-radius: calc(var(--md-comp-progress-thickness) / 2);
140
+ animation: micl-progress-linear-indeterminate var(--md-comp-progress-indeterminate-duration) var(--md-sys-motion-easing-standard, ease-in-out) infinite;
141
+
142
+ &::-webkit-progress-bar,
143
+ &::-webkit-progress-value { background: transparent; }
144
+ &::-moz-progress-bar { background: transparent; }
145
+ }
146
+ }
147
+
148
+ @keyframes micl-progress-linear-indeterminate {
149
+ 0% { background-position: -40% center, left center; }
150
+ 100% { background-position: 140% center, left center; }
151
+ }
152
+
153
+ //
154
+ // Circular progress indicator
155
+ //
156
+ // Determinate: a static conic-gradient ring punched by a radial mask (white =
157
+ // kept under both alpha and luminance masking). It is painted once and never
158
+ // animated, so it costs nothing per frame.
159
+ //
160
+ progress.micl-progress-circular {
161
+ @extend %progress-reset;
162
+
163
+ --md-comp-progress-circular-gap-deg: 12deg;
164
+
165
+ display: inline-block;
166
+ box-sizing: border-box;
167
+ inline-size: var(--md-comp-progress-circular-size);
168
+ block-size: var(--md-comp-progress-circular-size);
169
+ border-radius: 50%;
170
+ background-image: conic-gradient(
171
+ #{$active} 0
172
+ max(0deg, calc(var(--md-comp-progress-fraction, 0) * 360deg - var(--md-comp-progress-circular-gap-deg))),
173
+ transparent
174
+ max(0deg, calc(var(--md-comp-progress-fraction, 0) * 360deg - var(--md-comp-progress-circular-gap-deg)))
175
+ calc(var(--md-comp-progress-fraction, 0) * 360deg),
176
+ #{$track} calc(var(--md-comp-progress-fraction, 0) * 360deg) 360deg);
177
+ -webkit-mask: radial-gradient(farthest-side,
178
+ transparent calc(100% - var(--md-comp-progress-thickness)),
179
+ #fff calc(100% - var(--md-comp-progress-thickness)));
180
+ mask: radial-gradient(farthest-side,
181
+ transparent calc(100% - var(--md-comp-progress-thickness)),
182
+ #fff calc(100% - var(--md-comp-progress-thickness)));
183
+
184
+ &::-webkit-progress-bar,
185
+ &::-webkit-progress-value { background: transparent; }
186
+ &::-moz-progress-bar { background: transparent; }
187
+
188
+ &.micl-progress-circular--s { --md-comp-progress-circular-size: 28px; }
189
+ &.micl-progress-circular--m { --md-comp-progress-circular-size: 48px; }
190
+ &.micl-progress-circular--l { --md-comp-progress-circular-size: 64px; }
191
+
192
+ // Indeterminate: the canonical lightweight CSS spinner — a plain bordered
193
+ // ring (no gradient, no mask) rotated by transform. Chrome promotes this to
194
+ // a compositor layer, rasterises it once, and spins the texture on the GPU,
195
+ // so it is essentially free per frame regardless of how many are visible.
196
+ &:indeterminate {
197
+ background-image: none;
198
+ -webkit-mask: none;
199
+ mask: none;
200
+ border: var(--md-comp-progress-thickness) solid #{$track};
201
+ border-block-start-color: $active;
202
+ animation: micl-progress-circular-spin var(--md-comp-progress-indeterminate-duration) linear infinite;
203
+ }
204
+ }
205
+
206
+ @keyframes micl-progress-circular-spin {
207
+ to { transform: rotate(360deg); }
208
+ }
209
+
210
+ //
211
+ // Right-to-left: mirror the linear sweep direction.
212
+ //
213
+ [dir=rtl] progress.micl-progress-linear {
214
+ transform: scaleX(-1);
215
+ }
216
+
217
+ //
218
+ // Honour reduced-motion: stop the only two animated (loading) states.
219
+ //
220
+ @media (prefers-reduced-motion: reduce) {
221
+ progress.micl-progress-linear:indeterminate,
222
+ progress.micl-progress-circular:indeterminate {
223
+ animation: none;
224
+ }
225
+ }