Skip to main content

A custom cursor handler

A bouncing pill above the current beat, a lyric bubble that hops from word to word, a companion animation that rides the notation โ€” anything that has to move in lockstep with the cursor but is not the cursor. Assign an ICursorHandler to api.customCursorHandler and alphaTab hands you the cursor elements plus every placement callback. Toggle the demo below to swap between the default cursor and a handler that rides a bouncing pill adorner above the beat.

When to reach for itโ€‹

The CSS-level knobs cover most restyling. Reach for a custom handler when placement itself needs to change:

  • Extra DOM adornments that ride along with the cursor (a fingering pointer, a metronome ticker).
  • Animation curves the CSS transition cannot express (physics-driven follow, easing per cursor mode).
  • Coupling cursor motion to another surface (a scrubber on an external video track).

The handler implements ICursorHandler: onAttach, onDetach, placeBarCursor, placeBeatCursor, and transitionBeatCursor. alphaTab still creates the cursor IContainer elements โ€” your handler decides where they sit and how they move.

Show it in codeโ€‹

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

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

api.customCursorHandler = {
_adorner: undefined,

onAttach(cursors) {
this._adorner = document.createElement('div');
this._adorner.className = 'my-cursor-adorner';
cursors.cursorWrapper.element.appendChild(this._adorner);
},

onDetach(cursors) {
this._adorner?.remove();
this._adorner = undefined;
},

placeBarCursor(barCursor, beatBounds) {
const bar = beatBounds.barBounds.masterBarBounds.visualBounds;
barCursor.setBounds(bar.x, bar.y, bar.w, bar.h);
},

placeBeatCursor(beatCursor, beatBounds, startBeatX) {
const bar = beatBounds.barBounds.masterBarBounds.visualBounds;
beatCursor.transitionToX(0, startBeatX);
beatCursor.setBounds(startBeatX, bar.y, 1, bar.h);
this._adorner.style.transform = `translate(${startBeatX}px, ${bar.y - 12}px)`;
},

transitionBeatCursor(beatCursor, beatBounds, startBeatX, nextBeatX, duration, cursorMode) {
beatCursor.transitionToX(duration, nextBeatX);
this._adorner.style.transition = `transform ${duration}ms linear`;
this._adorner.style.transform = `translate(${nextBeatX}px, ${beatBounds.barBounds.masterBarBounds.visualBounds.y - 12}px)`;
},
};

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โ€‹