A non-`auto` `scrollbar-width` or `scrollbar-color` makes Chromium and Safari discard every `::-webkit-scrollbar*` rule for that element, including `::-webkit-scrollbar-thumb:hover`. Declaring both unconditionally left the hover tokens rendering nowhere: the engines implementing the hover pseudo-element are exactly the ones the standard properties silence, and Firefox has no hover pseudo-element to fall back on. Both hover tokens and all four elevated surfaces' hover rebinds were therefore dead code. Measured in chromium on probe elements with `scrollbar-gutter: stable`: an 8px `::-webkit-scrollbar` alone reserved a 30px band, and adding `scrollbar-width: thin` dropped it to the 10px `thin` reserves. The standard properties now sit inside `@supports not selector(::-webkit-scrollbar)`, so Firefox takes them and WebKit-based engines take the pseudo-elements. The WebKit rules stay ungated: an engine without those pseudo-elements drops them as unknown selectors, and gating them would hide them from an engine that implements them without `selector()` — the pre-16.4 Safari the ungated form serves correctly. Three unit assertions pin the split by source offset, which the existing at-rule-flattening parser cannot see. The web e2e now reads the path chromium actually takes: the `auto` standard properties as the gate's signature, the pseudo-element sizing and track, the indirection variables resolved per throwaway probe, and the hover declaration as cascade rule text — chromium folds the `:hover` rule into `getComputedStyle(el, '::-webkit-scrollbar-thumb')`, so no computed query separates the states.
86 lines
3.7 KiB
CSS
86 lines
3.7 KiB
CSS
/* Scrollbar skin: the sole consumer of the four --dsw-alias-scrollbar-*
|
|
* tokens. Without it every scrolling region renders the UA scrollbar, which
|
|
* ignores the theme — a light native bar over the dark palette.
|
|
*
|
|
* The rules sit on `body`, not `html`: design-platform.css declares the
|
|
* --dsw-alias-* tokens on `body` (and the dark overrides on
|
|
* `body[data-ds-dark-theme]`), and custom properties only inherit downward,
|
|
* so an `html` rule resolves them to the guaranteed-invalid value and
|
|
* `scrollbar-color` falls back to `auto`.
|
|
*
|
|
* Surfaces pick their elevation by rebinding --dsh-scrollbar-thumb{,-hover}:
|
|
* the l1 pair here is the base-surface default, and an elevated surface
|
|
* (menu, popover, dialog) rebinds to the l2 pair on its own container. Both
|
|
* rendering paths below read the indirection, so one rebind reaches whichever
|
|
* path the engine took. */
|
|
|
|
body {
|
|
--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l1);
|
|
--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l1);
|
|
}
|
|
|
|
/* The two paths are mutually exclusive, and the gate is load-bearing rather
|
|
than defensive. A non-`auto` `scrollbar-width` or `scrollbar-color` makes
|
|
Chromium and Safari drop every `::-webkit-scrollbar*` rule for that
|
|
element, including `::-webkit-scrollbar-thumb:hover` — measured in chromium
|
|
as an 8px `::-webkit-scrollbar` width taking effect on its own and being
|
|
ignored as soon as `scrollbar-width: thin` is added. Declaring both
|
|
unconditionally therefore leaves the hover tokens with no rendering at all,
|
|
because the engines that implement the hover pseudo-element are exactly the
|
|
ones the standard properties silence, and Firefox has no hover
|
|
pseudo-element to fall back on.
|
|
|
|
`not selector(::-webkit-scrollbar)` is true only where the pseudo-element
|
|
is unimplemented, so Firefox takes the standard path and WebKit-based
|
|
engines take the pseudo-element path. An engine too old for the
|
|
`selector()` function makes the condition invalid, which evaluates false
|
|
and selects the pseudo-element path — the correct side for the pre-16.4
|
|
Safari that is the realistic case. */
|
|
@supports not selector(::-webkit-scrollbar) {
|
|
/* Declared on every element rather than inherited from `body`. Inheriting
|
|
would pass down the COLOUR already substituted at `body`, so a descendant
|
|
rebinding --dsh-scrollbar-thumb could not change it; re-declaring makes
|
|
each element substitute the variable as it sees it, which is what gives
|
|
an elevated surface a working rebind. `scrollbar-width` is not an
|
|
inherited property at all, so it needs the per-element declaration
|
|
regardless.
|
|
|
|
No hover counterpart exists on this path: `scrollbar-color` states one
|
|
thumb colour and the engine derives its own hover treatment. */
|
|
body,
|
|
body * {
|
|
scrollbar-width: thin;
|
|
scrollbar-color: var(--dsh-scrollbar-thumb) transparent;
|
|
}
|
|
}
|
|
|
|
/* Not gated in turn: an engine that does not implement these pseudo-elements
|
|
drops the rules as unknown selectors, so the gate would only restate what
|
|
selector matching already does. Not inherited either, hence the unscoped
|
|
selectors. */
|
|
::-webkit-scrollbar {
|
|
width: 8px;
|
|
height: 8px;
|
|
}
|
|
|
|
/* Track stays transparent so the thumb reads against whatever surface scrolls
|
|
under it; only the thumb carries a token colour. */
|
|
::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb {
|
|
border-radius: 4px;
|
|
background: var(--dsh-scrollbar-thumb);
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb:hover {
|
|
background: var(--dsh-scrollbar-thumb-hover);
|
|
}
|
|
|
|
/* Both scrollbars meeting in a corner: no separate token, so the corner
|
|
matches the transparent track rather than the UA's opaque default. */
|
|
::-webkit-scrollbar-corner {
|
|
background: transparent;
|
|
}
|