Skip to content

Svelte

triiiceratops/svelte is the native Svelte 5 component — no custom element, no wrapper layer. Core ships the viewer as a source-distributed component, so your application compiles it inside its own Svelte runtime: it tree-shakes normally and never bundles a second copy of Svelte.

This is also the only entry point that needs the optional svelte peer installed. It is a superset of the root entry — everything triiiceratops exports is re-exported here, so a Svelte app imports from this one specifier and nothing else.

Install

pnpm add triiiceratops svelte
npm install triiiceratops svelte
bun add triiiceratops svelte

Your first viewer

<script lang="ts">
    import { TriiiceratopsViewer } from 'triiiceratops/svelte';
    // Import the design tokens + themes exactly once, anywhere in your app.
    import 'triiiceratops/style.css';
</script>

<!-- The container must have a height; the viewer fills it. -->
<div style="height: 600px;">
    <TriiiceratopsViewer manifestId="https://example.org/manifest.json" />
</div>

The stylesheet is an explicit import

Importing the component adds no global CSS side effects. Styling comes from the one import 'triiiceratops/style.css'. Every rule in that stylesheet is scoped to the viewer root, so it cannot restyle your host page.

Works in SvelteKit out of the box

Bundler-neutral (no import.meta.env reliance) and SSR-safe — core server-renders cleanly and lazily loads browser-only dependencies (OpenSeadragon), so it hydrates without mismatch warnings. You do not need export const ssr = false or a browser-only guard. Import the stylesheet once in your root +layout.svelte.

Reading and commanding state

Svelte needs no handle abstraction and no selector API. ViewerState is a rune class, so its members are already reactive: read them in your markup or in a $derived and Svelte tracks the dependency for you. There are two ways to get the instance.

bind:viewerState — when the state belongs to this component:

<script lang="ts">
    import { TriiiceratopsViewer, type ViewerState } from 'triiiceratops/svelte';
    import 'triiiceratops/style.css';

    let viewerState = $state<ViewerState | undefined>();

    // Reactive: recomputes when the viewer navigates.
    const canvasId = $derived(viewerState?.canvasId ?? 'No canvas yet');
</script>

<p>{canvasId}</p>
<button onclick={() => viewerState?.nextCanvas()}>Next canvas</button>

<div style="height: 600px;">
    <TriiiceratopsViewer
        bind:viewerState
        manifestId="https://example.org/manifest.json"
    />
</div>

getContext(VIEWER_STATE_KEY) — inside any descendant, with no prop threading:

<script lang="ts">
    import { getContext } from 'svelte';
    import { VIEWER_STATE_KEY, type ViewerState } from 'triiiceratops/svelte';

    const state = getContext<ViewerState>(VIEWER_STATE_KEY);
</script>

<span>{state.currentCanvasIndex + 1} / {state.sequenceCount}</span>

The component sets that context, so this is the same instance bind:viewerState exposes — the one the plugin SDK and every other host read. ViewerState is per-viewer: two viewers on a page are two independent instances.

undefined until the viewer mounts

bind:viewerState is populated when the component mounts its viewer, so the bound variable is undefined for the first tick. $derived plus ?., as above, is the whole pattern.

The framework-neutral triiiceratops/selectors runtime exists for hosts that need memoized, equality-gated projections. Svelte's reactivity already does that work, so you do not need it here.

Props

Prop Type
manifestId string
canvasId string
manifestJson Record<string, any>
theme BuiltInTheme
themeConfig ThemeConfig
config ViewerConfig
initialCanvasRegion CanvasRegion \| null
plugins readonly SdkPlugin[]
searchProvider SearchProvider \| null
viewerState ViewerState (bindable, read-only in practice)
onpluginerror (error: PluginError) => void
onviewererror (error: ViewerError) => void

Plain Svelte props — objects and functions pass straight through, so none of the attribute-versus-property care the custom element needs applies. manifestId and canvasId are uncontrolled: one-way instructions to the viewer, not enforced bindings. Re-asserting the current canvasId writes nothing, so your markup never fights a user who navigated internally; read viewerState to follow where the viewer actually is.

Errors

The two callback props receive the same structured objects the custom element dispatches as pluginerror and viewererror events:

<script lang="ts">
    import { TriiiceratopsViewer } from 'triiiceratops/svelte';
    import 'triiiceratops/style.css';
</script>

<TriiiceratopsViewer
    manifestId="https://example.org/manifest.json"
    onpluginerror={(error) => console.warn(error.pluginName, error.phase)}
    onviewererror={(error) => console.error(error.code, error.message)}
/>

PluginError carries a retry(), so a host can offer the user a second attempt rather than only reporting the failure.

Multiple manifests

manifestsState (and its ManifestsState class) is exported from this entry for apps that coordinate several manifests — a collection browser, say — outside any single viewer instance.

import { manifestsState } from 'triiiceratops/svelte';

Where to go next

Everything below is framework-neutral and has a Svelte tab on every example:

Verified against the packed package

CI runs packed-consumer fixtures across Chromium, Firefox, and WebKit: svelte-vite (the native component under Vite) and sveltekit-ssr (server-rendered, then hydrated, with no mismatch warnings). Every code sample on this site is type-checked against those same tarballs.