Skip to content

Click-driven routines

A routine is a list of places the cursor goes. At each stop it presses the element for real, so the mock changes through the handlers you already wrote. You never say when a page should change, only what gets clicked — the change is caused, not timed.

typescript
busk(root, {
initialScene: 'home',
start: [0.55, 0.25],
steps: [
{click: '[data-nav-item="alerts"]', wait: 900, moveFor: 550},
{click: '[data-row="p0"]', wait: 1500, moveFor: 700},
{click: '[data-close]', wait: 2600},
{to: [0.55, 0.25], wait: 900, moveFor: 900},
],
routes: [
{click: '[data-nav-item="alerts"]', scene: 'alerts'},
{click: '[data-row="p0"]', scene: 'case'},
],
});

Each step is one beat, and they run back to back. The loop is as long as the beats add up to — there is no duration to keep in step.

A step either presses something:

FieldDefaultWhat it does
clickSelector of the element to press. Really clicked.
wait0Pause before the cursor sets off. This is reading time for whatever the last press opened.
moveFor600How long the glide takes.
dwell250How long the cursor hovers on the target before pressing.

…or drifts somewhere without pressing:

FieldDefaultWhat it does
toA selector, or [x, y] as a fraction of the root’s size.
wait0Pause before setting off.
moveFor600How long the glide takes.

A drift is how you get the cursor back out of the way before the loop starts over, or park it somewhere neutral while something animates on its own.

Time goes into wait, not into the glide. wait is how long a visitor gets to look at what just appeared; moveFor is only the travel. A step that opens something dense wants a long wait on the next step, not a slow glide on this one.

A press is a stroke rather than an instant: the cursor goes down on the target, and the real click fires as it lifts. That is what stops a button from vanishing underneath its own click when it is the thing that changes the scene.

Because the beats are relative, you can drop a step into the middle of a routine and nothing after it needs touching.

A route says what a click does:

typescript
routes: [
{click: '[data-nav-item="alerts"]', scene: 'alerts'},
]

Routes do two jobs, which is the reason they are one list:

  1. They switch scenes — for the cursor’s clicks and for a visitor’s, identically.
  2. They mark what is clickable. Every route target gets is-interactive, which is what gives it a pointer cursor. So the things that look clickable are exactly the things that are, with no CSS list to maintain alongside.

Anything your own handlers do — opening a modal, filtering a table, toggling a row — needs no route at all. Busker clicked the element; your code took it from there. Routes are only for the part busker owns, which is which scene is up.

start is where the cursor rests before the first beat and after a drift, as a fraction of the root: [0.55, 0.25] is a bit right of centre, a quarter of the way down. It defaults to the middle.

Targets are resolved from the DOM on every frame, so a routine keeps working when the mock reflows, when the container resizes, and at every breakpoint. There are no coordinates to re-measure after a design change — if the button moved, the cursor moves with it.

Markup · Next: Hand-timed routines