Liquid Glass for the web: the rim refraction, light and motion of iOS 27 glass on any rounded element. React bindings and pill tabs. MIT.
| 1 | /* liquid-glass. Layout and accessibility for the glass; its filters are set |
| 2 | inline by the script, as literal values Safari can read. */ |
| 3 | |
| 4 | /* The glass sits behind the surface's content, so the surface must be its own |
| 5 | stacking context. It must not be a backdrop root itself: no filter, opacity |
| 6 | below 1, mask or backdrop-filter, or the glass would only see the surface. */ |
| 7 | :has |
| 8 | isolation: isolate; |
| 9 | } |
| 10 |
| 11 | .liquid-glass, |
| 12 | .liquid-glass-layer, |
| 13 | .liquid-glass-hue, |
| 14 | .liquid-glass-light { |
| 15 | position: absolute; |
| 16 | inset: 0; |
| 17 | border-radius: inherit; |
| 18 | pointer-events: none; |
| 19 | } |
| 20 | .liquid-glass { |
| 21 | z-index: -1; |
| 22 | } |
| 23 | .liquid-glass[data-fit="fixed"] { |
| 24 | position: fixed; |
| 25 | } |
| 26 | .liquid-glass[data-fit="scroll"] { |
| 27 | bottom: auto; |
| 28 | } |
| 29 | .liquid-glass-layer[data-layer="body"] { |
| 30 | transition: backdrop-filter 300ms ease, background-color 300ms ease; |
| 31 | } |
| 32 | /* Rim layers wait for their masks; unmasked, they would brighten the whole surface. */ |
| 33 | .liquid-glass:not([data-ready]) > :is([data-layer="shade"], [data-layer="glow"], [data-layer="edge"], [data-layer="highlight"]) { |
| 34 | display: none; |
| 35 | } |
| 36 |
| 37 | /* Floating glass (the `floating` option) casts a drop shadow from its surface, |
| 38 | without a ring or lit line: the glass draws its own edge, and an inset line |
| 39 | would sit under the glass and bend with it. Set --liquid-glass-shadow to change it. */ |
| 40 | [data-liquid-glass][data-liquid-glass-floating] { |
| 41 | box-shadow: var(--liquid-glass-shadow, 0 8px 20px -6px rgb(0 0 0 / 0.5), 0 24px 48px -12px rgb(0 0 0 / 0.6)); |
| 42 | } |
| 43 | [data-liquid-glass="light"][data-liquid-glass-floating] { |
| 44 | box-shadow: var(--liquid-glass-shadow, 0 6px 16px -4px rgb(30 30 40 / 0.1), 0 20px 40px -12px rgb(30 30 40 / 0.16)); |
| 45 | } |
| 46 |
| 47 | /* Interactive glass (the `interactive` option) lights up from within: the pointer |
| 48 | warms a patch of glass beneath it, and a press blooms out from there. Letting |
| 49 | go, the light fades where it spread and only then gathers back. Plain alpha, |
| 50 | never a blend mode, which would isolate the glass and cut it off from the page. */ |
| 51 | @property --liquid-glass-light-size { |
| 52 | syntax: "<length>"; |
| 53 | inherits: false; |
| 54 | initial-value: 56px; |
| 55 | } |
| 56 | @property --liquid-glass-light { |
| 57 | syntax: "<number>"; |
| 58 | inherits: false; |
| 59 | initial-value: 0; |
| 60 | } |
| 61 | .liquid-glass-light { |
| 62 | --liquid-glass-light-size: 56px; |
| 63 | --liquid-glass-light: 0; |
| 64 | background: radial-gradient( |
| 65 | circle var(--liquid-glass-light-size) at var(--liquid-glass-x, -999px) var(--liquid-glass-y, -999px), |
| 66 | rgb(255 255 255 / var(--liquid-glass-light)), |
| 67 | rgb(255 255 255 / calc(var(--liquid-glass-light) * 0.3)) 40%, |
| 68 | transparent |
| 69 | ); |
| 70 | transition: --liquid-glass-light 360ms ease-out, --liquid-glass-light-size 240ms ease-out 360ms; |
| 71 | } |
| 72 | [data-liquid-glass-pointer] > .liquid-glass > .liquid-glass-light { |
| 73 | --liquid-glass-light: 0.07; |
| 74 | } |
| 75 | [data-liquid-glass-pressed] > .liquid-glass > .liquid-glass-light { |
| 76 | --liquid-glass-light-size: 150px; |
| 77 | --liquid-glass-light: 0.24; |
| 78 | transition: --liquid-glass-light 90ms ease-out, --liquid-glass-light-size 450ms cubic-bezier(0.23, 1, 0.32, 1); |
| 79 | } |
| 80 | [data-liquid-glass="light"][data-liquid-glass-pointer] > .liquid-glass > .liquid-glass-light { |
| 81 | --liquid-glass-light: 0.3; |
| 82 | } |
| 83 | [data-liquid-glass="light"][data-liquid-glass-pressed] > .liquid-glass > .liquid-glass-light { |
| 84 | --liquid-glass-light: 0.6; |
| 85 | } |
| 86 |
| 87 | /* Controls on a glass surface don't get glass of their own: Apple's guidance is |
| 88 | to avoid glass on glass and make them a thin part of the material. A well |
| 89 | brightens the glass beneath it. No element between it and the surface may be |
| 90 | a backdrop root. */ |
| 91 | [data-liquid-glass] .liquid-glass-well { |
| 92 | background: transparent; |
| 93 | -webkit-backdrop-filter: brightness(1.45); |
| 94 | backdrop-filter: brightness(1.45); |
| 95 | transition: backdrop-filter 150ms ease-out, scale 150ms ease-out, color 150ms ease-out; |
| 96 | } |
| 97 | [data-liquid-glass] .liquid-glass-well:hover { |
| 98 | -webkit-backdrop-filter: brightness(1.8); |
| 99 | backdrop-filter: brightness(1.8); |
| 100 | } |
| 101 | [data-liquid-glass="light"] .liquid-glass-well { |
| 102 | -webkit-backdrop-filter: brightness(0.92); |
| 103 | backdrop-filter: brightness(0.92); |
| 104 | } |
| 105 | [data-liquid-glass="light"] .liquid-glass-well:hover { |
| 106 | -webkit-backdrop-filter: brightness(0.86); |
| 107 | backdrop-filter: brightness(0.86); |
| 108 | } |
| 109 | /* A well can't fade with its surface's contents, or the glass beneath would blank, |
| 110 | so it eases back to clear glass as the surface dematerializes. */ |
| 111 | [data-liquid-glass][data-liquid-glass-leaving] .liquid-glass-well { |
| 112 | -webkit-backdrop-filter: brightness(1); |
| 113 | backdrop-filter: brightness(1); |
| 114 | } |
| 115 |
| 116 | /* Materializing, for surfaces that appear and disappear under CSS control: run |
| 117 | these on .liquid-glass-layer with the surface's own timing. The surface must |
| 118 | not fade or scale instead: opacity hides the glass, and a changing scale leaves |
| 119 | Chromium's backdrop-filter with a blank backdrop for frames. The script sets |
| 120 | each layer's --liquid-glass-clear, --liquid-glass-full and --liquid-glass-wash. |
| 121 | materialize() and dematerialize() do the same from script. */ |
| 122 | @keyframes liquid-glass-materialize { |
| 123 | from { |
| 124 | backdrop-filter: var(--liquid-glass-clear); |
| 125 | background-color: transparent; |
| 126 | } |
| 127 | to { |
| 128 | backdrop-filter: var(--liquid-glass-full); |
| 129 | background-color: var(--liquid-glass-wash, transparent); |
| 130 | } |
| 131 | } |
| 132 | @keyframes liquid-glass-dematerialize { |
| 133 | from { |
| 134 | backdrop-filter: var(--liquid-glass-full); |
| 135 | background-color: var(--liquid-glass-wash, transparent); |
| 136 | } |
| 137 | to { |
| 138 | backdrop-filter: var(--liquid-glass-clear); |
| 139 | background-color: transparent; |
| 140 | } |
| 141 | } |
| 142 |
| 143 | /* Tabs (attachLiquidGlassTabs). The selected tab is a well in the track's glass; |
| 144 | moving, the thumb lifts into a clear lens above the labels and drops back in. |
| 145 | Every fade is a filter transition, never opacity: Chromium drops a backdrop |
| 146 | filter for frames while its element fades. */ |
| 147 | [data-liquid-glass-tabs] { |
| 148 | position: relative; |
| 149 | isolation: isolate; |
| 150 | -webkit-user-select: none; |
| 151 | user-select: none; |
| 152 | } |
| 153 | /* On another glass surface the track is a recessed tint, not glass of its own. */ |
| 154 | [data-liquid-glass-tabs-on-glass] { |
| 155 | background-color: rgb(0 0 0 / 0.2); |
| 156 | box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.25); |
| 157 | } |
| 158 | [data-liquid-glass-tabs-on-glass][data-liquid-glass-tabs-appearance="light"] { |
| 159 | background-color: rgb(0 0 0 / 0.045); |
| 160 | box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.06); |
| 161 | } |
| 162 | [data-liquid-glass-tab] { |
| 163 | position: relative; |
| 164 | z-index: 1; |
| 165 | } |
| 166 | .liquid-glass-tabs-light, |
| 167 | .liquid-glass-tabs-well, |
| 168 | .liquid-glass-tabs-lens { |
| 169 | position: absolute; |
| 170 | inset: 0; |
| 171 | border-radius: inherit; |
| 172 | pointer-events: none; |
| 173 | } |
| 174 | /* The pointer lights the glass beneath it. */ |
| 175 | @property --liquid-glass-tabs-light-size { |
| 176 | syntax: "<length>"; |
| 177 | inherits: false; |
| 178 | initial-value: 56px; |
| 179 | } |
| 180 | .liquid-glass-tabs-light { |
| 181 | z-index: -1; |
| 182 | --liquid-glass-tabs-light-size: 56px; |
| 183 | -webkit-mask-image: radial-gradient(circle var(--liquid-glass-tabs-light-size) at var(--liquid-glass-tabs-x, -999px) var(--liquid-glass-tabs-y, -999px), #000, transparent); |
| 184 | mask-image: radial-gradient(circle var(--liquid-glass-tabs-light-size) at var(--liquid-glass-tabs-x, -999px) var(--liquid-glass-tabs-y, -999px), #000, transparent); |
| 185 | transition: backdrop-filter 240ms ease-out, --liquid-glass-tabs-light-size 300ms ease-out; |
| 186 | } |
| 187 | [data-liquid-glass-tabs-pointer] > .liquid-glass-tabs-light { |
| 188 | -webkit-backdrop-filter: brightness(1.3) saturate(1.15); |
| 189 | backdrop-filter: brightness(1.3) saturate(1.15); |
| 190 | } |
| 191 | [data-liquid-glass-tabs-appearance="light"][data-liquid-glass-tabs-pointer] > .liquid-glass-tabs-light { |
| 192 | -webkit-backdrop-filter: brightness(1.05) saturate(1.1); |
| 193 | backdrop-filter: brightness(1.05) saturate(1.1); |
| 194 | } |
| 195 | /* As iOS's tab bar does, a press brightens the glass under it, and letting go |
| 196 | the light spreads across the bar as it fades. */ |
| 197 | [data-liquid-glass-tabs-press] > .liquid-glass-tabs-light { |
| 198 | --liquid-glass-tabs-light-size: 40px; |
| 199 | -webkit-backdrop-filter: brightness(1.6) saturate(1.2); |
| 200 | backdrop-filter: brightness(1.6) saturate(1.2); |
| 201 | transition: backdrop-filter 100ms ease-out, --liquid-glass-tabs-light-size 100ms ease-out; |
| 202 | } |
| 203 | [data-liquid-glass-tabs-appearance="light"][data-liquid-glass-tabs-press] > .liquid-glass-tabs-light { |
| 204 | -webkit-backdrop-filter: brightness(1.05) saturate(1.15); |
| 205 | backdrop-filter: brightness(1.05) saturate(1.15); |
| 206 | } |
| 207 | [data-liquid-glass-tabs-release] > .liquid-glass-tabs-light { |
| 208 | --liquid-glass-tabs-light-size: 160px; |
| 209 | -webkit-backdrop-filter: brightness(1); |
| 210 | backdrop-filter: brightness(1); |
| 211 | transition: backdrop-filter 500ms ease-out, --liquid-glass-tabs-light-size 500ms cubic-bezier(0.23, 1, 0.32, 1); |
| 212 | } |
| 213 | @media (prefers-reduced-motion: reduce) { |
| 214 | [data-liquid-glass-tabs-press] > .liquid-glass-tabs-light, |
| 215 | [data-liquid-glass-tabs-release] > .liquid-glass-tabs-light { |
| 216 | --liquid-glass-tabs-light-size: 56px; |
| 217 | } |
| 218 | } |
| 219 | /* Hovering another tab brightens its patch of glass, as the well does. */ |
| 220 | [data-liquid-glass-tab]::before { |
| 221 | content: ""; |
| 222 | position: absolute; |
| 223 | inset: 0; |
| 224 | z-index: -1; |
| 225 | border-radius: inherit; |
| 226 | pointer-events: none; |
| 227 | transition: backdrop-filter 150ms ease-out; |
| 228 | } |
| 229 | [data-liquid-glass-tab]:not([data-liquid-glass-tab-selected]):hover::before { |
| 230 | -webkit-backdrop-filter: brightness(1.2); |
| 231 | backdrop-filter: brightness(1.2); |
| 232 | } |
| 233 | [data-liquid-glass-tabs-appearance="light"] [data-liquid-glass-tab]:not([data-liquid-glass-tab-selected]):hover::before { |
| 234 | -webkit-backdrop-filter: brightness(0.96); |
| 235 | backdrop-filter: brightness(0.96); |
| 236 | } |
| 237 | /* The selected tab can be dragged across the others. */ |
| 238 | [data-liquid-glass-tabs="horizontal"] [data-liquid-glass-tab-selected] { |
| 239 | touch-action: pan-y; |
| 240 | } |
| 241 | /* Under the labels at rest, over them while it is a lens (data-raised), so it can |
| 242 | bend and magnify them. It goes back under them as it finishes dropping in, by |
| 243 | when it no longer magnifies. Its shadow grows with the lift |
| 244 | (--liquid-glass-tabs-lift, 0 to 1). */ |
| 245 | .liquid-glass-tabs-thumb { |
| 246 | position: absolute; |
| 247 | top: 0; |
| 248 | left: 0; |
| 249 | z-index: 0; |
| 250 | pointer-events: none; |
| 251 | box-shadow: |
| 252 | 0 8px 20px -6px rgb(0 0 0 / calc(0.35 * var(--liquid-glass-tabs-lift, 0))), |
| 253 | 0 2px 6px -2px rgb(0 0 0 / calc(0.15 * var(--liquid-glass-tabs-lift, 0))); |
| 254 | } |
| 255 | .liquid-glass-tabs-thumb:not([data-shown]) { |
| 256 | visibility: hidden; |
| 257 | } |
| 258 | .liquid-glass-tabs-thumb[data-raised] { |
| 259 | z-index: 2; |
| 260 | } |
| 261 | [data-liquid-glass-tabs-appearance="light"] > .liquid-glass-tabs-thumb { |
| 262 | box-shadow: |
| 263 | 0 8px 20px -6px rgb(0 0 0 / calc(0.18 * var(--liquid-glass-tabs-lift, 0))), |
| 264 | 0 2px 6px -2px rgb(0 0 0 / calc(0.08 * var(--liquid-glass-tabs-lift, 0))); |
| 265 | } |
| 266 | /* The selected tab is a well in the glass: brighter glass in dark, dimmer in |
| 267 | light, with nothing painted over it. It clears as the lens lifts and refills |
| 268 | after it drops, by --liquid-glass-tabs-platter (1 at rest, 0 clear), and the |
| 269 | pointer over it brightens it further. Safari ignores custom properties in |
| 270 | -webkit-backdrop-filter, so it keeps the resting values. */ |
| 271 | .liquid-glass-tabs-well { |
| 272 | -webkit-backdrop-filter: brightness(1.45); |
| 273 | backdrop-filter: brightness(calc(1 + 0.45 * var(--liquid-glass-tabs-platter, 1))); |
| 274 | box-shadow: 0 1px 3px rgb(0 0 0 / calc(0.3 * var(--liquid-glass-tabs-platter, 1))); |
| 275 | transition: backdrop-filter 180ms ease-out, box-shadow 180ms ease-out; |
| 276 | } |
| 277 | [data-liquid-glass-tabs-hover] > .liquid-glass-tabs-thumb > .liquid-glass-tabs-well { |
| 278 | -webkit-backdrop-filter: brightness(1.7); |
| 279 | backdrop-filter: brightness(calc(1 + 0.7 * var(--liquid-glass-tabs-platter, 1))); |
| 280 | } |
| 281 | [data-liquid-glass-tabs-appearance="light"] > .liquid-glass-tabs-thumb > .liquid-glass-tabs-well { |
| 282 | -webkit-backdrop-filter: brightness(0.92); |
| 283 | backdrop-filter: brightness(calc(1 - 0.08 * var(--liquid-glass-tabs-platter, 1))); |
| 284 | box-shadow: none; |
| 285 | } |
| 286 | [data-liquid-glass-tabs-appearance="light"][data-liquid-glass-tabs-hover] > .liquid-glass-tabs-thumb > .liquid-glass-tabs-well { |
| 287 | -webkit-backdrop-filter: brightness(0.86); |
| 288 | backdrop-filter: brightness(calc(1 - 0.14 * var(--liquid-glass-tabs-platter, 1))); |
| 289 | } |
| 290 | /* Moving, those values change every frame; a transition would only lag them. */ |
| 291 | .liquid-glass-tabs-thumb[data-moving] > .liquid-glass-tabs-well { |
| 292 | transition: none; |
| 293 | } |
| 294 | /* The thumb is a clear lens over the well: the rim bends what is beneath, and the |
| 295 | body only lifts it a little. At rest that is the track under it; lifted, the labels. */ |
| 296 | .liquid-glass-tabs-lens { |
| 297 | isolation: isolate; |
| 298 | z-index: 1; |
| 299 | } |
| 300 | .liquid-glass-tabs-lens .liquid-glass-layer { |
| 301 | transition: backdrop-filter 180ms ease-out; |
| 302 | } |
| 303 | .liquid-glass-tabs-thumb[data-moving] .liquid-glass-tabs-lens .liquid-glass-layer { |
| 304 | transition: none; |
| 305 | } |
| 306 |
| 307 | /* With more contrast asked for, the sides darken and the edge becomes the |
| 308 | contrasting border Apple gives glass under Increase Contrast. */ |
| 309 | @media (prefers-contrast: more) { |
| 310 | .liquid-glass[data-appearance="dark"] > [data-layer="shade"] { |
| 311 | -webkit-backdrop-filter: brightness(0.1) !important; |
| 312 | backdrop-filter: brightness(0.1) !important; |
| 313 | } |
| 314 | .liquid-glass[data-appearance="light"] > [data-layer="shade"] { |
| 315 | -webkit-backdrop-filter: brightness(0.6) !important; |
| 316 | backdrop-filter: brightness(0.6) !important; |
| 317 | } |
| 318 | .liquid-glass[data-appearance="dark"] > [data-layer="edge"] { |
| 319 | -webkit-backdrop-filter: brightness(0.3) !important; |
| 320 | backdrop-filter: brightness(0.3) !important; |
| 321 | } |
| 322 | .liquid-glass[data-appearance="light"] > [data-layer="edge"] { |
| 323 | -webkit-backdrop-filter: brightness(0.5) !important; |
| 324 | backdrop-filter: brightness(0.5) !important; |
| 325 | } |
| 326 | } |
| 327 | @media (prefers-reduced-transparency: reduce) { |
| 328 | .liquid-glass { |
| 329 | display: none; |
| 330 | } |
| 331 | [data-liquid-glass] { |
| 332 | background: var(--liquid-glass-solid); |
| 333 | } |
| 334 | [data-liquid-glass] .liquid-glass-well { |
| 335 | -webkit-backdrop-filter: none; |
| 336 | backdrop-filter: none; |
| 337 | background: rgb(255 255 255 / 0.05); |
| 338 | } |
| 339 | .liquid-glass-tabs-light, |
| 340 | .liquid-glass-tabs-lens { |
| 341 | display: none; |
| 342 | } |
| 343 | [data-liquid-glass-tab]::before { |
| 344 | content: none; |
| 345 | } |
| 346 | /* Over the light and hover wells, which are more specific. */ |
| 347 | [data-liquid-glass-tabs] > .liquid-glass-tabs-thumb > .liquid-glass-tabs-well { |
| 348 | -webkit-backdrop-filter: none !important; |
| 349 | backdrop-filter: none !important; |
| 350 | background-color: rgb(255 255 255 / 0.1); |
| 351 | } |
| 352 | } |
| 353 | @media (forced-colors: active) { |
| 354 | .liquid-glass { |
| 355 | display: none; |
| 356 | } |
| 357 | .liquid-glass-tabs-well { |
| 358 | outline: 1px solid CanvasText; |
| 359 | } |
| 360 | } |
| 361 |