Skip to main content

A custom scroll handler

Keeping the active bar centered inside a container whose height changes with the viewport โ€” a lesson player embedded in a resizable pane, a modal that stretches to fit its content โ€” cannot be expressed as a fixed scrollOffsetY. Assign an IScrollHandler to api.customScrollHandler and drive the scroll yourself using the container geometry at scroll time. Pick "Center bar" in the mode pills below to switch from the built-in strategies to a handler that keeps the current bar vertically centered in the scroll container.

  • Continuous
  • OffScreen
  • Smooth
  • Off
  • Center bar

When to reach for itโ€‹

  • The active bar should sit at a container-relative position โ€” vertical center, one-third from the top, snapped to a grid โ€” that the fixed scrollOffsetY cannot express because it depends on the container height at render time.
  • The scroll target is not a DOM element โ€” a virtualized list, a canvas overlay, an external component.
  • The layout scrolls in a way alphaTab does not model โ€” paged pagination, snap points, an animation library.

IScrollHandler has two methods. forceScrollTo(beatBounds) is invoked when alphaTab needs an instant scroll (scrollToCursor(), seek). onBeatCursorUpdating(...) fires each time the beat cursor advances and passes the current bar bounds and the cursor's raw motion parameters. Both receive beatBounds.barBounds.masterBarBounds.realBounds โ€” the pixel rectangle of the bar in the render target โ€” which is enough to compute any container-relative target.

Setting customScrollHandler replaces alphaTab's default handler for the running session. Set it back to undefined to restore the built-in behavior.

Show it in codeโ€‹

The example below centers the active bar vertically inside a bounded scroll container โ€” the same logic the live demo uses.

import * as alphaTab from '@coderline/alphatab';

const container = getAlphaTabContainer();
const scroller = document.getElementById('scroller');
const api = new alphaTab.AlphaTabApi(container, {
core: { file: '/scores/song.gp' },
player: {
playerMode: alphaTab.PlayerMode.EnabledAutomatic,
scrollElement: scroller,
},
});

const centerBar = (beatBounds, behavior) => {
const bar = beatBounds.barBounds.masterBarBounds.realBounds;
const height = scroller.clientHeight;
const maxScroll = Math.max(0, scroller.scrollHeight - height);
const target = Math.min(
maxScroll,
Math.max(0, bar.y - (height - bar.h) / 2),
);
scroller.scrollTo({ top: target, behavior });
};

api.customScrollHandler = {
forceScrollTo(currentBeatBounds) {
centerBar(currentBeatBounds, 'instant');
},
onBeatCursorUpdating(startBeat) {
centerBar(startBeat, 'smooth');
},
[Symbol.dispose]() {},
};

getAlphaTabContainer() is a placeholder for the platform-specific step of obtaining the render target. Every other line runs unchanged on all three runtimes.

Where to look nextโ€‹