Reduce Stylus Latency on the Web with the Ink API

A focused female engineer wearing glasses meticulously reviews a complex technical blueprint displayed on a large, illuminated digital screen in a professional workspace, employing a stylus

Digital inking on the web has historically suffered from input-to-display latency. When a user draws on a <canvas> element using a stylus or finger, the input pipeline passes through several processing layers: hardware digitizers fire interrupts, the operating system handles the incoming device events, the browser process receives them, and those events are queued onto the JavaScript main thread. By the time your pointermove handler executes, updates the 2D or WebGL context, and the browser schedules paint and composite steps, multiple display frames may have passed. This delay creates a visible gap where stroke trails drag behind the physical stylus tip, degrading the user experience for digital signatures, note-taking, and whiteboard tools.

The Ink API tackles this issue by bypassing the JavaScript event loop for immediate stroke feedback. Instead of waiting on the browser’s standard rendering pipeline, the API allows web applications to delegate transient stroke rendering directly to the underlying operating system’s low-latency compositor. Calling navigator.ink.requestPresenter() returns a DelegatedInkTrailPresenter object that renders hardware-accelerated ink trails between pointer events. Because these trails are temporary and automatically cleared as your canvas strokes commit to the screen, you retain complete authority over your document state and canvas data while delivering native-feeling input responsiveness.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Ink API Demo</title>
    <link href="style.css" rel="stylesheet" />
    <script src="ink_API.js" defer></script>
</head>
    
<body>
    <!-- HTML: Canvas container for inking demo -->
    <canvas id="ink-canvas" width="800" height="500"></canvas>
</body>

</html>
/* CSS: Prevent default browser touch interactions from canceling drawing */
#ink-canvas {
    touch-action: none;
    /* Required to suppress default browser scroll and pinch gestures */
    background-color: #ffffff;
    border: 1px solid #cbd5e1;
    border-radius: 4px;
    cursor: crosshair;
    display: block;
}
// FULLY COMMENTED CODE DEMO
window.addEventListener('DOMContentLoaded', async () => {
	const canvas = document.querySelector('#ink-canvas');
	if (!canvas) {
		console.error('Canvas element not found.');
		return;
	}

	const ctx = canvas.getContext('2d');
	if (!ctx) {
		console.error('2D context is not supported on this device.');
		return;
	}

	// Define consistent stroke styling shared between the canvas and the Ink API
	const strokeStyle = {
		color: '#0f172a', // Slate 900
		diameter: 4 // Pixel width of the stroke
	};

	ctx.lineCap = 'round';
	ctx.lineJoin = 'round';
	ctx.lineWidth = strokeStyle.diameter;
	ctx.strokeStyle = strokeStyle.color;

	// Track active drawing stroke state
	let isDrawing = false;
	let lastX = 0;
	let lastY = 0;

	// Hold reference to the DelegatedInkTrailPresenter instance
	let inkPresenter = null;

	// Feature detection: Check for Ink API support before requesting a presenter
	if (
		'ink' in navigator &&
		typeof navigator.ink.requestPresenter === 'function'
	) {
		try {
			// Confine ink trail rendering to the canvas bounding box
			inkPresenter = await navigator.ink.requestPresenter({
				presentationArea: canvas
			});
		} catch (error) {
			// Fallback: If presenter acquisition fails, standard canvas drawing continues seamlessly
			console.warn(
				'Ink API initialization failed; continuing with standard rendering.',
				error
			);
			inkPresenter = null;
		}
	} else {
		// Informational fallback for unsupported engines (Firefox, Safari, older Chrome versions)
		console.info(
			'Ink API is not supported in this browser environment. Using standard canvas pipeline.'
		);
	}

	// Pointer Down: Initialize path tracking
	canvas.addEventListener('pointerdown', (event) => {
		// Restrict drawing to primary button interactions (stylus tip, touch, left click)
		if (event.button !== 0) return;

		isDrawing = true;
		lastX = event.offsetX;
		lastY = event.offsetY;

		// Retain input focus even if the pointer moves outside the canvas boundary
		canvas.setPointerCapture(event.pointerId);
	});

	// Pointer Move: Render persistent stroke and trigger predicted compositor trail
	canvas.addEventListener('pointermove', (event) => {
		if (!isDrawing) return;

		const currentX = event.offsetX;
		const currentY = event.offsetY;

		// 1. Draw the permanent segment to the 2D canvas context
		ctx.beginPath();
		ctx.moveTo(lastX, lastY);
		ctx.lineTo(currentX, currentY);
		ctx.stroke();

		lastX = currentX;
		lastY = currentY;

		// 2. Delegate low-latency trail rendering to the OS compositor
		// updateInkTrailStartPoint requires a trusted PointerEvent
		if (inkPresenter && event.isTrusted) {
			inkPresenter.updateInkTrailStartPoint(event, {
				color: strokeStyle.color,
				diameter: strokeStyle.diameter
			});
		}
	});

	// Shared cleanup logic for pointer release and boundary interruptions
	const stopDrawing = (event) => {
		if (!isDrawing) return;
		isDrawing = false;

		if (canvas.hasPointerCapture(event.pointerId)) {
			canvas.releasePointerCapture(event.pointerId);
		}
	};

	// Pointer Up & Cancel: Teardown pointer state
	canvas.addEventListener('pointerup', stopDrawing);
	canvas.addEventListener('pointercancel', stopDrawing);
});
more posts:
Online video streaming platform and digital content management concept with media player interface, cloud library, and multimedia files on laptop, representing content distribution, video marketing
Code

Breaking into the Pipeline: Real-Time Frame Manipulation with the Insertable Streams for MediaStreamTrack API

The experimental Insertable Streams for MediaStreamTrack API closes a long-standing gap in web multimedia by treating live camera feeds as standard Web Streams. By piping individual VideoFrame objects through a TransformStream and recording the transformed output via MediaRecorder, developers can execute frame-by-frame processing—like real-time filtering, watermarking, and computer vision overlays—without relying on hidden canvas hacks.

Discover More »
Apps & Custom Software

What Building Our Own App Taught Us About Building Yours

When Hess WebTech built and launched Tericipe—an Android app that turns printed recipes into a searchable digital collection—in under three months, we had to live with every decision we usually advise clients on. From testing against real-world messy data and handling AI uncertainty to server-side payment security and post-launch maintenance, here are five crucial lessons from our own build that apply to almost any software project.

Discover More »