Drive Triiiceratops Programmatically
Reach into a viewer's live state from your own code: read where it is, command it to move, and hear when it changes.
Every viewer owns one live ViewerState object. It is the same object the viewer's own toolbar, panels, and plugins use, and your code can access it too. Use it to read the current position, call commands to change it, and subscribe to changes.
Commands can do anything the built-in UI can do. Use them to build your own page controls, connect the viewer to the rest of your app, or script it from a test.
Get the state from your host: a property on the element, a hook in React, a template ref in Vue, a bound prop in Svelte.
Call commands on it, such as
nextCanvas(),setCanvas(id), orzoomIn().Read and subscribe when your UI needs to show what the viewer is doing.
Only step 1 differs between hosts. From step 2 on, the code is the same everywhere.
1. Get the state
The element exposes the state as a getter-only viewerState property, and fires viewerstateavailable when it appears:
import 'triiiceratops/element/register';
import type { TriiiceratopsViewerElement } from 'triiiceratops';
type ViewerState = NonNullable<TriiiceratopsViewerElement['viewerState']>;
const el = document.querySelector<TriiiceratopsViewerElement>(
'triiiceratops-viewer',
)!;
function drive(state: ViewerState) {
document
.querySelector('button#next')!
.addEventListener('click', () => state.nextCanvas());
}
// Listen first, then check: catches state published before or after this runs.
el.addEventListener('viewerstateavailable', (event) => {
drive((event as CustomEvent<ViewerState>).detail);
});
if (el.viewerState) drive(el.viewerState);Create a handle, pass it to the component, and read the state through useViewer():
import {
TriiiceratopsViewer,
useViewer,
useViewerHandle,
} from 'triiiceratops/react';
export function Reader() {
const handle = useViewerHandle();
const state = useViewer(handle);
return (
<>
<button type="button" onClick={() => state?.nextCanvas()}>
Next canvas
</button>
<TriiiceratopsViewer
handle={handle}
manifestId="https://example.org/manifest.json"
style={{ display: 'block', height: '600px' }}
/>
</>
);
}For values you render, use useViewerSelector() instead; see React: reactive reads.
A template ref is the handle. Its state member is the viewer state:
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import {
TriiiceratopsViewer,
type TriiiceratopsViewerInstance,
} from 'triiiceratops/vue';
const viewer = useTemplateRef<TriiiceratopsViewerInstance>('viewer');
function next(): void {
viewer.value?.state?.nextCanvas();
}
</script>
<template>
<button type="button" @click="next">Next canvas</button>
<TriiiceratopsViewer
ref="viewer"
manifest-id="https://example.org/manifest.json"
style="display: block; height: 600px"
/>
</template>For values you render, use useViewerSelector(); see Vue: reactive reads.
Bind viewerState. Its members are already reactive, so you can read them in markup or a $derived:
<script lang="ts">
import { TriiiceratopsViewer, type ViewerState } from 'triiiceratops/svelte';
import 'triiiceratops/style.css';
let viewerState = $state<ViewerState | undefined>();
</script>
<button onclick={() => viewerState?.nextCanvas()}>Next canvas</button>
<div style="height: 600px;">
<TriiiceratopsViewer
bind:viewerState
manifestId="https://example.org/manifest.json"
/>
</div>Inside a descendant component, getContext(VIEWER_STATE_KEY) returns the same instance; see Svelte.
2. Call commands
Commands are methods on the state. Call them; do not assign to its properties. state.setCanvas(id) updates everything that depends on the canvas, while state.canvasId = id skips that work and is not a supported API.
import type { ViewerState } from 'triiiceratops';
declare const state: ViewerState;
// Step through the manifest, or jump to a canvas by id.
state.nextCanvas();
state.setCanvas('https://example.org/iiif/canvas/p3');
// Open the annotations panel if it is closed.
if (!state.showAnnotations) state.toggleAnnotations();
// Frame a detail of the current canvas, in canvas coordinates.
state.fitBounds({ x: 1200, y: 800, width: 600, height: 400 });
// Open a different manifest at a chosen canvas.
void state.setManifest('https://example.org/iiif/other/manifest.json', {
canvasId: 'https://example.org/iiif/other/canvas/1',
});Common commands
Command | What it does |
|---|---|
| Step one canvas, or one spread in paged mode. Does nothing at either end. |
| Go to a canvas. Optionally start media at |
| Switch to another sequence in a manifest that has several. |
| Pick one image from a IIIF Choice on a canvas. |
| Fetch and open a manifest, optionally at a canvas. Returns a promise. |
| Open manifest JSON you already have. Returns a promise. |
| Zoom one step, like the toolbar's + and − buttons. |
| Zoom to an absolute scale, in screen pixels per canvas unit (the unit |
| Centre the view on a point in canvas coordinates. |
| Frame a box in canvas coordinates. |
| Fit a whole canvas: the current one, or go to the named one and fit it. |
| Re-fit what is on screen, like the Home key. |
| Open or close that panel. |
| Show the gallery, expand it into a grid, or dock it to |
| Open or close the toolbar. |
| Enter or leave fullscreen. Browsers only allow this from a user gesture, such as a click handler. |
| Switch between |
| In paged mode, shift the spreads by one page, for example to show a cover on its own. |
| Change the chrome's language. |
| Run a IIIF content search; results are written to |
| Show or hide annotation overlays. |
| Select an annotation, as tapping it does. Passing the selected id again deselects it. |
| Adjust how the image renders. Pass only the keys you change; numbers are percentages, with 100 meaning unchanged. |
| Open or close a plugin's panel or flyout. See controlling plugin UI at runtime. |
| Access a plugin's own commands, such as the audio and video plugin's |
This is the common set, not the complete list. The state inventory records every member of ViewerState and the command that changes it. To change configuration, set the config prop or property as described in Configuration.
3. Read and subscribe
Read members directly. Reads are synchronous and always current.
Member | Holds |
|---|---|
| The open manifest and canvas, or |
| The current sequence's canvases, and the open one's position in it ( |
| Whether |
|
|
| Whether each panel is open. |
| The last search and its results. |
| The canvases on screen: one, a spread, or the folios in view while scrolling. |
| Whether zoom, pan, and fit commands will take effect yet. |
| Where the view is right now. Read these on demand; they change every frame and never notify. |
To be notified of changes, call subscribe(). Notifications are batched and carry no payload: they mean “state changed, read what you need”. It returns a function that unsubscribes.
import type { ViewerState } from 'triiiceratops';
declare const state: ViewerState;
declare const counter: HTMLElement;
const stop = state.subscribe(() => {
counter.textContent = `${state.currentCanvasIndex + 1} / ${state.canvases.length}`;
});
// Later, when your control goes away:
stop();Viewport values are the exception: they move every frame, so subscribe() does not notify for them. Use subscribeFrame(), which has the same shape and fires as the view animates.
In a framework, prefer its reactive reads over a manual subscription: useViewerSelector() in React and Vue, or plain $derived in Svelte. The custom element also fires DOM events such as canvaschange; see reacting to state changes.
Common pitfalls
Viewport commands need a renderer.
zoomIn(),panTo(), and the fit commands do nothing untilrendererReadyis true.`manifestId` and `canvasId` set where the viewer starts. They do not hold it there: treat them as starting points, and call
setCanvas()to navigate later. See controlling the active canvas.One state per viewer. Two viewers on a page have two independent states. If the element is removed and re-added, it gets a new state and fires
viewerstateavailableagain; drop the old one. The React and Vue handles follow this for you.
For a full custom toolbar in every framework, see building your own chrome.