developer guide
Data Grid Scrollbars: Always Visible or Drawn
A scrollbar that only appears while you scroll hides how much data there is. The grid can keep them visible on either axis, or draw its own so they look and behave the same on every platform, and either way they are announced to assistive technology.
Developer guide › Theming and density › Data Grid Scrollbars: Always Visible or Drawn
Always-visible and grid-drawn scrollbars
Native scrollbars are overlay bars on most platforms now: they fade away when the pointer is
idle, which reads as a cleaner surface but hides the affordance - a touchpad user has no
standing sign that a grid scrolls at all. scrollbars takes three modes.
'auto' (the default) leaves the platform's own behaviour alone, so an existing
grid is unchanged on upgrade. 'always' keeps the platform's bar shown whether or
not the pointer is over the grid. 'custom' replaces it with a bar the grid draws
itself.
The two axes are separate decisions, so the object form controls each on its own:
{ y: 'always' } pins the vertical bar while the horizontal one stays native, and
{ x: 'always', y: 'always' } is the same as the bare 'always'. Under
'always' the pinned axis switches to overflow: scroll so its track is
present even when the content fits, and the scrollbar is styled as a classic, always-drawn bar
rather than a fading overlay. The same viewport also suppresses the
overscroll rubber-band bounce, so a synchronised grid does not spring at its scroll boundary.
Why 'custom' exists, and when to reach for it
'always' pins the native bar, which leaves two things it cannot fix. Its
size is still the platform's - on Chrome/macOS a 7 pixel overlay ribbon, small to
see and fiddly to grab. And the rules that style it are a WebKit/Blink extension, so Firefox
ignores them: 'always' is not the same feature there. 'custom' makes
the grid draw the bar, so its thickness, colour, minimum thumb length and hit area are the
same in every browser on every platform, and all of them are theme tokens a host can raise.
The default is a 12 pixel track with a 32 pixel minimum thumb, which is a comfortably
larger target than the platform's own.
Scrolling is unchanged. The drawn bars are display and input only: the grid's
body still scrolls itself, so the wheel, the trackpad, a finger, the keyboard and
scrollToRow behave exactly as they do in the other two modes, with the browser's
own momentum and acceleration. The thumb is a readout of the scroll position that happens to
be draggable, and it is placed in the same painted frame as the content, so it never trails
what is on screen.
Everything a scrollbar does. The thumb's length is the fraction of the content
on screen, never shorter than --lattice-scrollbar-thumb-min; dragging it scrolls;
pressing the track above or below it pages by one viewport; and with the bar focused the
arrows, Page Up/Page Down, Home and End
all work. Each bar carries role="scrollbar" with
aria-orientation, aria-controls and
aria-valuenow, and its accessible name comes from the message catalogue, so it is
announced in the grid's own language. It is deliberately not in the page's tab order:
the grid is a single tab stop and its own arrow keys already scroll.
Two things to know. The gutter the drawn bar occupies is reserved permanently
while the mode is on - the same 12 pixels 'always' reserves, so column
widths and columns.fit() are unaffected by the change and nothing moves under the
pointer as rows come and go. And no browser lets one axis' native scrollbar be hidden on its
own, so setting 'custom' on one axis hides the native bar on both; the
grid warns once if the two axes disagree. Set 'custom' on both axes, or on
neither.
Theming the drawn bar
| Token | Default | What it sets |
|---|---|---|
--lattice-scrollbar-size | 12px | The thickness of each bar, and the gutter reserved for it. Raise it for a larger grab target. |
--lattice-scrollbar-thumb-min | 32px | The shortest the thumb may ever be. Sized strictly in proportion, a very long list gives a thumb of a pixel or two. |
--lattice-scrollbar-track | transparent | The track behind the thumb. |
--lattice-scrollbar-thumb | var(--lattice-border-strong) | The thumb at rest. Derived from the theme's line colour, so dark, high-contrast and terminal themes follow without declaring anything. |
--lattice-scrollbar-thumb-hover | var(--lattice-foreground-muted) | The thumb while the pointer is over its bar. |
--lattice-scrollbar-thumb-active | var(--lattice-accent) | The thumb while it is being dragged, or while the bar has keyboard focus. |
--lattice-scrollbar-radius | var(--lattice-radius-pill) | The thumb's corner radius. Set it to 0 for a square thumb. |
The three modes, and one axis at a time
// The renderer resolves `scrollbars` to a per-axis mode it stamps on the root.
const { resolveScrollbars } = await import('../packages/dom/src/renderer/renderer.js');
const both = resolveScrollbars('always'); // pins both native bars
const drawn = resolveScrollbars('custom'); // the grid draws both
const onlyY = resolveScrollbars({ y: 'always' }); // pins the vertical bar only
const mixed = resolveScrollbars({ x: 'custom' }); // draws x, leaves y native (and warns)
return `always: ${both.x}/${both.y}; custom: ${drawn.x}/${drawn.y}; `
+ `y-only: ${onlyY.x}/${onlyY.y}; mixed: ${mixed.x}/${mixed.y}`;
A bigger grab target than the platform's
createGrid(host, {
columns, rows,
// Always visible, the same in every browser, and drawn by the grid.
scrollbars: 'custom',
});
/* Wider and darker than the default, in your own stylesheet: */
.lattice {
--lattice-scrollbar-size: 16px;
--lattice-scrollbar-thumb-min: 48px;
--lattice-scrollbar-thumb: #8a9199;
}