Redesigning the laser tool for better annotations

Jan 31, 2026

Steve Ruiz

When we first built tldraw's laser pointer tool, we modeled it on the way a laser pointer looks in real life: a point that leaves a tail behind it as it moves. We implemented it as a self-consuming polyline. As your pointer moves forward, points are removed from behind, creating a constant-length trail. Stop drawing, and the trail shrinks away.

This week, we redesigned the laser to work better for annotations.

We modeled the behavior after an app that I'd made a few years ago, telestrator, which in turn had been inspired by the screen annotation tool in an early version of Slack. In this design, the user's marks remain on the screen until the user stops drawing. After a moment, or if the user triggers a cancel event, the marks all fade away together.

While perhaps a little less beautiful, it works much better for annotation.

I think the implementation is pretty interesting, too, so I thought I'd share it here.

Scribbles

The tldraw SDK has a scribble system for managing scribbles, which are ephemeral freehand lines that are shown as an overlay on top of shapes but behind cursors. It uses the same variable width line algorithm as we use for our pen tool and highlighter, though scribbles aren't shapes in the same sense.

We use scribbles not only for our laser tool, but also for our eraser and the "scribble select" alt-mode of our select tool.

Up to now, the system managed an array of scribbles that could be in various states, handled interactions that modified or extended the scribbles, and managed the time-based points-shifting. A separate system (in the React component tree) rendered the user's scribbles or the scribbles belonging to collaborators.

Scribble sessions

With the new pattern, we would be moving from managing single scribbles to groups of scribbles, called sessions. These were opt in—the eraser may only need one scribble line per session—but essential so that we could handle the state of the session rather than the state of the individual scribble.

const sessionId = this.editor.scribbles.startSession({
	selfConsume: false, // don't eat the tail while drawing
	idleTimeoutMs: 1200, // auto-stop after inactivity
	fadeMode: 'grouped', // all scribbles fade together
	fadeEasing: 'ease-in', // smooth acceleration
})

A session's selfConsume flag controls the "shrinking tail" behavior found in the eraser and scribble-select tool. When false, as it is in the new laser tool, points will accumulate as you draw instead of being shifted off. The scribble manager handles the rest: when to keep things visible, when to start fading, and how to fade everything together.

The state machine

The LaserTool manages a single session ID that persists across multiple strokes.

When you start a click, we check if there's already an active session. If there's no active session then we start a new one and add the new scribble to it; otherwise, we create a new scribble within the current session.

getSessionId(): string {
  // Reuse existing session if it's still active
  if (this.sessionId && this.editor.scribbles.isSessionActive(this.sessionId)) {
    return this.sessionId
  }

  // Create a new session
  this.sessionId = this.editor.scribbles.startSession({ ... })

  return this.sessionId
}

As you drag around, we add points to the current session's active stroke.

We also mark the session as still active, resetting the "fade out" timer.

When you lift the pointer, we mark the scribble complete, but we don't stop the session. It stays alive, waiting for more strokes. After 1200ms of inactivity, the idle timeout fires and stops the session. At that point, all the scribble marks you made will fade out together.

Fading out together

The fade behavior took some thought, too. How should a group of lines disappear?

We first had each line self-consume independently. This version also still had a taper effect on the line. Like the old laser design, these lines also "evaporated" by reducing the line width as the tail consumed.

Next, we tried treating all of the lines as a single big scribble. This worked well, but it meant that lines with many sessions would take a long time to fade out.

We modulated the rate of consumption so that every session would complete in the same amount of time, regardless of the number of points in the session's scribbles. We also eased the rate so that the rate would start slowly, then increase toward the end, fixing a perceptual quirk where linear progression appears to slow down toward the end.

The final implementation looked like this:

private tickGroupedFade(session: Session, elapsed: number): void {
  const progress = session.fadeElapsed / session.options.fadeDurationMs
  const easedProgress = session.options.fadeEasing === 'ease-in'
    ? progress * progress
    : progress

  const targetRemoved = Math.floor(easedProgress * session.totalPointsAtFadeStart)
  const actuallyRemoved = session.totalPointsAtFadeStart - remainingPoints
  const pointsToRemove = Math.max(1, targetRemoved - actuallyRemoved)

  // Remove points from first scribble that has any
  let removed = 0
  let itemIndex = 0
  while (removed < pointsToRemove && itemIndex < session.items.length) {
    const item = session.items[itemIndex]
    if (item.scribble.points.length > 0) {
      item.scribble.points.shift()
      removed++
    } else {
      itemIndex++
    }
  }
}

Cancellation

One benefit of the session pattern is clean cancellation. If something interrupts the tool (pressing escape, switching tools, a cancel event) we can clear everything in one operation:

override onCancel() {
  if (this.sessionId && this.editor.scribbles.isSessionActive(this.sessionId)) {
    this.editor.scribbles.clearSession(this.sessionId)
    this.sessionId = null
    this.transition('idle')
  }
}

Even though the wait time is short, it's never nice to get locked into waiting for something to complete; better to give the user a manual way to cancel the session if they want to.

Scribbidi sdkoilet

You can try out the new laser tool on tldraw.com.

If you're a developer, you'll be able to use the new scribbles in the next SDK release, or right now on our pre-release branches. The scribble system is a general-purpose API, meaning you can use it for whatever you can imagine in your own tools. Remember that scribbles sync, too, so maybe you'll come up with some interesting use for multiplayer collaboration.

If you build something clever, let us know on the Discord!

© 2026 tldraw

© 2026 tldraw

© 2026 tldraw