- Shipped
- September 5, 2026 at 6:45 PM UTC
- Author
- Kamo
- Commit
- b1bc689
The terminal tool window had a scrollbar worth keeping — 32px, with a button for each END of the scrollback as well as the usual step arrows, because in a long document the two ends are not two more positions, they are the two places you actually want. The rest of the app had thirty-four hand-written copies of a lesser idea: 138 `::-webkit-scrollbar` declarations at six different widths (4, 6, 8, 12, 14, 20px), plus three global classes at a seventh. A guard test already existed because four chat windows had drifted to three widths; it could only ever fix a twelfth of the problem. The bar is now a component, painted OVER the element it scrolls rather than placed inside it. That is the decision everything else follows from. Wrapping each scroller to seat a bar beside the content would be a layout change in all 228 of them — flex children, grid children, `minHeight: 0` columns — and would still not reach the scrollports this app does not own: ag-Grid's, the DataGrid's, Lexical's. Painting over them is a layout change in none. The price is paid in overlayGeometry.ts: clipping by intersecting the clipping ancestors, stacking from the nearest ancestor's z-index plus one (which puts a page's bar under an open dialog and a dialog's bar over it, from one rule), and standing down when MUI marks the content behind a modal aria-hidden. Adoption is by marker class, not by sweeping the document for `overflow: auto`. Both reach every scroller; only one is debuggable, and only one keeps the bar out of every Select menu and 40px popper in the app. scripts/codemod-kamo-scroll.mjs applied it — parsing with TypeScript's own parser and splicing at byte offsets, so formatting and CRLF survive — and stripped the declarations it made dead. It degrades, because it now hangs off a 250px emoji popover as well as a page: buttons and track at 140px or longer, track alone below that, nothing at all when the element is not overflowing. Horizontal is the same component with the icons turned a quarter turn rather than a second set that would drift. A scroller that wants NO bar — the collapsed nav rail, the console mastheads, the filmstrip, the profile rail on a phone — now says so once, with `data-kamo-scrollbar="off"`, instead of in the six different ways it used to be said. Only TerminalTool still names a scrollbar in CSS, to hide xterm's own. TerminalScrollbar is left as an adapter: an xterm buffer into the model, in LINES rather than pixels, and moves back out as xterm calls or the wheel reports tmux binds. It keeps the indeterminate grip for a scrollback that cannot say where it is, and the lit jump-to-end button for output below the fold. Guards: oneScrollbar.test.ts fails if anything outside globals.css styles a scrollbar, if the component's width and the stylesheet's token disagree, or if the layer's marker list and the stylesheet's hide list fall out of step — an element on one and not the other gets two bars or none, and both read as a rendering glitch rather than a missing line. scrollbarView.test.tsx drives the bar from the keyboard, which is the point of the buttons being buttons.