For developers
The viewer of Kasane is available as the package iiif-kasane, to put on your own site (MIT license). This page shows three ways to use it, each with a live viewer and its code: as React components, as the HTML tag <iiif-kasane>, and in Astro.
In each example, the tabs switch between the preview (a live viewer) and the code.
Install
The package is published on npm as iiif-kasane. Install three.js and the other 3D packages even if you show only images: your bundler resolves them, but the browser fetches them only when a 3D Scene is shown. The tag needs no install.
npm install iiif-kasane
npm install react react-dom jotai three three-stdlib @react-three/fiber @react-three/drei gsapReact: Viewer
The simplest way. Viewer fills its parent: give the parent a height and pass className="h-full". iiifContent is a manifest URL or a IIIF Content State.
import { Viewer } from 'iiif-kasane';
import 'iiif-kasane/style.css';
export default function Page() {
return (
<div style={{ height: '32rem' }}>
<Viewer
iiifContent="https://iiif.io/api/cookbook/recipe/0001-mvm-image/manifest.json"
locale="en"
className="h-full"
/>
</div>
);
}Other props include locale (ja / en), theme (light / dark / auto), target to show a given Canvas from outside, and onSelect to hear which Canvas is shown.
React: the parts in your own layout
Inside ViewerProvider, place the parts (CanvasView for the display, AnnotationList, Metadata, …) in any layout. The parts share one state: choosing an annotation in the list moves the view, and the metadata follows the page turns. Your own components read the same state with useViewer() (PageLabel in the example).
// The viewer's parts placed in a layout of your own. ViewerProvider loads the manifest and holds the
// shown Canvas; the parts inside it read the same state, so a click on an annotation in the list
// moves the 3D view, and the metadata follows the page turns.
import {
AnnotationList,
CanvasView,
Metadata,
ViewerProvider,
useViewer,
type ViewerSelection,
} from 'iiif-kasane';
import { labelIn } from 'iiif-kasane/core';
import 'iiif-kasane/style.css';
import { useState } from 'react';
// A part of your own: reads the provider's state with useViewer(). IIIF labels are language maps
// ({ en: [...], none: [...] }); labelIn picks one language as a string.
function PageLabel() {
const { container, selectedIndex, containers } = useViewer();
if (!container) return null;
return (
<p className="page-label">
{selectedIndex + 1} / {containers.length}: {labelIn(container.label, 'en') || container.id}
</p>
);
}
export default function FreeLayout({ manifest }: { manifest: string }) {
const [shown, setShown] = useState<ViewerSelection | null>(null);
return (
<>
{/* The provider draws one <div class="iiif-viewer">; className makes it your layout's container. */}
<ViewerProvider iiifContent={manifest} locale="en" theme="light" className="free-layout" onSelect={setShown}>
<section className="area-view">
<PageLabel />
<CanvasView className="view" />
</section>
{/* AnnotationList has its own heading, search and "show on model" switch. */}
<section className="area-annotations">
<AnnotationList />
</section>
<section className="area-metadata">
<h2>About</h2>
<Metadata />
</section>
</ViewerProvider>
<p>onSelect: {shown ? `${shown.index}, ${shown.canvasId}` : '(not yet)'}</p>
</>
);
}/*
* The layout is this page's own CSS. The package's style.css has only the classes its own parts
* use, and they apply only inside the provider, so write your layout with your own classes.
*/
.free-layout {
display: grid;
grid-template-columns: 2fr 1fr;
grid-template-rows: 22rem 14rem;
grid-template-areas: 'view annotations' 'view metadata';
gap: 0.75rem;
}
.free-layout section { display: flex; flex-direction: column; min-height: 0; border: 1px solid #ccc; }
.free-layout h2 { margin: 0; padding: 0.25rem 0.75rem; font-size: 1rem; background: #f3f4f6; }
.area-view { grid-area: view; }
.area-view .view { flex: 1; }
.area-annotations { grid-area: annotations; overflow-y: auto; }
.area-metadata { grid-area: metadata; overflow-y: auto; }
.page-label { margin: 0; padding: 0.25rem 0.75rem; font-size: 0.875rem; }
@media (max-width: 48rem) {
.free-layout {
grid-template-columns: 1fr;
grid-template-rows: 24rem auto auto;
grid-template-areas: 'view' 'annotations' 'metadata';
}
}Write the layout with your own CSS. The package’s style.css holds only the classes the parts use, and they apply only inside the viewer.
The tag: <iiif-kasane>
A page without any framework can use the viewer as a tag by loading one script; React is inside it. Set the height with CSS or --iiif-viewer-height, and the colors with the --iiif-viewer-* variables. Each version stays at its own URL, so you can pin one.
<script type="module" src="https://kasane.ldas.jp/element/v0.3.0/iiif-kasane.js"></script>
<iiif-kasane
iiif-content="https://iiif.io/api/cookbook/recipe/0001-mvm-image/manifest.json"
locale="en"
style="height: 32rem"
></iiif-kasane>Its attributes are iiif-content, locale, theme and options (JSON, e.g. {"panelOpen":false,"pager":false}). Changing an attribute updates the viewer.
The share menu of the viewer writes the embed code for a given item. About structure, sharing and embedding
Astro
Both ways work in Astro. tag.astro sends the tag as plain HTML, and the script turns it into a viewer in the browser (the preview shows this). parts.astro puts FreeLayout.tsx from “the parts in your own layout” on the page as a React island; the viewer reads the browser (WebGL, the window size), so it is rendered with client:only="react".
---
import Layout from '../components/Layout.astro';
const manifests = [
{ label: 'Globe (3D, with annotations)', url: 'https://kasane.ldas.jp/manifests/sample-manifest-with-annotations.json' },
{ label: 'Image (IIIF Cookbook 0001)', url: 'https://iiif.io/api/cookbook/recipe/0001-mvm-image/v4/manifest.json' },
];
---
<Layout title="The <iiif-kasane> tag">
<h1>The <iiif-viewer> tag</h1>
<p>
The tag needs no framework: Astro sends it as plain HTML, and the script below turns it into a
viewer in the browser. The controls change its attributes.
</p>
<form class="controls">
<label>Manifest
<select id="manifest">
{manifests.map((m) => <option value={m.url}>{m.label}</option>)}
</select>
</label>
<label>Theme
<select id="theme">
<option>auto</option>
<option>light</option>
<option>dark</option>
</select>
</label>
</form>
<iiif-kasane id="viewer" iiif-content={manifests[0].url} locale="en" theme="auto"></iiif-kasane>
<!-- On a real site: src="https://kasane.ldas.jp/element/v<version>/iiif-kasane.js".
The script loads style.css from its own directory. -->
<script is:inline type="module" src="/iiif-viewer/iiif-kasane.js"></script>
<script>
const viewer = document.getElementById('viewer')!;
const bind = (id: string, attribute: string) =>
document.getElementById(id)!.addEventListener('change', (e) =>
viewer.setAttribute(attribute, (e.target as HTMLSelectElement).value),
);
bind('manifest', 'iiif-content');
bind('theme', 'theme');
</script>
<style>
.controls { display: flex; gap: 1rem; margin-bottom: 0.75rem; }
/* The tag fills the height you give it (36rem by default). */
iiif-kasane { height: 32rem; border: 1px solid #ccc; }
</style>
</Layout>---
import Layout from '../components/Layout.astro';
import FreeLayout from '../components/FreeLayout';
---
<Layout title="React parts in your own layout">
<h1>React parts in your own layout</h1>
<p>
<code>ViewerProvider</code> with <code>CanvasView</code>, <code>AnnotationList</code> and
<code>Metadata</code> in a grid of this page. The viewer reads the browser (WebGL, the window
size), so the island is rendered on the client only.
</p>
<FreeLayout client:only="react" manifest="https://kasane.ldas.jp/manifests/sample-manifest-with-annotations.json" />
<style is:global>
/*
* The layout is this page's own CSS. The package's style.css has only the classes its own parts
* use, and they apply only inside the provider, so write your layout with your own classes.
*/
.free-layout {
display: grid;
grid-template-columns: 2fr 1fr;
grid-template-rows: 22rem 14rem;
grid-template-areas: 'view annotations' 'view metadata';
gap: 0.75rem;
}
.free-layout section { display: flex; flex-direction: column; min-height: 0; border: 1px solid #ccc; }
.free-layout h2 { margin: 0; padding: 0.25rem 0.75rem; font-size: 1rem; background: #f3f4f6; }
.area-view { grid-area: view; }
.area-view .view { flex: 1; }
.area-annotations { grid-area: annotations; overflow-y: auto; }
.area-metadata { grid-area: metadata; overflow-y: auto; }
.page-label { margin: 0; padding: 0.25rem 0.75rem; font-size: 0.875rem; }
@media (max-width: 48rem) {
.free-layout {
grid-template-columns: 1fr;
grid-template-rows: 24rem auto auto;
grid-template-areas: 'view' 'annotations' 'metadata';
}
}
</style>
</Layout>// The viewer's parts placed in a layout of your own. ViewerProvider loads the manifest and holds the
// shown Canvas; the parts inside it read the same state, so a click on an annotation in the list
// moves the 3D view, and the metadata follows the page turns.
import {
AnnotationList,
CanvasView,
Metadata,
ViewerProvider,
useViewer,
type ViewerSelection,
} from 'iiif-kasane';
import { labelIn } from 'iiif-kasane/core';
import 'iiif-kasane/style.css';
import { useState } from 'react';
// A part of your own: reads the provider's state with useViewer(). IIIF labels are language maps
// ({ en: [...], none: [...] }); labelIn picks one language as a string.
function PageLabel() {
const { container, selectedIndex, containers } = useViewer();
if (!container) return null;
return (
<p className="page-label">
{selectedIndex + 1} / {containers.length}: {labelIn(container.label, 'en') || container.id}
</p>
);
}
export default function FreeLayout({ manifest }: { manifest: string }) {
const [shown, setShown] = useState<ViewerSelection | null>(null);
return (
<>
{/* The provider draws one <div class="iiif-viewer">; className makes it your layout's container. */}
<ViewerProvider iiifContent={manifest} locale="en" theme="light" className="free-layout" onSelect={setShown}>
<section className="area-view">
<PageLabel />
<CanvasView className="view" />
</section>
{/* AnnotationList has its own heading, search and "show on model" switch. */}
<section className="area-annotations">
<AnnotationList />
</section>
<section className="area-metadata">
<h2>About</h2>
<Metadata />
</section>
</ViewerProvider>
<p>onSelect: {shown ? `${shown.index}, ${shown.canvasId}` : '(not yet)'}</p>
</>
);
}The whole example (how to run it is in its README): packages/iiif-viewer/examples/astro