---
title: "Tools, cursors & touch — Svelte"
description: "The active tool decides what the pointer does: select, pan, draw. Switch tools, add your own, and choose cursors, touch behavior and haptics."
framework: "Svelte"
source: "https://www.embedpdf.com/docs/headless/svelte/ui/tools"
---

# Tools, cursors & touch

At any moment one **tool** is active, and it decides what a click or a drag on the page does: select
text, scroll, draw a rectangle, place a stamp. The interaction plugin keeps track of it, and every
feature that uses the pointer works through it. Register `interactionPlugin()`:

**`interaction-plugin.svelte`**

```svelte
<script lang="ts">
  import { interactionPlugin } from '@embedpdf/svelte/interaction';
  import { renderPlugin } from '@embedpdf/svelte/render';
  import { Viewer } from '@embedpdf/svelte/runtime';
  import { stagePlugin } from '@embedpdf/svelte/stage';
  import { engine } from './pdf';

  const plugins = [stagePlugin(), renderPlugin(), interactionPlugin()];
</script>

<Viewer {engine} {plugins}>
  <!-- your toolbar and pages -->
</Viewer>
```

Then switch tools from your toolbar:

**`tool-switch.svelte`**

```svelte
<script lang="ts">
  import { useInteraction, useInteractionState } from '@embedpdf/svelte/interaction';

  const interaction = useInteraction();
  const state = useInteractionState();
</script>

<button aria-pressed={state.activeToolId === 'pointer'} onclick={() => interaction.activateTool('pointer')}>
  Select
</button>
<button aria-pressed={state.activeToolId === 'pan'} onclick={() => interaction.activateTool('pan')}>
  Hand
</button>
```

**`App.svelte`**

```svelte
<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { interactionPlugin } from '@embedpdf/svelte/interaction';
  import { SelectionLayer, selectionPlugin } from '@embedpdf/svelte/selection';
  import { localEngine } from '@embedpdf/engine';
  import ToolSwitch from './ToolSwitch.svelte';

  import '../switch.css';

  const engine = localEngine();
  // The document opens with the hand tool.
  const plugins = [
    stagePlugin(),
    renderPlugin(),
    interactionPlugin({ defaultTool: 'pan' }),
    selectionPlugin(),
  ];

  const ebook = async (): Promise<OpenInput> => {
    const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
    return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <ToolSwitch />
    <Stage class="stage">
      <RenderLayer />
      <SelectionLayer />
    </Stage>
  </DocumentGate>
</Viewer>
```

**`switch.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --accent-tint: #e7f0fe;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The tool switch, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 14px;
  margin-bottom: 14px;
}

.segmented {
  display: inline-flex;
  gap: 2px;
  padding: 3px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 11px;
  box-shadow: 0 1px 2px var(--shadow);
}

.segment {
  min-width: 72px;
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  color: var(--muted);
  background: transparent;
  border: 0;
  border-radius: 8px;
  padding: 8px 12px;
  cursor: pointer;
  transition:
    background-color 0.15s ease,
    color 0.15s ease;
}

.segment:hover {
  color: var(--ink);
  background: var(--tint);
}

.segment[aria-pressed='true'] {
  color: var(--accent);
  background: var(--accent-tint);
}

.segment:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.readout {
  font-size: 13px;
  color: var(--muted);
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

**`ToolSwitch.svelte`**

```svelte
<script lang="ts" module>
  const LABELS: Record<string, string> = { pointer: 'Select', pan: 'Hand' };
  const HINTS: Record<string, string> = {
    pointer: 'A drag selects text',
    pan: 'A drag scrolls the pages',
  };
</script>

<!-- A button for every tool you can switch to, the active one pressed. -->
<script lang="ts">
  import { useInteraction, useInteractionState } from '@embedpdf/svelte/interaction';

  const interaction = useInteraction();
  const state = useInteractionState();
</script>

