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():
<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:
<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>Loading live preview…
<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>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 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:
<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} />Loading live preview…
<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>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:
const remove = interaction.registerTool({
id: 'pin',
cursor: 'copy',
onPointerDown: ({ page, point }) => {
addPin(page, point);
return true; // this tool handled the click
},
});
interaction.activateTool('pin');Loading live preview…
<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>| 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:
Loading live preview…
<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>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 awidthand aheight, andhotspotis 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.
<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>Loading live preview…
<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>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 for annotation tools.
Touch#
On a touch screen, the viewer behaves like a native app:
- One finger scrolls, and two fingers pinch-zoom, with
pointerandpan. - 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:
<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 |
pushTool(id), popTool() | Switches to a tool and remembers the one before; goes back to it. 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 |
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 |
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 |
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#
<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().
Your feedback goes directly to the documentation team.