@expressivecss/expressive 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/CHANGELOG.md +1313 -1
  2. package/README.md +76 -30
  3. package/dist/css/expressive.css +4896 -1777
  4. package/dist/css/expressive.css.map +1 -1
  5. package/dist/css/expressive.min.css +1 -1
  6. package/dist/css/expressive.min.css.map +1 -1
  7. package/dist/fonts/LICENSE-material-symbols +93 -0
  8. package/dist/fonts/LICENSE-noto-sans +93 -0
  9. package/dist/fonts/LICENSE-roboto +93 -0
  10. package/dist/fonts/material-symbols-outlined.woff2 +0 -0
  11. package/dist/fonts/material-symbols-rounded.woff2 +0 -0
  12. package/dist/fonts/material-symbols-sharp.woff2 +0 -0
  13. package/dist/fonts/noto-sans-latin-400.woff2 +0 -0
  14. package/dist/fonts/noto-sans-latin-500.woff2 +0 -0
  15. package/dist/fonts/roboto-latin-400.woff2 +0 -0
  16. package/dist/fonts/roboto-latin-500.woff2 +0 -0
  17. package/dist/js/expressive.cjs +1673 -1338
  18. package/dist/js/expressive.cjs.map +4 -4
  19. package/dist/js/expressive.js +1673 -1338
  20. package/dist/js/expressive.js.map +4 -4
  21. package/dist/js/expressive.min.js +13 -13
  22. package/dist/js/expressive.min.js.map +4 -4
  23. package/dist/js/expressive.mjs +1673 -1338
  24. package/dist/js/expressive.mjs.map +4 -4
  25. package/dist/types/behaviors/bottomSheets.d.ts +19 -11
  26. package/dist/types/behaviors/sheetDrag.d.ts +40 -0
  27. package/dist/types/behaviors/sideSheets.d.ts +2 -9
  28. package/dist/types/components/appBar.d.ts +26 -0
  29. package/dist/types/components/autocomplete.d.ts +15 -3
  30. package/dist/types/components/buttonGroup.d.ts +39 -0
  31. package/dist/types/components/buttons.d.ts +5 -1
  32. package/dist/types/components/cards.d.ts +11 -13
  33. package/dist/types/components/carousel.d.ts +87 -53
  34. package/dist/types/components/chips.d.ts +21 -1
  35. package/dist/types/components/expandingCard.d.ts +31 -0
  36. package/dist/types/components/index.d.ts +23 -20
  37. package/dist/types/components/lightbox.d.ts +1 -0
  38. package/dist/types/components/menu.d.ts +11 -1
  39. package/dist/types/components/{sidenav.d.ts → navigationDrawer.d.ts} +16 -16
  40. package/dist/types/components/registry.d.ts +18 -8
  41. package/dist/types/components/slider.d.ts +26 -85
  42. package/dist/types/components/snackbar.d.ts +20 -5
  43. package/dist/types/components/tabs.d.ts +12 -0
  44. package/dist/types/components/timepicker.d.ts +12 -10
  45. package/dist/types/components/tooltip.d.ts +1 -0
  46. package/dist/types/core/utils.d.ts +64 -53
  47. package/dist/types/index.d.ts +8 -9
  48. package/package.json +39 -3
  49. package/src/sass/README.md +61 -11
  50. package/src/sass/abstracts/_breakpoints.scss +17 -19
  51. package/src/sass/abstracts/_mixins.scss +155 -21
  52. package/src/sass/abstracts/_variables.scss +69 -5
  53. package/src/sass/base/_fonts.scss +63 -0
  54. package/src/sass/base/_global.scss +6 -24
  55. package/src/sass/base/_grid.scss +15 -12
  56. package/src/sass/base/_normalize.scss +34 -38
  57. package/src/sass/base/_typography.scss +4 -18
  58. package/src/sass/components/_badges.scss +3 -5
  59. package/src/sass/components/_banners.scss +298 -0
  60. package/src/sass/components/_bottom-app-bar.scss +180 -0
  61. package/src/sass/components/_bottom-sheet.scss +12 -4
  62. package/src/sass/components/_button-groups.scss +358 -0
  63. package/src/sass/components/_buttons.scss +245 -92
  64. package/src/sass/components/_cards.scss +514 -78
  65. package/src/sass/components/_carousel.scss +562 -68
  66. package/src/sass/components/_chips.scss +64 -5
  67. package/src/sass/components/_datepicker.scss +24 -24
  68. package/src/sass/components/_dialog.scss +17 -11
  69. package/src/sass/components/_drag-handle.scss +130 -0
  70. package/src/sass/components/_expanding-card.scss +226 -0
  71. package/src/sass/components/_fab-menu.scss +232 -0
  72. package/src/sass/components/_floating-sheet.scss +86 -0
  73. package/src/sass/components/_icon-buttons.scss +172 -0
  74. package/src/sass/components/_icons-material-design.scss +22 -10
  75. package/src/sass/components/_index.scss +22 -6
  76. package/src/sass/components/_list.scss +16 -13
  77. package/src/sass/components/_loading-indicator.scss +106 -0
  78. package/src/sass/components/_menu.scss +335 -61
  79. package/src/sass/components/_navbar.scss +245 -45
  80. package/src/sass/components/_navigation-bar.scss +22 -11
  81. package/src/sass/components/{_sidenav.scss → _navigation-drawer.scss} +123 -33
  82. package/src/sass/components/_navigation-rail.scss +31 -23
  83. package/src/sass/components/_page-footer.scss +19 -16
  84. package/src/sass/components/_pagination.scss +24 -6
  85. package/src/sass/components/_panes.scss +52 -11
  86. package/src/sass/components/{_preloader.scss → _progress.scss} +11 -3
  87. package/src/sass/components/_scrim.scss +37 -0
  88. package/src/sass/components/_search.scss +259 -0
  89. package/src/sass/components/_segmented-buttons.scss +188 -0
  90. package/src/sass/components/_side-sheet.scss +2 -2
  91. package/src/sass/components/_snackbar.scss +20 -15
  92. package/src/sass/components/_split-button.scss +196 -0
  93. package/src/sass/components/_tabs.scss +7 -8
  94. package/src/sass/components/_timepicker.scss +67 -58
  95. package/src/sass/components/_toolbar.scss +206 -80
  96. package/src/sass/components/_transitions.scss +8 -1
  97. package/src/sass/components/forms/_checkboxes.scss +49 -12
  98. package/src/sass/components/forms/_fieldset.scss +4 -4
  99. package/src/sass/components/forms/_file-input.scss +8 -1
  100. package/src/sass/components/forms/_index.scss +1 -1
  101. package/src/sass/components/forms/_input-fields.scss +50 -44
  102. package/src/sass/components/forms/_radio-buttons.scss +42 -10
  103. package/src/sass/components/forms/_select.scss +38 -1
  104. package/src/sass/components/forms/{_range.scss → _slider.scss} +8 -9
  105. package/src/sass/components/forms/_switches.scss +6 -7
  106. package/src/sass/expressive.scss +5 -0
  107. package/src/sass/tokens/_index.scss +4 -1
  108. package/src/sass/tokens/_reference.scss +70 -61
  109. package/src/sass/tokens/_state.scss +24 -0
  110. package/src/sass/tokens/_theme.scss +6 -3
  111. package/src/sass/tokens/_vibrant.scss +76 -0
  112. package/src/sass/utilities/_colors.scss +14 -1
  113. package/src/sass/utilities/_helpers.scss +5 -5
  114. package/src/sass/utilities/_visibility.scss +48 -26
  115. package/dist/types/behaviors/waves.d.ts +0 -15
  116. package/dist/types/components/parallax.d.ts +0 -16
  117. package/dist/types/components/range.d.ts +0 -43
  118. package/dist/types/core/bounding.d.ts +0 -6
  119. package/dist/types/core/edges.d.ts +0 -6
  120. package/src/sass/components/_parallax.scss +0 -104
  121. package/src/sass/components/_pulse.scss +0 -37
  122. package/src/sass/components/_slider.scss +0 -129