<div class="toolbar">
  <div class="segmented" role="group" aria-label="Tool">
    {#each state.tools as tool (tool.id)}
      <button
        type="button"
        class="segment"
        aria-pressed={tool.id === state.activeToolId}
        onclick={() => interaction.activateTool(tool.id)}
      >
        {LABELS[tool.id] ?? tool.id}
      </button>
    {/each}
  </div>
  <output class="readout">{state.activeToolId ? HINTS[state.activeToolId] : ''}</output>
</div>
```

## The built-in tools

| Tool      | A drag on the page                             | Also                                     |
| :-------- | :--------------------------------------------- | :--------------------------------------- |
| `pointer` | Selects text, or selects and moves annotations | Fills forms, follows links. The default. |
| `pan`     | Scrolls, like Acrobat's hand tool              | Fills forms, follows links               |

Plugins add their own: the annotation plugin a tool per kind (`square`, `highlight`, `note`…), the form
plugin a tool per field, redaction `redact`, measurements `distance`. They're all switched the same
way, with `activateTool(id)`, and `tools` in [State](#state) lists them.

## Hold a tool for a moment

`pushTool()` switches to a tool and remembers the one before; `popTool()` goes back. That's how
"hold Space to pan" works:

**`hold-space.svelte`**

```svelte
<script lang="ts">
  import { useInteraction } from '@embedpdf/svelte/interaction';

  const interaction = useInteraction();

  function down(event: KeyboardEvent) {
    if (event.code === 'Space' && !event.repeat) interaction.pushTool('pan');
  }
  function up(event: KeyboardEvent) {
    if (event.code === 'Space') interaction.popTool();
  }
</script>

<svelte:window onkeydown={down} onkeyup={up} />
```

**`App.svelte`**

```svelte
<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { interactionPlugin } from '@embedpdf/svelte/interaction';
  import { SelectionLayer, selectionPlugin } from '@embedpdf/svelte/selection';
  import { localEngine } from '@embedpdf/engine';
  import ActiveTool from './ActiveTool.svelte';
  import HoldSpaceToPan from './HoldSpaceToPan.svelte';

  import '../hold-space.css';

  const engine = localEngine();
  const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), selectionPlugin()];

  const ebook = async (): Promise<OpenInput> => {
    const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
    return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <ActiveTool />
    <HoldSpaceToPan>
      <Stage class="stage">
        <RenderLayer />
        <SelectionLayer />
      </Stage>
    </HoldSpaceToPan>
  </DocumentGate>
</Viewer>
```

**`hold-space.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --surface: #ffffff;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --accent-tint: #e7f0fe;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The active tool, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
  margin-bottom: 14px;
}

.badge {
  min-width: 56px;
  text-align: center;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 6px 12px;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    color 0.15s ease;
}

.badge[data-active='true'] {
  color: var(--accent);
  background: var(--accent-tint);
  border-color: transparent;
}

