- Shipped
- September 7, 2026 at 8:15 AM UTC
- Author
- Kamo
- Commit
- 158b8c8
The scrollbar already let a surface tint itself: five custom properties on the scroller, inherited, read by the overlay. For every screen in the app that is the whole of what "matching" means. It is not enough for the achievements cabinet, where the bar has to stop being a tasteful pill and become part of the furniture. So: a SKIN, resolved from `--kamo-scroll-skin` exactly as the palette is resolved — same mechanism, same inheritance, no new plumbing — carrying everything about the look that is not a colour: rounding, bevel, track and thumb texture, outline, and the class any motion hangs off. IT IS DATA, NOT COMPONENTS. A new skin is an entry in a table, not a branch inside the view, so the app still has genuinely one scrollbar: every skin gets the same five controls, the same keyboard handling, the same ARIA and the same drag maths. A test pins that the arcade skin still draws all four end buttons and a track, because a skin must never cost a member a control — those buttons are why this component replaced seven hand-written bars. `default` is unchanged and is what a caller that never mentions a skin gets, which is the risk worth guarding: it is what ~40 scrollers render with, and nothing else in the suite would notice it drifting. An unrecognised name resolves to it rather than throwing — this comes from a CSS custom property, so a stylesheet typo reaches it, and that should cost a screen its decoration, never its scrollbar. One real bug found on the way, and only because the skin exposed it: every colour here was written with the `background` SHORTHAND, including the two imperative hover handlers. The shorthand resets `background-image`, which is where a skin's texture lives — so the texture was correct at rest and erased the moment a pointer touched the thumb. All of it is longhand now, and a test asserts the shorthand never comes back. (The flat look in my first capture was jsdom's old CSS parser dropping the gradient entirely, not this bug. Chrome parses it fine either way. Worth saying because the two look identical in a dump and only one is real.)