@@ -0,0 +1,180 @@
1
+ @use "../abstracts" as *;
2
+
3
+ // Bottom app bar. A row of *commands* at the bottom edge, with an optional
4
+ // FAB — the counterpart of the top app bar, not of the navigation bar.
5
+ //
6
+ // It is not `nav.navigation-bar`, and the difference is the whole reason this
7
+ // component exists twice over. A navigation bar holds 3–5 destinations that do
8
+ // not change per screen; a bottom app bar holds the actions of the screen you
9
+ // are already on. So this is not a <nav> and takes no landmark (rule 4), and
10
+ // M3 says never show the two at once. The retired version of this component
11
+ // keyed on `footer:has(> nav:only-child)` — an element, which both claimed the
12
+ // landmark and put the two one markup slip apart. The host is a class now, and
13
+ // `semantics.json` forbids writing it on a <nav>.
14
+ //
15
+ // Anatomy is the HTML. Direct <a> / <button> children with a single icon are
16
+ // the actions; a `.button.extra.circle` child is the FAB and is pushed to the
17
+ // end. `.icon-button` styles itself and opts out, as it does in every host.
18
+ //
19
+ // M3 Expressive's toolbar (`.toolbar.docked`) is the newer answer to the same
20
+ // problem and holds the same commands in a 64dp bar. Both ship; this one is
21
+ // the app-bar-shaped 80dp version the spec still publishes tokens for.
22
+ //
23
+ // Tokens (md.comp.bottom-app-bar.*, DSP 34.0.21 — the container is all of it;
24
+ // the actions are icon buttons and the FAB is a FAB):
25
+ // --md-comp-bottom-app-bar-container-color surface-container
26
+ // --md-comp-bottom-app-bar-container-height 80dp, one height with or
27
+ // without a FAB (with-fab's 72dp
28
+ // is deprecated), content centred
29
+ // --md-comp-bottom-app-bar-container-shape 0
30
+ // --md-comp-bottom-app-bar-icon-color on-surface-variant
31
+ // --md-comp-bottom-app-bar-icon-size 24dp
32
+ // --md-comp-bottom-app-bar-action-size 48dp target
33
+ // --md-comp-bottom-app-bar-leading-space 4dp — 16dp to the glyph, less
34
+ // --md-comp-bottom-app-bar-trailing-space the 12dp the target insets
35
+ // --md-comp-bottom-app-bar-fab-container-color secondary-container, per
36
+ // BottomAppBarDefaults, not the
37
+ // free-standing FAB's primary
38
+ // --md-comp-bottom-app-bar-fab-trailing-space 16dp from the container edge,
39
+ // measured from the edge and not
40
+ // added to the container's own
41
+ // trailing space
42
+ // --md-comp-bottom-app-bar-z 997, the navigation bar's rung
43
+ .bottom-app-bar {
44
+ --md-comp-bottom-app-bar-container-color: var(--md-sys-color-surface-container);
45
+ --md-comp-bottom-app-bar-container-height: 80px;
46
+ --md-comp-bottom-app-bar-container-shape: 0;
47
+ --md-comp-bottom-app-bar-icon-color: var(--md-sys-color-on-surface-variant);
48
+ --md-comp-bottom-app-bar-icon-size: 24px;
49
+ --md-comp-bottom-app-bar-action-size: 48px;
50
+ --md-comp-bottom-app-bar-leading-space: 4px;
51
+ --md-comp-bottom-app-bar-trailing-space: 4px;
52
+ --md-comp-bottom-app-bar-fab-container-color: var(--md-sys-color-secondary-container);
53
+ --md-comp-bottom-app-bar-fab-trailing-space: 16px;
54
+ --md-comp-bottom-app-bar-z: 997;
55
+
56
+ box-sizing: border-box;
57
+ display: flex;
58
+ // No gap: the actions are 48dp targets and abut, which is Compose's
59
+ // `Arrangement.Start` and puts their glyph centres 48dp apart. The top app
60
+ // bar spaces its actions the same way.
61
+ align-items: center;
62
+ width: 100%;
63
+ min-height: var(--md-comp-bottom-app-bar-container-height);
64
+ margin: 0;
65
+ padding-inline: var(--md-comp-bottom-app-bar-leading-space)
66
+ var(--md-comp-bottom-app-bar-trailing-space);
67
+ border-radius: var(--md-comp-bottom-app-bar-container-shape);
68
+ background-color: var(--md-comp-bottom-app-bar-container-color);
69
+ color: var(--md-sys-color-on-surface);
70
+ @include z-depth("2");
71
+
72
+ // Icon-only actions. Same shape as the top app bar's: a bare control with a
73
+ // single icon becomes a 48dp target. Anything that styles itself — the
74
+ // common button, the icon button, the FAB — is left alone.
75
+ > :is(a, button):not(.button, .icon-button):not(.circle):not(.extra):has(> #{$icon}:only-child) {
76
+ display: inline-flex;
77
+ flex: 0 0 auto;
78
+ align-items: center;
79
+ justify-content: center;
80
+ width: var(--md-comp-bottom-app-bar-action-size);
81
+ height: var(--md-comp-bottom-app-bar-action-size);
82
+ margin: 0;
83
+ padding: 0;
84
+ border: 0;
85
+ border-radius: 50%;
86
+ background: transparent;
87
+ color: var(--md-comp-bottom-app-bar-icon-color);
88
+ cursor: pointer;
89
+ appearance: none;
90
+ font: inherit;
91
+ text-decoration: none;
92
+ @include focus-visible;
93
+
94
+ > #{$icon} {
95
+ display: block;
96
+ width: var(--md-comp-bottom-app-bar-icon-size);
97
+ height: var(--md-comp-bottom-app-bar-icon-size);
98
+ font-size: var(--md-comp-bottom-app-bar-icon-size);
99
+ line-height: var(--md-comp-bottom-app-bar-icon-size);
100
+ }
101
+
102
+ &:hover {
103
+ background-color: color-mix(in oklab, currentColor state-opacity("hover"), transparent);
104
+ }
105
+
106
+ &:active {
107
+ background-color: color-mix(in oklab, currentColor state-opacity("pressed"), transparent);
108
+ }
109
+
110
+ &:disabled,
111
+ &.disabled {
112
+ pointer-events: none;
113
+ color: color-mix(in oklab, var(--md-sys-color-on-surface) 38%, transparent);
114
+ background: transparent;
115
+ }
116
+ }
117
+
118
+ // The FAB. `margin-inline-start: auto` is the spacer, so the bar needs no
119
+ // filler element between the actions and it. Flat and secondary-container:
120
+ // the bar is already at level 2, and a second shadow inside it reads as a
121
+ // sticker. Specificity beats the FAB's own `:where()`-scoped block.
122
+ > :is(a, button).extra.circle,
123
+ > :is(a, button).large.circle {
124
+ margin-inline-start: auto;
125
+ // 16dp from the container edge. The bar's own trailing space is already
126
+ // spent, so this is the remainder - Compose's `16.dp - HorizontalPadding`.
127
+ margin-inline-end: calc(
128
+ var(--md-comp-bottom-app-bar-fab-trailing-space) -
129
+ var(--md-comp-bottom-app-bar-trailing-space)
130
+ );
131
+ background-color: var(--md-comp-bottom-app-bar-fab-container-color);
132
+ color: var(--md-sys-color-on-secondary-container);
133
+ box-shadow: none;
134
+
135
+ &:hover {
136
+ box-shadow: none;
137
+ background-color: color-mix(
138
+ in oklab,
139
+ var(--md-sys-color-on-secondary-container) state-opacity("hover"),
140
+ var(--md-comp-bottom-app-bar-fab-container-color)
141
+ );
142
+ }
143
+
144
+ // Focus and pressed are separate states with separate opacities, the way
145
+ // `abstracts/_mixins` keeps them. `:focus`, not `:focus-visible`: the FAB's
146
+ // own block in `_buttons.scss` paints a `primary-container` tint on plain
147
+ // `:focus`, and leaving that uncovered would flash the wrong colour family
148
+ // at a mouse click. The ring is `focus-visible`'s job and the base rule
149
+ // already carries it.
150
+ &:focus {
151
+ background-color: color-mix(
152
+ in oklab,
153
+ var(--md-sys-color-on-secondary-container) state-opacity("focus"),
154
+ var(--md-comp-bottom-app-bar-fab-container-color)
155
+ );
156
+ }
157
+
158
+ &:active {
159
+ background-color: color-mix(
160
+ in oklab,
161
+ var(--md-sys-color-on-secondary-container) state-opacity("pressed"),
162
+ var(--md-comp-bottom-app-bar-fab-container-color)
163
+ );
164
+ }
165
+ }
166
+ }
167
+
168
+ // Pinned to the bottom edge. Same z rung as nav.navigation-bar.fixed: the two
169
+ // are alternatives, never neighbours, so neither needs to sit above the other.
170
+ .bottom-app-bar.fixed {
171
+ position: fixed;
172
+ inset: auto 0 0 0;
173
+ z-index: var(--md-comp-bottom-app-bar-z);
174
+ // The bar grows by the inset rather than spending its own height on it.
175
+ // Under `border-box` the padding would otherwise come out of the 80dp the
176
+ // token block and the docs both promise, and the actions would sit in
177
+ // whatever was left.
178
+ min-height: calc(var(--md-comp-bottom-app-bar-container-height) + env(safe-area-inset-bottom, 0px));
179
+ padding-bottom: env(safe-area-inset-bottom, 0px);
180
+ }
@@ -10,7 +10,7 @@
10
10
  //