.readout {
  font-size: 13px;
  color: var(--muted);
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

**`ActiveTool.svelte`**

```svelte
<script lang="ts">
  import { useInteractionState } from '@embedpdf/svelte/interaction';

  const state = useInteractionState();
  const panning = $derived(state.activeToolId === 'pan');
</script>

<div class="toolbar">
  <span class="badge" data-active={panning}>{panning ? 'Hand' : 'Select'}</span>
  <span class="readout">
    {panning ? 'Drag to scroll, then let go of Space' : 'Hold Space over the pages to scroll'}
  </span>
</div>
```

**`HoldSpaceToPan.svelte`**

```svelte
<!-- While Space is held over the pages, the hand tool is active; on release, the tool from before. -->
<script lang="ts">
  import type { Snippet } from 'svelte';
  import { useInteraction } from '@embedpdf/svelte/interaction';

  let { children }: { children: Snippet } = $props();

  const interaction = useInteraction();
  // Only over the pages, so Space still scrolls the rest of your page.
  let viewer: HTMLDivElement | undefined = $state();
  let held = false;

  function down(event: KeyboardEvent) {
    if (event.code !== 'Space' || !viewer?.matches(':hover')) return;
    event.preventDefault();
    if (event.repeat || held) return;
    held = true;
    interaction.pushTool('pan');
  }

  function up(event: KeyboardEvent) {
    if (event.code !== 'Space' || !held) return;
    held = false;
    interaction.popTool();
  }
</script>

<svelte:window onkeydown={down} onkeyup={up} />

<div bind:this={viewer} class="viewer">
  {@render children()}
</div>
```

`activateDefaultTool()` goes back to the default tool, `pointer` unless you set another.

## Your own tool

A tool of your own gets the pointer on the page, in page coordinates:

```ts
const remove = interaction.registerTool({
  id: 'pin',
  cursor: 'copy',
  onPointerDown: ({ page, point }) => {
    addPin(page, point);
    return true; // this tool handled the click
  },
});

interaction.activateTool('pin');
```

**`App.svelte`**

```svelte
<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { renderPlugin } from '@embedpdf/svelte/render';
  import { stagePlugin } from '@embedpdf/svelte/stage';
  import { interactionPlugin } from '@embedpdf/svelte/interaction';
  import { localEngine } from '@embedpdf/engine';
  import PinBoard from './PinBoard.svelte';

  import '../pin.css';

  const engine = localEngine();
  const plugins = [stagePlugin(), renderPlugin(), interactionPlugin()];

  const ebook = async (): Promise<OpenInput> => {
    const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
    return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <PinBoard />
  </DocumentGate>
</Viewer>
```

**`pin.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --accent-tint: #e7f0fe;
  --pin: #e5484d;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The tools, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
  margin-bottom: 14px;
}

.segmented {
  display: inline-flex;
  gap: 2px;
  padding: 3px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 11px;
  box-shadow: 0 1px 2px var(--shadow);
}

.segment,
.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  cursor: pointer;
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease,
    color 0.15s ease;
}

.segment {
  min-width: 64px;
  color: var(--muted);
  background: transparent;
  border: 0;
  border-radius: 8px;
  padding: 8px 12px;
}

.segment:hover {
  color: var(--ink);
  background: var(--tint);
}

.segment[aria-pressed='true'] {
  color: var(--accent);
  background: var(--accent-tint);
}

.button {
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 10px 13px;
  box-shadow: 0 1px 2px var(--shadow);
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.segment:focus-visible,
.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.readout {
  font-size: 13px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* A pin: its tip on the point that was clicked */
.pin {
  position: absolute;
  box-sizing: border-box;
  min-width: 24px;
  height: 24px;
  padding: 0 5px;
  transform: translate(-50%, calc(-100% - 7px));
  display: grid;
  place-items: center;
  font-size: 11px;
  font-weight: 700;
  color: var(--surface);
  background: var(--pin);
  border: 2px solid var(--surface);
  border-radius: 999px;
  box-shadow: 0 2px 6px rgb(7 32 76 / 0.3);
  pointer-events: none;
}

.pin::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 100%;
  border: 6px solid transparent;
  border-top-color: var(--pin);
  transform: translate(-50%, 1px);
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

**`PinBoard.svelte`**

```svelte
<script lang="ts">
  import { onMount } from 'svelte';
  import { RenderLayer } from '@embedpdf/svelte/render';
  import { Stage } from '@embedpdf/svelte/stage';
  import { svgCursor, useInteraction, useInteractionState } from '@embedpdf/svelte/interaction';
  import PinLayer, { type Pin } from './PinLayer.svelte';

  // The cursor is the pin itself; its tip is the point that clicks.
  const PIN_CURSOR = svgCursor({
    svg: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
      <path d="M12 23s7.5-7.4 7.5-12.5a7.5 7.5 0 0 0-15 0C4.5 15.6 12 23 12 23z" fill="#e5484d" stroke="#fff" stroke-width="1.5"/>
      <circle cx="12" cy="10.5" r="2.8" fill="#fff"/>
    </svg>`,
    hotspot: { x: 12, y: 23 },
    fallback: 'copy',
  });

  const interaction = useInteraction();
  const tools = useInteractionState();
  let pins: readonly Pin[] = $state.raw([]);

  // A tool of your own: a click on a page drops a pin there, in page coordinates.
  onMount(() => {
    const remove = interaction.registerTool({
      id: 'pin',
      cursor: PIN_CURSOR,
      onPointerDown: ({ page, point }) => {
        pins = [...pins, { id: pins.length + 1, page, point }];
        return true; // this tool handled the click
      },
    });
    interaction.activateTool('pin');
    return remove;
  });
</script>

<div class="toolbar">
  <div class="segmented" role="group" aria-label="Tool">
    <button
      type="button"
      class="segment"
      aria-pressed={tools.activeToolId === 'pin'}
      onclick={() => interaction.activateTool('pin')}
    >
      Pin
    </button>
    <button
      type="button"
      class="segment"
      aria-pressed={tools.activeToolId === 'pan'}
      onclick={() => interaction.activateTool('pan')}
    >
      Hand
    </button>
  </div>
  <button type="button" class="button" disabled={pins.length === 0} onclick={() => (pins = [])}>
    Clear
  </button>
  <output class="readout">
    {pins.length === 0 ? 'Click a page to drop a pin' : `${pins.length} pins`}
  </output>
</div>
<Stage class="stage">
  {#snippet children(page)}
    <RenderLayer />
    <PinLayer {page} {pins} />
  {/snippet}
</Stage>
```

**`PinLayer.svelte`**

```svelte
<script lang="ts" module>
  import type { PageRef } from '@embedpdf/svelte/runtime';

  export interface Pin {
    readonly id: number;
    readonly page: PageRef;
    readonly point: { readonly x: number; readonly y: number };
  }
</script>

<!-- The pins on one page, placed in its pixels at the last moment. -->
<script lang="ts">
  import { pageRefsEqual, type PageContextValue } from '@embedpdf/svelte/runtime';

  let { page, pins }: { page: PageContextValue; pins: readonly Pin[] } = $props();
</script>

{#each pins.filter((pin) => pageRefsEqual(pin.page, page.ref)) as pin (pin.id)}
  {@const { x, y } = page.transform.toPixels(pin.point)}
  <span class="pin" style:left="{x}px" style:top="{y}px">{pin.id}</span>
{/each}
```

| Option                         | What it is                                                                                |
| :----------------------------- | :---------------------------------------------------------------------------------------- |
| `id`                           | The tool's name, for `activateTool(id)`                                                   |
| `cursor`                       | The cursor over the page, any CSS cursor                                                  |
| `onPointerDown`                | A press on a page: `page`, `point`, `modifiers`. Return `true` to take the gesture        |
| `onPointerMove`, `onPointerUp` | The rest of a gesture you took                                                            |
| `onHover`                      | The pointer moving over a page, with no button pressed                                    |
| `touch`                        | `'draw'`: one finger drives the tool; `'tap'` (default): a tap is a click, a drag scrolls |

`registerTool()` returns a function that removes the tool again. A tool you drag with takes the press and
follows the gesture:

**`App.svelte`**

```svelte
<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { renderPlugin } from '@embedpdf/svelte/render';
  import { stagePlugin } from '@embedpdf/svelte/stage';
  import { interactionPlugin } from '@embedpdf/svelte/interaction';
  import { localEngine } from '@embedpdf/engine';
  import Ruler from './Ruler.svelte';

  import '../ruler.css';

  const engine = localEngine();
  const plugins = [stagePlugin(), renderPlugin(), interactionPlugin()];

  const ebook = async (): Promise<OpenInput> => {
    const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
    return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <Ruler />
  </DocumentGate>
</Viewer>
```

**`ruler.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --surface: #ffffff;
  --canvas: #f7f9fc;
  --ruler: #e5484d;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The measurement, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 4px 16px;
  min-height: 20px;
  margin-bottom: 14px;
}

.readout {
  font-size: 14px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.readout.muted {
  font-size: 13px;
  font-weight: 450;
  color: var(--muted);
}

/* The line on the page, and its length at its middle */
.ruler-layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}

.ruler-line {
  stroke: var(--ruler);
  stroke-width: 2;
  stroke-dasharray: 6 4;
}

.ruler-end {
  fill: var(--surface);
  stroke: var(--ruler);
  stroke-width: 2;
}

.ruler-label {
  position: absolute;
  transform: translate(-50%, calc(-100% - 8px));
  font-size: 12px;
  font-weight: 600;
  white-space: nowrap;
  color: var(--surface);
  background: var(--ruler);
  border-radius: 999px;
  padding: 4px 9px;
  box-shadow: 0 2px 6px rgb(7 32 76 / 0.25);
  pointer-events: none;
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

**`Ruler.svelte`**

```svelte
<script lang="ts">
  import { onMount } from 'svelte';
  import { RenderLayer } from '@embedpdf/svelte/render';
  import { Stage } from '@embedpdf/svelte/stage';
  import { useInteraction } from '@embedpdf/svelte/interaction';
  import RulerLayer, { lengthOf, type Measurement, type Point } from './RulerLayer.svelte';

  const interaction = useInteraction();
  let measurement: Measurement | null = $state.raw(null);
  let pointer: Point | null = $state.raw(null);

  // A tool you drag with: the press starts a line, the moves stretch it, the release ends it.
  onMount(() => {
    const remove = interaction.registerTool({
      id: 'ruler',
      cursor: 'crosshair',
      touch: 'draw', // one finger measures, two fingers scroll and zoom
      onPointerDown: ({ page, point }) => {
        measurement = { page, from: point, to: point };
        return true;
      },
      onPointerMove: ({ point }) => {
        if (measurement) measurement = { ...measurement, to: point };
      },
      onPointerUp: ({ point }) => {
        if (measurement) measurement = { ...measurement, to: point };
      },
      onHover: ({ point }) => (pointer = point),
    });
    interaction.activateTool('ruler');
    return remove;
  });
</script>

<div class="toolbar">
  <output class="readout">
    {measurement ? lengthOf(measurement) : 'Drag on a page to measure'}
  </output>
  <output class="readout muted">
    {pointer ? `x ${Math.round(pointer.x)} · y ${Math.round(pointer.y)} pt` : ''}
  </output>
</div>
<Stage class="stage">
  {#snippet children(page)}
    <RenderLayer />
    <RulerLayer {page} {measurement} />
  {/snippet}
</Stage>
```

**`RulerLayer.svelte`**

```svelte
<script lang="ts" module>
  import type { PageRef } from '@embedpdf/svelte/runtime';

  export interface Point {
    readonly x: number;
    readonly y: number;
  }

  export interface Measurement {
    readonly page: PageRef;
    readonly from: Point;
    readonly to: Point;
  }

  // Page coordinates are points, 72 to the inch.
  export const lengthOf = ({ from, to }: Measurement) => {
    const inches = Math.hypot(to.x - from.x, to.y - from.y) / 72;
    return `${inches.toFixed(2)} in · ${(inches * 2.54).toFixed(1)} cm`;
  };
</script>

<!-- The measured line on its page, placed in the page's pixels. -->
<script lang="ts">
  import { pageRefsEqual, type PageContextValue } from '@embedpdf/svelte/runtime';

  let { page, measurement }: { page: PageContextValue; measurement: Measurement | null } = $props();
</script>

{#if measurement && pageRefsEqual(measurement.page, page.ref)}
  {@const from = page.transform.toPixels(measurement.from)}
  {@const to = page.transform.toPixels(measurement.to)}
  <svg class="ruler-layer">
    <line class="ruler-line" x1={from.x} y1={from.y} x2={to.x} y2={to.y} />
    <circle class="ruler-end" cx={from.x} cy={from.y} r={4} />
    <circle class="ruler-end" cx={to.x} cy={to.y} r={4} />
  </svg>
  <span
    class="ruler-label"
    style:left="{(from.x + to.x) / 2}px"
    style:top="{(from.y + to.y) / 2}px"
  >
    {lengthOf(measurement)}
  </span>
{/if}
```

## Cursors

Each tool has a cursor. Over things the tool can act on, the viewer switches to the right one by itself:
a move or resize cursor over an annotation, a text cursor over text, a hand over a link. You choose the
tool's own cursor, in three steps:

- **A CSS cursor**: `cursor: 'crosshair'`.
- **An image**: `cursor: svgCursor({ svg: penIcon, hotspot: { x: 2, y: 22 } })`. The SVG needs a
  `width` and a `height`, and `hotspot` is the point that clicks.
- **An image that changes**, such as a pen in the current color: `useToolCursor()` from a component, which follows the values it reads.

**`pen-cursor.svelte`**

```svelte
<script lang="ts">
  import { useToolCursor } from '@embedpdf/svelte/interaction';
  import { penIcon } from './icons';

  let { color }: { color: string } = $props();

  useToolCursor(() => ({
    toolId: 'ink',
    cursors: { crosshair: { svg: penIcon(color), hotspot: { x: 2, y: 22 } } },
  }));
</script>
```

**`App.svelte`**

```svelte
<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { renderPlugin } from '@embedpdf/svelte/render';
  import { stagePlugin } from '@embedpdf/svelte/stage';
  import { interactionPlugin } from '@embedpdf/svelte/interaction';
  import { localEngine } from '@embedpdf/engine';
  import Dots from './Dots.svelte';

  import '../cursor.css';

  const engine = localEngine();
  const plugins = [stagePlugin(), renderPlugin(), interactionPlugin()];

  const ebook = async (): Promise<OpenInput> => {
    const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
    return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <Dots />
  </DocumentGate>
</Viewer>
```

**`cursor.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --surface: #ffffff;
  --canvas: #f7f9fc;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The colors and the cursor switch, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 18px;
  margin-bottom: 14px;
}

.swatches {
  display: inline-flex;
  gap: 8px;
}

.swatch {
  width: 26px;
  height: 26px;
  padding: 0;
  border: 2px solid var(--surface);
  border-radius: 50%;
  box-shadow: 0 0 0 1px var(--line);
  cursor: pointer;
  transition: box-shadow 0.15s ease;
}

.swatch[aria-checked='true'] {
  box-shadow: 0 0 0 2px var(--ink);
}

.swatch:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 3px;
}

.check {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  color: var(--ink);
  cursor: pointer;
}

/* A dot where the pen clicked */
.dot {
  position: absolute;
  width: 12px;
  height: 12px;
  transform: translate(-50%, -50%);
  border: 2px solid var(--surface);
  border-radius: 50%;
  box-shadow: 0 1px 4px rgb(7 32 76 / 0.3);
  pointer-events: none;
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

**`DotLayer.svelte`**

```svelte
<script lang="ts" module>
  import type { PageRef } from '@embedpdf/svelte/runtime';

  export interface Dot {
    readonly id: number;
    readonly page: PageRef;
    readonly point: { readonly x: number; readonly y: number };
    readonly color: string;
  }
</script>

<script lang="ts">
  import { pageRefsEqual, type PageContextValue } from '@embedpdf/svelte/runtime';

  let { page, dots }: { page: PageContextValue; dots: readonly Dot[] } = $props();
</script>

{#each dots.filter((dot) => pageRefsEqual(dot.page, page.ref)) as dot (dot.id)}
  {@const { x, y } = page.transform.toPixels(dot.point)}
  <span class="dot" style:left="{x}px" style:top="{y}px" style:background={dot.color}></span>
{/each}
```

**`Dots.svelte`**

```svelte
<script lang="ts">
  import { onMount } from 'svelte';
  import { RenderLayer } from '@embedpdf/svelte/render';
  import { Stage } from '@embedpdf/svelte/stage';
  import { useInteraction } from '@embedpdf/svelte/interaction';
  import DotLayer, { type Dot } from './DotLayer.svelte';
  import PenCursor from './PenCursor.svelte';

  const COLORS = ['#e5484d', '#2f80ed', '#30a46c', '#1a2748'];

  const interaction = useInteraction();
  let color = $state(COLORS[0]!);
  let penCursor = $state(true);
  let dots: readonly Dot[] = $state.raw([]);

  // The tool reads the color at the moment of the click.
  onMount(() => {
    const remove = interaction.registerTool({
      id: 'dot',
      cursor: 'crosshair',
      onPointerDown: ({ page, point }) => {
        dots = [...dots, { id: dots.length + 1, page, point, color }];
        return true;
      },
    });
    interaction.activateTool('dot');
    return remove;
  });
</script>

{#if penCursor}
  <PenCursor {color} />
{/if}
<div class="toolbar">
  <div class="swatches" role="radiogroup" aria-label="Color">
    {#each COLORS as swatch (swatch)}
      <button
        type="button"
        role="radio"
        aria-checked={swatch === color}
        aria-label={swatch}
        class="swatch"
        style:background={swatch}
        onclick={() => (color = swatch)}
      ></button>
    {/each}
  </div>
  <label class="check">
    <input type="checkbox" bind:checked={penCursor} />
    Pen cursor
  </label>
</div>
<Stage class="stage">
  {#snippet children(page)}
    <RenderLayer />
    <DotLayer {page} {dots} />
  {/snippet}
</Stage>
```

**`PenCursor.svelte`**

```svelte
<script lang="ts" module>
  // A pen in the current color, with its tip at the bottom left.
  const penIcon = (color: string) =>
    `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
      <path d="M3 21l1.6-5.6L16.2 3.8a2.2 2.2 0 0 1 3.1 3.1L7.7 18.5z" fill="${color}" stroke="#fff" stroke-width="1.5" stroke-linejoin="round"/>
    </svg>`;
</script>

<!-- While this is mounted, the dot tool's crosshair is a pen in `color`. -->
<script lang="ts">
  import { useToolCursor } from '@embedpdf/svelte/interaction';

  let { color }: { color: string } = $props();

  useToolCursor(() => ({
    toolId: 'dot',
    cursors: { crosshair: { svg: penIcon(color), hotspot: { x: 3, y: 21 } } },
  }));
</script>
```

`cursors` replaces the tool's cursors by name. Pass a function, and the cursor follows `color`; the
tool's own comes back when the component unmounts.

See [The cursor](../annotations/tools#the-cursor) for annotation tools.

## Touch

On a touch screen, the viewer behaves like a native app:

- **One finger scrolls**, and two fingers pinch-zoom, with `pointer` and `pan`.
- **Drawing tools take one finger**: it draws, and two fingers still scroll and zoom.
- **Click tools**, such as placing a note, place with a tap, and a drag still scrolls.
- **Long-press selects a word**, and handles grow the selection.

A tool of your own chooses with `touch`: `'draw'` for tools you drag with, `'tap'` for tools you
click with.

## Haptic feedback

Register a feedback provider, and a long-press that lands on a word gives the small tick people know
from native apps:

**`haptics.svelte`**

```svelte
<script lang="ts">
  import { feedbackPlugin, interactionPlugin, vibrationFeedback } from '@embedpdf/svelte/interaction';
  import { Viewer } from '@embedpdf/svelte/runtime';
  import { engine } from './pdf';

  const plugins = [/* … */ interactionPlugin(), feedbackPlugin({ provider: vibrationFeedback })];
</script>

<Viewer {engine} {plugins}>
  <!-- your toolbar and pages -->
</Viewer>
```

It's safe to leave in: where the browser can't vibrate, iOS Safari included, nothing happens. In a
native app shell, pass `wkFeedback('yourHandlerName')` to use the app's own haptics.

## Methods

Everything `useInteraction()` gives you.

| Method                                                        | What it does                                                                                                                                                                          |
| :------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `activateTool(id)`                                            | Makes a tool the active one. An id that isn't registered is refused. [The built-in tools](#the-built-in-tools)                                                                        |
| `pushTool(id)`, `popTool()`                                   | Switches to a tool and remembers the one before; goes back to it. [Hold a tool for a moment](#hold-a-tool-for-a-moment)                                                               |
| `activateDefaultTool()`                                       | Goes back to the default tool                                                                                                                                                         |
| `getDefaultToolId()`                                          | The tool a document opens with, `pointer` unless you set another                                                                                                                      |
| `registerTool(tool, { replace? })`                            | Adds a tool of your own, and returns a function that removes it. An id that's taken is refused unless you pass `replace: true`. [Your own tool](#your-own-tool)                       |
| `setToolCursor(id, cursors)`                                  | Replaces a tool's cursors by name while the app runs; `null` brings its own back. In a component, use `useToolCursor()`, which undoes it when the component goes. [Cursors](#cursors) |
| `getSettings()`, `updateSettings(changes)`, `resetSettings()` | Reads the settings, changes them while the app runs, or goes back to what you registered. In a component, use `useInteractionSettings()`. [Settings](#settings)                       |

## State

Everything `useInteractionState()` gives you, as one reactive object: read its fields, and your markup updates when they change. The method in the last column reads the same value once.

| Field          | What it is                                                                          | Read once                                                    |
| :------------- | :---------------------------------------------------------------------------------- | :----------------------------------------------------------- |
| `activeToolId` | The active tool's id                                                                | `getActiveToolId()`, or `getActiveTool()` for the whole tool |
| `tools`        | Every tool you can switch to, in the order they were added, the built-in ones first | `listTools()`, or `getTool(id)` and `hasTool(id)` for one    |

## Settings

Set them when you register the plugin, and change them while the app runs with
`interaction.updateSettings({ … })`; `interaction.resetSettings()` goes back to what you registered, and
`useInteractionSettings()` gives you the current settings.

| Setting       | What it does                                 | Default     |
| :------------ | :------------------------------------------- | :---------- |
| `defaultTool` | The tool that's active when a document opens | `'pointer'` |
| `tools`       | Your own tools, registered from the start    | none        |

## Events

**`tool-events.svelte`**

```svelte
<script lang="ts">
  import { useInteractionEvent } from '@embedpdf/svelte/interaction';
  import { analytics } from './analytics';

  useInteractionEvent(
    (interaction) => interaction.onToolChanged,
    ({ toolId }) => analytics.track('tool', { toolId }),
  );
</script>

<!-- your toolbar and pages -->
```

| Event                | Fires when                                       | Carries                         |
| :------------------- | :----------------------------------------------- | :------------------------------ |
| `onToolChanged`      | Another tool became active                       | `toolId`, `previousToolId`      |
| `onGestureStarted`   | A drag with a tool began                         | `toolId`, `page`, `pointerType` |
| `onGestureEnded`     | It ended                                         | `toolId`, `page`, `pointerType` |
| `onGestureCancelled` | It was cancelled, for example by a second finger | `toolId`, `page`, `pointerType` |
| `onSettingsChanged`  | The settings changed                             | `settings`, `changed`           |

## Good to know

### Each document has its own tool

The active tool belongs to the document: switch tabs, and each document is in the tool you left it in.

### Permissions

Tools don't check permissions themselves; the features behind them do. A drawing tool without
permission to create draws nothing. Hide the buttons with each plugin's checks, such as
`annotation.canCreate()`.
