Lattice Grid Buy a licence

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 guideTheming 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

TokenDefaultWhat it sets
--lattice-scrollbar-size12pxThe thickness of each bar, and the gutter reserved for it. Raise it for a larger grab target.
--lattice-scrollbar-thumb-min32pxThe shortest the thumb may ever be. Sized strictly in proportion, a very long list gives a thumb of a pixel or two.
--lattice-scrollbar-tracktransparentThe track behind the thumb.
--lattice-scrollbar-thumbvar(--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-hovervar(--lattice-foreground-muted)The thumb while the pointer is over its bar.
--lattice-scrollbar-thumb-activevar(--lattice-accent)The thumb while it is being dragged, or while the bar has keyboard focus.
--lattice-scrollbar-radiusvar(--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;
}