Skip to content

Scrollbar

JS

A scrollbar is the kit’s own thumb, painted over a scrolling pane. Use it when you want the same chrome in Chrome, Firefox, and Safari. Native bars look different in each, and CSS can only recolor them. Don’t use this for a progress reading. That’s progress or gauge.

Wrap the content that overflows:

<k-scrollbar id="pane" class="k-scrollbar k-scrollbar--y" style="height: 12rem">
<ul>…</ul>
</k-scrollbar>

Give the host a height for a vertical pane and a width for a horizontal one. The element wraps the children in a viewport, hides the native bars, and paints overlay thumbs. Drag a thumb or click the track; the pane still takes wheel, touch, and keys.

This is the one element with no numbered content and, in the usual case, no options: axis, thickness, and autohide are modifier classes you write on the tag, and a class change patches without a rebuild.

Importing the kit already paints a thumb over the page, so you don’t set one up for the document. To paint over a scroller you didn’t write, point one at it:

document.getElementById('other').options = { target: '#their-pane' };

attachScrollbar(target) does the same in one call and reuses a scrollbar already attached to that node. Changing target rebuilds.

ClassTypeDescription
k-scrollbarcomponentThe one class you write. The element generates the viewport and thumbs inside it.
k-scrollbar--ymodifierPaint the vertical thumb only.
k-scrollbar--xmodifierPaint the horizontal thumb only.
k-scrollbar--smmodifierThinner rails. Skip it for 0.5rem.
k-scrollbar--lgmodifierThicker rails.
k-scrollbar--no-autohidemodifierKeep the rails showing. Without it they fade out until hover, focus, or scroll.
Generated classes
ClassTypeDescription
k-scrollbar__viewportpartThe scrolling pane. Native bars are hidden.
k-scrollbar__trackpartOne overlay rail. Combined with --y or --x.
k-scrollbar__thumbpartThe draggable thumb. Combined with --y or --x.

Skip --x and --y and the element paints both, hiding the rail for an axis that doesn’t overflow.

OptionTypeDefaultDescription
target"viewport" or a selector—Paint over another scroller instead of wrapping the host. viewport is the page.
PropertyTypeDescription
optionsKScrollbarOptionsRead/write. Writing a new target rebuilds.
axisKScrollbarAxisRead/write. x, y, or both. Writing sets the class.
targetstringRead/write. Empty in wrap mode.
autohidebooleanRead/write. False adds k-scrollbar--no-autohide.
sizeKScrollbarSize | undefinedRead/write. sm or lg. Undefined is the default thickness.
hasOverflowYbooleanRead-only. True when the pane is taller than its host.
hasOverflowXbooleanRead-only. True when the pane is wider than its host.
MethodReturnsDescription
goTo(top, left)voidMoves the pane and fires k-change.
getScroll()KScrollbarMetricsThe live scrollTop and scrollLeft. Both are 0 while disconnected.
getViewport()HTMLElement | nullThe scrolling node. In wrap mode that is the generated viewport. With a target, it is that element. Null while disconnected.
getThumbY()HTMLElement | nullThe vertical thumb, or null while disconnected.
getThumbX()HTMLElement | nullThe horizontal thumb, or null while disconnected.
refresh()voidRebuilds from the current classes and target.
disconnect()voidRemoves listeners.

Scroll changes dispatch k-change with { scrollTop, scrollLeft }, and the event bubbles. A class change does not fire it.

A 12rem host with more lines than fit. The native bar is gone, and the kit thumb fades in on the right as you scroll.

  • Overview
  • Install the kit
  • Put a class on a button
  • Import the JS once
  • Tabs label their own panels
  • Pagination counts the pages it finds
  • Dropdown builds the menu
  • Carousel drives the track next door
  • Gauge paints a square reading
  • This pane is taller than its host
  • Wheel, drag the thumb, or click the track
  • Native bars stay hidden