11
11
  // Tokens follow https://m3.material.io/components/bottom-sheets
12
12
  // container surface-container-low, 28dp top corners, elevation 1
13
- // max width 640dp; 56dp inset from the small breakpoint
13
+ // max width 640dp; 56dp inset from the Medium breakpoint
14
14
  // max height viewport minus 72dp
15
15
  // handle 32×4dp bar in a 48dp hit target, on-surface-variant
16
16
  // scrim modal only (UA ::backdrop + the dialog scrim token)
@@ -60,7 +60,7 @@ dialog.bottom-sheet {
60
60
  z-index: 1100;
61
61
  }
62
62
 
63
- @include bp-up("small") {
63
+ @include bp-up("medium") {
64
64
  width: min(
65
65
  var(--md-comp-bottom-sheet-container-max-width),
66
66
  calc(100% - 2 * var(--md-comp-bottom-sheet-margin-inline))
@@ -90,12 +90,12 @@ dialog.bottom-sheet {
90
90
  pointer-events: none;
91
91
  }
92
92
 
93
- &:has(> .handle)::before {
93
+ &:has(> :is(.handle, .drag-handle))::before {
94
94
  content: none;
95
95
  display: none;
96
96
  }
97
97
 
98
- > .handle {
98
+ > :is(.handle, .drag-handle) {
99
99
  box-sizing: border-box;
100
100
  flex: 0 0 48px;
101
101
  align-self: center;
@@ -111,6 +111,14 @@ dialog.bottom-sheet {
111
111
  appearance: none;
112
112
  }
113
113
 
114
+ // The sheet paints its own bar with a mask on the element, so the vertical
115
+ // bar _drag-handle.scss draws in ::before would print across it. This is the
116
+ // whole cost of one class serving both hosts, and it is cheaper than a
117
+ // `horizontal` modifier whose values M3 does not publish.
118
+ > .drag-handle::before {
119
+ content: none;
120
+ }
121
+
114
122
  > :is(h1, h2, h3, h4, h5, h6) {
115
123
  padding-top: 8px;
116
124
  }
@@ -0,0 +1,358 @@
1
+ @use "sass:map";
2
+ @use "../abstracts" as *;
3
+
4
+ // Material 3 Expressive button groups: standard and connected, each across
5
+ // xsmall through xlarge. Direct children can be common buttons or icon
6
+ // buttons. A size on the group propagates to children unless a child has an
7
+ // explicit size of its own.
8
+ //
9
+ // ButtonGroup supplies standard-group width redistribution for every group.
10
+ // Toggle groups opt into managed selection with `data-selection="single|multiple"`;
11
+ // groups without it retain native action-button semantics. Native buttons keep
12
+ // their normal Tab, Space, and Enter behavior. The container may use
13
+ // `role="group"` and an accessible name, but never a composite role such as
14
+ // `toolbar` because this component has no roving-focus or arrow-key contract.
15
+ //
16
+ // Standard groups keep separate items and grow/reshape the pressed one.
17
+ // Connected groups fill their available width, divide it evenly, keep a 2dp
18
+ // seam, and morph the shared corners. Toggle selection changes color, shape,
19
+ // and icon fill so color is never the only cue.
20
+ $_item: ":is(button, a.button)";
21
+ $_label-item: ":is(button:not(.icon-button), a.button)";
22
+ $_filled-item: ":where(.filled, :not(.elevated, .filled, .tonal, .outlined, .text))";
23
+ $_toggle-item: ":is(button:not(.icon-button):not(.text), a.button:not(.text), .icon-button:is(.filled, .tonal, .outlined))";
24
+
25
+ // The size classes, as one `:not()` argument list - which takes the weight of
26
+ // a single class, not five. An item carrying its own size class sets the
27
+ // geometry tokens on itself and beats the group by inheritance; the type role
28
+ // has no such mechanism, so the group's rule has to step aside by selector or
29
+ // an `xlarge` item inside a `medium` group gets xlarge geometry and medium
30
+ // type.
31
+ $_sized: ".xsmall, .small, .medium, .large, .xlarge";
32
+
33
+ // md.comp.button-group.standard.<size>.between-space
34
+ $_standard-space: (
35
+ "xsmall": 18px,
36
+ "small": 12px,
37
+ "medium": 8px,
38
+ "large": 8px,
39
+ "xlarge": 8px,
40
+ );
41
+
42
+ // md.comp.button.<size>.{container-shape-square, pressed-container-shape}.
43
+ // Common buttons and icon buttons share these numbers, so the group can morph
44
+ // either kind of child through one pair of properties.
45
+ $_item-shapes: (
46
+ "xsmall": (square: 12px, pressed: 8px),
47
+ "small": (square: 12px, pressed: 8px),
48
+ "medium": (square: 16px, pressed: 12px),
49
+ "large": (square: 28px, pressed: 16px),
50
+ "xlarge": (square: 28px, pressed: 16px),
51
+ );
52
+
53
+ // md.comp.button-group.connected.<size>.{inner-corner, pressed.inner-corner}.
54
+ // The live M3 measurement is authoritative for XS (4px). Material Web 34.0.21
55
+ // still emits 8px there; S and M agree at 8px.
56
+ $_connected-corners: (
57
+ "xsmall": (inner: 4px, pressed: 4px),
58
+ "small": (inner: 8px, pressed: 4px),
59
+ "medium": (inner: 8px, pressed: 4px),
60
+ "large": (inner: 16px, pressed: 12px),
61
+ "xlarge": (inner: 20px, pressed: 16px),
62
+ );
63
+
64
+ @mixin _disabled-toggle {
65
+ @include z-depth("0");
66
+ color: color-mix(in oklab, var(--md-sys-color-on-surface) 38%, transparent);
67
+ pointer-events: none;
68
+ box-shadow: none;
69
+ cursor: default;
70
+ }
71
+
72
+ .button-group {
73
+ --md-comp-button-group-between-space: 12px;
74
+ --md-comp-button-group-item-shape-round: 9999px;
75
+ --md-comp-button-group-item-shape-square: 12px;
76
+ --md-comp-button-group-pressed-item-shape: 8px;
77
+ // M3 states this as a percentage of the item's own width. CSS cannot
78
+ // multiply a length by one, so it is the same figure as a ratio.
79
+ --md-comp-button-group-pressed-item-width-multiplier: .15;
80
+ // Material's fast-spatial spring (damping .9, stiffness 1400), sampled into
81
+ // a CSS linear easing so width and corner motion use the published dynamics.
82
+ --md-comp-button-group-motion: linear(
83
+ 0, .056 5%, .1796 10%, .3252 15%, .4675 20%, .5936 25%,
84
+ .6988 30%, .7828 35%, .8475 40%, .8959 45%, .931 50%,
85
+ .9559 55%, .973 60%, .9845 65%, .9919 70%, .9965 75%,
86
+ .9992 80%, 1.0006 85%, 1.0013 90%, 1.0015 95%, 1.0015
87
+ );
88
+
89
+ display: flex;
90
+ align-items: center;
91
+ gap: var(--md-comp-button-group-between-space);
92
+
93
+ // The size axis. A group sizes its own items by rewriting the button
94
+ // tokens, which the items inherit - so a size is written once, on the
95
+ // group, and a button that wants to differ still overrides it with its own
96
+ // size class. The type role cannot travel that way: `btn` states a
97
+ // font-size of its own, so it is set on the items.
98
+ @each $name, $s in $button-sizes {
99
+ &.#{$name} {
100
+ @include button-size($name);
101
+
102
+ --md-comp-button-group-between-space: #{map.get($_standard-space, $name)};
103
+ --md-comp-button-group-item-shape-square: #{map.get(map.get($_item-shapes, $name), square)};
104
+ --md-comp-button-group-pressed-item-shape: #{map.get(map.get($_item-shapes, $name), pressed)};
105
+
106
+ > #{$_label-item}:not(#{$_sized}) {
107
+ @include typescale(map.get($s, label));
108
+ }
109
+
110
+ > .icon-button:not(#{$_sized}) {
111
+ @include icon-button-size($name);
112
+ }
113
+
114
+ > .icon-button.narrow:not(#{$_sized}) {
115
+ --md-comp-icon-button-leading-space: #{map.get(map.get($icon-button-sizes, $name), narrow)};
116
+ --md-comp-icon-button-trailing-space: #{map.get(map.get($icon-button-sizes, $name), narrow)};
117
+ }
118
+
119
+ > .icon-button.wide:not(#{$_sized}) {
120
+ --md-comp-icon-button-leading-space: #{map.get(map.get($icon-button-sizes, $name), wide)};
121
+ --md-comp-icon-button-trailing-space: #{map.get(map.get($icon-button-sizes, $name), wide)};
122
+ }
123
+ }
124
+
125
+ // Group shape tokens inherit, so an explicitly sized item must rewrite
126
+ // them on itself just as its button geometry tokens do.
127
+ > #{$_item}.#{$name} {
128
+ --md-comp-button-group-item-shape-square: #{map.get(map.get($_item-shapes, $name), square)};
129
+ --md-comp-button-group-pressed-item-shape: #{map.get(map.get($_item-shapes, $name), pressed)};
130
+ --md-comp-button-group-inner-corner-corner-size: #{map.get(map.get($_connected-corners, $name), inner)};
131
+ --md-comp-button-group-pressed-inner-corner-corner-size: #{map.get(map.get($_connected-corners, $name), pressed)};
132
+ }
133
+ }
134
+
135
+ > #{$_item} {
136
+ position: relative;
137
+ transition:
138
+ width 200ms var(--md-comp-button-group-motion),
139
+ border-radius 200ms var(--md-comp-button-group-motion),
140
+ background-color 200ms ease-out;
141
+ }
142
+
143
+ // Every item owns a centered 48dp minimum hit target. For M/L/XL this is a
144
+ // no-op because the visible container already exceeds it; the unconditional
145
+ // rule also covers a default-small group and explicit XS/S child overrides.
146
+ > #{$_item}::before {
147
+ content: "";
148
+ position: absolute;
149
+ top: 50%;
150
+ left: 50%;
151
+ width: 100%;
152
+ height: 100%;
153
+ min-width: 48px;
154
+ min-height: 48px;
155
+ translate: -50% -50%;
156
+ }
157
+
158
+ &:not(.connected) > #{$_item} {
159
+ --_corner: var(--md-comp-button-group-item-shape-round);
160
+ border-radius: var(--_corner);
161
+ }
162
+
163
+ &.square:not(.connected) > #{$_item} {
164
+ --_corner: var(--md-comp-button-group-item-shape-square);
165
+ }
166
+
167
+ &:not(.connected) > #{$_item}[aria-pressed=true] {
168
+ --_corner: var(--md-comp-button-group-item-shape-square);
169
+ }
170
+
171
+ &.square:not(.connected) > #{$_item}[aria-pressed=true] {
172
+ --_corner: var(--md-comp-button-group-item-shape-round);
173
+ }
174
+
175
+ // Toggle-button colors come from md.comp.button.<style>.{unselected,selected}.
176
+ // Shape and the filled icon below provide two non-color selection cues.
177
+ > #{$_label-item}#{$_filled-item}[aria-pressed=false],
178
+ > .icon-button.filled[aria-pressed=false] {
179
+ --_toggle-container-color: var(--md-sys-color-surface-container);
180
+ --_toggle-state-color: var(--md-sys-color-on-surface-variant);
181
+ color: var(--md-sys-color-on-surface-variant);
182
+ background-color: var(--_toggle-container-color);
183
+ }
184
+
185
+ > #{$_label-item}#{$_filled-item}[aria-pressed=true],
186
+ > .icon-button.filled[aria-pressed=true] {
187
+ --_toggle-container-color: var(--md-sys-color-primary);
188
+ --_toggle-state-color: var(--md-sys-color-on-primary);
189
+ color: var(--md-sys-color-on-primary);
190
+ background-color: var(--_toggle-container-color);
191
+ }
192
+
193
+ > #{$_label-item}.tonal[aria-pressed=false],
194
+ > .icon-button.tonal[aria-pressed=false] {
195
+ --_toggle-container-color: var(--md-sys-color-secondary-container);
196
+ --_toggle-state-color: var(--md-sys-color-on-secondary-container);
197
+ color: var(--md-sys-color-on-secondary-container);
198
+ background-color: var(--_toggle-container-color);
199
+ }
200
+
201
+ > #{$_label-item}.tonal[aria-pressed=true],
202
+ > .icon-button.tonal[aria-pressed=true] {
203
+ --_toggle-container-color: var(--md-sys-color-secondary);
204
+ --_toggle-state-color: var(--md-sys-color-on-secondary);
205
+ color: var(--md-sys-color-on-secondary);
206
+ background-color: var(--_toggle-container-color);
207
+ }
208
+
209
+ > #{$_label-item}.outlined[aria-pressed=false],
210
+ > .icon-button.outlined[aria-pressed=false] {
211
+ --_toggle-container-color: transparent;
212
+ --_toggle-state-color: var(--md-sys-color-on-surface-variant);
213
+ color: var(--md-sys-color-on-surface-variant);
214
+ background-color: var(--_toggle-container-color);
215
+ }
216
+
217
+ > #{$_label-item}.outlined[aria-pressed=true],
218
+ > .icon-button.outlined[aria-pressed=true] {
219
+ --_toggle-container-color: var(--md-sys-color-inverse-surface);
220
+ --_toggle-state-color: var(--md-sys-color-inverse-on-surface);
221
+ color: var(--md-sys-color-inverse-on-surface);
222
+ background-color: var(--_toggle-container-color);
223
+ }
224
+
225
+ > #{$_label-item}.elevated[aria-pressed=false] {
226
+ --_toggle-container-color: var(--md-sys-color-surface-container-low);
227
+ --_toggle-state-color: var(--md-sys-color-primary);
228
+ color: var(--md-sys-color-primary);
229
+ background-color: var(--_toggle-container-color);
230
+ }
231
+
232
+ > #{$_label-item}.elevated[aria-pressed=true] {
233
+ --_toggle-container-color: var(--md-sys-color-primary);
234
+ --_toggle-state-color: var(--md-sys-color-on-primary);
235
+ color: var(--md-sys-color-on-primary);
236
+ background-color: var(--_toggle-container-color);
237
+ }
238
+
239
+ > #{$_toggle-item}[aria-pressed]:hover {
240
+ background-color: color-mix(
241
+ in oklab,
242
+ var(--_toggle-state-color) state-opacity("hover"),
243
+ var(--_toggle-container-color)
244
+ );
245
+ }
246
+
247
+ > #{$_toggle-item}[aria-pressed]:focus {
248
+ background-color: color-mix(
249
+ in oklab,
250
+ var(--_toggle-state-color) state-opacity("focus"),
251
+ var(--_toggle-container-color)
252
+ );
253
+ }
254
+
255
+ > #{$_toggle-item}[aria-pressed]:is(:active, .button-group-pressed) {
256
+ background-color: color-mix(
257
+ in oklab,
258
+ var(--_toggle-state-color) state-opacity("pressed"),
259
+ var(--_toggle-container-color)
260
+ );
261
+ }
262
+
263
+ > #{$_label-item}#{$_filled-item}[aria-pressed]:is(:disabled, [disabled], .disabled),
264
+ > #{$_label-item}:is(.tonal, .elevated)[aria-pressed]:is(:disabled, [disabled], .disabled),
265
+ > .icon-button:is(.filled, .tonal)[aria-pressed]:is(:disabled, [disabled], .disabled),
266
+ > #{$_label-item}.outlined[aria-pressed=true]:is(:disabled, [disabled], .disabled),
267
+ > .icon-button.outlined[aria-pressed=true]:is(:disabled, [disabled], .disabled) {
268
+ @include _disabled-toggle;
269
+ background-color: color-mix(in oklab, var(--md-sys-color-on-surface) 10%, transparent);
270
+ border-color: transparent;
271
+ }
272
+
273
+ > #{$_label-item}.outlined[aria-pressed=false]:is(:disabled, [disabled], .disabled),
274
+ > .icon-button.outlined[aria-pressed=false]:is(:disabled, [disabled], .disabled) {
275
+ @include _disabled-toggle;
276
+ background-color: transparent;
277
+ border-color: var(--md-sys-color-outline-variant);
278
+ }
279
+
280
+ > #{$_item}[aria-pressed=true] > #{$icon} {
281
+ --md-icon-fill: 1;
282
+ }
283
+
284
+ // ButtonGroup measures the pressed item and takes its 15% growth from the
285
+ // immediate neighbors, keeping the standard group's total width stable.
286
+ // CSS owns only the simultaneous shape change.
287
+ &:not(.connected) > #{$_item}:is(:active, .button-group-pressed),
288
+ &.square:not(.connected) > #{$_item}[aria-pressed=true]:is(:active, .button-group-pressed) {
289
+ --_corner: var(--md-comp-button-group-pressed-item-shape);
290
+ }
291
+
292
+ // Connected. Emitted after the size rules on purpose: `.button-group.medium`
293
+ // and `.button-group.connected` are both two classes, so source order is the
294
+ // only thing that keeps a connected group at 2dp.
295
+ &.connected {
296
+ --md-comp-button-group-between-space: 2px;
297
+ --md-comp-button-group-container-shape: 9999px;
298
+ --md-comp-button-group-inner-corner-corner-size: 8px;
299
+ --md-comp-button-group-pressed-inner-corner-corner-size: 4px;
300
+ --md-comp-button-group-selected-inner-corner-corner-size: 50%;
301
+
302
+ width: 100%;
303
+
304
+ @each $name, $c in $_connected-corners {
305
+ &.#{$name} {
306
+ --md-comp-button-group-inner-corner-corner-size: #{map.get($c, inner)};
307
+ --md-comp-button-group-pressed-inner-corner-corner-size: #{map.get($c, pressed)};
308
+ }
309
+ }
310
+
311
+ &.square {
312
+ --md-comp-button-group-container-shape: var(--md-comp-button-group-inner-corner-corner-size);
313
+ }
314
+
315
+ > #{$_item} {
316
+ min-width: 48px;
317
+ }
318
+
319
+ // The corner goes through a variable so that pressing an item changes one
320
+ // value rather than restating `border-radius`. A shorthand for the pressed
321
+ // state would take the end items' round outer corners with it.
322
+ > #{$_item} {
323
+ --_corner: var(--md-comp-button-group-inner-corner-corner-size);
324
+ --_outer-corner: var(--md-comp-button-group-container-shape);
325
+
326
+ flex: 1 1 0;
327
+ border-radius: var(--_corner);
328
+
329
+ &[aria-pressed=true] {
330
+ --_corner: var(--md-comp-button-group-selected-inner-corner-corner-size);
331
+ --_outer-corner: var(--md-comp-button-group-selected-inner-corner-corner-size);
332
+ }
333
+
334
+ &:is(:active, .button-group-pressed) {
335
+ --_corner: var(--md-comp-button-group-pressed-inner-corner-corner-size);
336
+ }
337
+
338
+ // Logical, so the ends of the row are the ends of the row in RTL too.
339
+ &:first-child {
340
+ border-start-start-radius: var(--_outer-corner);
341
+ border-end-start-radius: var(--_outer-corner);
342
+ }
343
+
344
+ &:last-child {
345
+ border-start-end-radius: var(--_outer-corner);
346
+ border-end-end-radius: var(--_outer-corner);
347
+ }
348
+ }
349
+ }
350
+ }
351
+
352
+ // The press grow and the corner morph are both motion, and the only motion
353
+ // this component has.
354
+ @media (prefers-reduced-motion: reduce) {
355
+ .button-group > #{$_item} {
356
+ transition: none;
357
+ }
358
+ }