<k-scrollbar id="pane" class="k-scrollbar k-scrollbar--y" style="height: 12rem">
<ul>
<li>Overview</li>
<li>Install the kit</li>
<li>Put a class on a button</li>
<li>Import the JS once</li>
<li>Tabs label their own panels</li>
<li>Pagination counts the pages it finds</li>
<li>Dropdown builds the menu</li>
<li>Carousel drives the track next door</li>
<li>Gauge paints a square reading</li>
<li>This pane is taller than its host</li>
<li>Wheel, drag the thumb, or click the track</li>
<li>Native bars stay hidden</li>
</ul>
</k-scrollbar>
import 'k-web-ui/js';
import { Component, CUSTOM_ELEMENTS_SCHEMA, type AfterViewInit } from '@angular/core';
import 'k-web-ui/js';
@Component({
selector: 'app-example',
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<k-scrollbar id="pane" class="k-scrollbar k-scrollbar--y" style="height: 12rem">
<ul>
<li>Overview</li>
<li>Install the kit</li>
<li>Put a class on a button</li>
<li>Import the JS once</li>
<li>Tabs label their own panels</li>
<li>Pagination counts the pages it finds</li>
<li>Dropdown builds the menu</li>
<li>Carousel drives the track next door</li>
<li>Gauge paints a square reading</li>
<li>This pane is taller than its host</li>
<li>Wheel, drag the thumb, or click the track</li>
<li>Native bars stay hidden</li>
</ul>
</k-scrollbar>
`,
})
export class ExampleComponent implements AfterViewInit {
ngAfterViewInit() {
import 'k-web-ui/js';
}
}
import { useEffect } from 'react';
import 'k-web-ui/js';
export function Example() {
useEffect(() => {
import 'k-web-ui/js';
}, []);
return (
<>
<k-scrollbar id="pane" className="k-scrollbar k-scrollbar--y" style={{ height: '12rem' }}>
<ul>
<li>Overview</li>
<li>Install the kit</li>
<li>Put a class on a button</li>
<li>Import the JS once</li>
<li>Tabs label their own panels</li>
<li>Pagination counts the pages it finds</li>
<li>Dropdown builds the menu</li>
<li>Carousel drives the track next door</li>
<li>Gauge paints a square reading</li>
<li>This pane is taller than its host</li>
<li>Wheel, drag the thumb, or click the track</li>
<li>Native bars stay hidden</li>
</ul>
</k-scrollbar>
</>
);
}

k-scrollbar--no-autohide keeps them in view.

  • Overview
  • Install the kit
  • Put a class on a button
  • Import the JS once
  • Tabs label their own panels
  • Pagination counts the pages it finds
  • Dropdown builds the menu
  • Carousel drives the track next door
  • Gauge paints a square reading
  • This pane is taller than its host
  • Wheel, drag the thumb, or click the track
  • Native bars stay hidden
<k-scrollbar
id="pane-pinned"
class="k-scrollbar k-scrollbar--y k-scrollbar--no-autohide"
style="height: 12rem"
>
<ul>…</ul>
</k-scrollbar>
import 'k-web-ui/js';
import { Component, CUSTOM_ELEMENTS_SCHEMA, type AfterViewInit } from '@angular/core';
import 'k-web-ui/js';
@Component({
selector: 'app-example',
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<k-scrollbar
id="pane-pinned"
class="k-scrollbar k-scrollbar--y k-scrollbar--no-autohide"
style="height: 12rem"
>
<ul>…</ul>
</k-scrollbar>
`,
})
export class ExampleComponent implements AfterViewInit {
ngAfterViewInit() {
import 'k-web-ui/js';
}
}
import { useEffect } from 'react';
import 'k-web-ui/js';
export function Example() {
useEffect(() => {
import 'k-web-ui/js';
}, []);
return (
<>
<k-scrollbar
id="pane-pinned" className="k-scrollbar k-scrollbar--y k-scrollbar--no-autohide" style={{ height: '12rem' }}
>
<ul>…</ul>
</k-scrollbar>
</>
);
}

A wide strip in a short host. The thumb sits on the bottom.

  • Overview
  • Install the kit
  • Put a class on a button
  • Import the JS once
  • Tabs label their own panels
  • Pagination counts the pages it finds
  • Dropdown builds the menu
  • Carousel drives the track next door
  • Gauge paints a square reading
  • This pane is taller than its host
  • Wheel, drag the thumb, or click the track
  • Native bars stay hidden
<k-scrollbar id="strip" class="k-scrollbar k-scrollbar--x" style="height: 4.5rem">
<ul>…</ul>
</k-scrollbar>
import 'k-web-ui/js';
import { Component, CUSTOM_ELEMENTS_SCHEMA, type AfterViewInit } from '@angular/core';
import 'k-web-ui/js';
@Component({
selector: 'app-example',
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<k-scrollbar id="strip" class="k-scrollbar k-scrollbar--x" style="height: 4.5rem">
<ul>…</ul>
</k-scrollbar>
`,
})
export class ExampleComponent implements AfterViewInit {
ngAfterViewInit() {
import 'k-web-ui/js';
}
}
import { useEffect } from 'react';
import 'k-web-ui/js';
export function Example() {
useEffect(() => {
import 'k-web-ui/js';
}, []);
return (
<>
<k-scrollbar id="strip" className="k-scrollbar k-scrollbar--x" style={{ height: '4.5rem' }}>
<ul>…</ul>
</k-scrollbar>
</>
);
}

The viewport is in the tab order in wrap mode, so keys still scroll it. Each thumb has role="scrollbar", aria-orientation, aria-valuemin, aria-valuemax, and aria-valuenow. aria-controls points at the viewport when that node has an id. Give the host an id, since the generated ids derive from it.

Reduced motion drops the autohide fade. The thumbs still work.

Do

  • Give a wrapping host a height for k-scrollbar--y, and a width for k-scrollbar--x. Otherwise the pane grows and never overflows.
  • Use attachScrollbar() for a scroller you don’t own.
  • Listen for k-change when the scroll position matters to the rest of the UI.

Don’t

  • Restyle native ::-webkit-scrollbar on the same pane. The kit hides those.
  • Add a scrollbar for the page. The import already did.
  • Use a scrollbar to show task progress.
  • Point target at a node that isn’t there. The element throws.