Repository navigation
Plugin Development Guide
Before you start developing your own Mirador 3 plugin, you should be familiar with a few technologies that are essential for Mirador 3 plugin development:
-
Familarity with a JavaScript build tool: This can be something like Webpack, Parcel, Snowpack, etc,
as long as it supports loading ES6 modules. This is needed, since Mirador 3 plugins cannot be loaded
from a
<script>tag at the moment, but have to be part of the same bundle as Mirador 3. If you don't have a favorite tool yet, Parcel is very easy to use. - React: This should be obvious, Mirador 3 is a React app and plugin development is based on adding custom React components to the app. Essential resources are the React Tutorial and the section on Higher Order Components
- Redux and React-Redux: The state in Mirador 3 is completely handled by the Redux library. Plugins that want to access or mutate the app state, or need their own global state, must make use of it. Essential resources are the Redux Essentials and the Redux Fundamentals. It's also a good idea to read through the Selector Intro for an introduction to selector functions, which are heavily used in M3.
- Redux-Saga: Side-effects in Mirador 3 are handled with this library. It's also a highly useful tool for reacting to Redux actions, which can come in extremely handy when developing plugins (e.g. if you want your plugin to react to a page change). Essential resources are the Redux-Saga Introduction and Redux-Saga Basic Concepts.
You can learn all three of those technologies "as you go", but I recommend to work at least through the basic introductions beforehand.
A Mirador 3 plugin is a React Component that targets plugin-aware React Components within Mirador. Such a plugin component can attach itself to a plugin-aware Mirador 3 component in two distinct ways:
-
add: The target component includes a slot where the plugin component will be added as part of the target components' virtual DOM tree. This is used to extend existing components with new widgets, e.g. a button in the window title bar or a new entry in the ribbon menu. -
wrap: The plugin components wraps the target component and (optionally!) renders the target component as a child of its own virtual DOM tree. This allows the complete substitution of every plugin-aware React Component inside of Mirador 3. The wrapped component can be accessed via theTargetComponentprop that gets passed to the wrapping plugin component, this can be used to render it in the plugin's JSX via.<props.TargetComponent ...props>. This is useful if you want to e.g. add the option to toggle the visibility of a given component or you want to wrap the existing component with custom markup.
To define a plugin, create a JavaScript object with the following keys:
{
// The plugin component that should be added
component: MyPluginComponent,
// The name of the plugin-aware Mirador 3 component that this plugin targets
target: 'SomeMiradorComponent',
// Can be 'add' or 'wrap', defines how the plugin component is rendered
mode: 'add',
// Refer to the Redux documentation for these two functions. The props that are
// passed are those of the target component
mapDispatchToProps: (dispatch, props) => { /* ... */ },
mapStateToProps: (state, props) => { /* ... */ },
// Define new sub-stores along with their respective reducers
reducers: { /* ... */ },
// Define a custom saga that should be run as part of the Mirador 3 root saga
saga: myCustomSaga,
}Then, to tell Mirador 3 to load the plugin, pass an array of all plugin definitions as the
second argument to the Mirador.viewer initialization function:
// This assumes that the above plugin definition is exported from a `myPlugin.js` file in the
// same directory.
import myPluginDefinition from './myPlugin';
const myMiradorInstance = Mirador.viewer({ /* mirador config ... */ }, [myPluginDefinition]);To check if a given component supports add plugins, search for uses of the PluginHook component
inside of src/components modules in the Mirador 3 source code (or use this GitHub Link).
The position of the <PluginHook /> definition will be the position where the plugin component is rendered.
For components that support wrap plugins, search for uses of the withPlugins HOC inside of the
src/containers modules, (or use this GitHub Link). All of the container
components that are wrapped with this HOC can be wrapped by a plugin component.
If you want to get access to the current application state, e.g. to tell what the currently displayed
manifest is, you will have to access Mirador 3's global application state, which is implemented as a
Redux store. You cannot access it directly, but will instead have to supply a mapStateToProps object
in your plugin definition that maps your plugin component's props to a selector function that pulls
a value from the Redux store and passes it as a prop.
The tl;dr of a selector function is that it's a function that thakes the Redux State or one or more other selectors and extracts and returns some data from that. Think of them like a stored procedure in a DBMS, targetting your Redux state.
Mirador 3 ships with a ton of selector functions that should satisfy most of your needs as a plugin author,
you can find them in the src/state/selectors directory. Unfortunately they're barely
documented at the moment, but grepping for them in the code base and unit tests should give you a good idea
of how to use them. Maybe you can even take the time to make a documentation PR when you've understood a
selector? :-)
To illustrate, here's a small plugin definition that receives the identifier of the manifest that is currently
rendered in its associated window. It targets the OpenSeadragonViewer component, which receives a prop
windowId that has the identifier of the window that renders the current instance of the component.
With this, we can use the getWindow selector to get the state of the current window, which has a property
manifestId with the manifest that is displayed.
import { getWindow } from 'mirador/selectors';
const myPlugin = {
component: MyPluginComponent,
target: 'OpenSeadragonViewer',
mode: 'add',
// The function receives the Redux state and the target component's props
mapStateToProps: (state, { windowId }) => ({
// The plugin component will receive a `manifestId` prop with the identifier
// of the window's current manifest
manifestId: getWindow(state, { windowId }).manifestId,
})
}The hard part of the above is finding out which part of the Redux state you actually need and which of the pre-existing selectors you can use to get that. For the former, it's a good idea to have the Redux DevTools installed, they allow you to browse through the Redux state and check where the information you need is located. From there, you can browse through the available selectors and check if there is one that extracts it already. Generally the selector modules are structured like this:
- Selectors for a few common top level sub-stores are located in the
gettersmodule - Selectors for specific sub-stores and their children are usually in a module that's named after
the sub-store, e.g. the
companionWindowssub-store has acompanionWindows.jsselector module that has selector functions to access its values
If you want to trigger some change in the application, you have to change the corresponding value in the Redux store. As usual, this cannot be done directly, but only through actions that are triggered by action creator functions. Much like selectors, Mirador ships with creator functions for every possible action, and we can use them in our plugins.
To use action creator functions from Mirador 3, map them to your plugin component's props with the
mapDispatchToProps field in the plugin definition. As an example, here's a custom pagination plugin
that uses the setNextCanvas and setPreviousCanvas action creator functions to page
through a manifest's canvases (maybe the plugin adds a slideshow mode?)
import { setNextCanvas, setPreviousCanvas } from 'mirador/actions';
const myPlugin = {
component: MyPluginComponent,
target: 'OpenSeadragonViewer',
mode: 'add',
// The function receives the dispatch function and the target component's props
mapDispatchToProps: (dispatch, { windowId }) => ({
// The props are functions that create the action with the action creator function and
// dispatch it to the Redux middleware, i.e. you can ju st call props.setNextCanvas()
// in the component
setNextCanvas: () => dispatch(setNextCanvas(windowId)),
setPreviousCanvas: () => dispatch(setPreviousCanvas(windowId)),
})
}TODO
This is a minimal plugin that simply adds a small button to the window bar that simply prints
Hello World from the window title bar! to the console.

Step 1: Locating the target component
First, we need to find out which component we should target for rendering the button. For this, it's very useful to run Mirador 3 locally (i.e. without source minification) and use the React developer tools in the browser to find the corresponding React component.

The component tree tells us that the component that renders the window title bar that we want
to add our button to is the WindowTopBar. But does it have a <PluginHook /> for rendering
add plugins? Grepping for it doesn't yield anything, but there are two other nodes in the
virtual DOM that relate to plugins: <WindowTopBarPluginArea /> and <WindowTopBarPluginMenu />!
The latter renders its children as a drop-down menu that is accessible via a "ribbon menu", while
the former directly adds its children to the window top bar, just what we need!
Step 2: Writing the plugin component
Looking at the source code of the WindowTopBar component, we can see that the buttons that
are already present in the top bar all make use of the MiradorMenuButton component to render.
So it's probably a good idea to use it in our plugin as well! It has just a few simple props,
of which we are only going to use onClick and the support for child elements:
// This should be in a `MyPlugin.jsx` file
// Mirador 3 uses the `material-ui` React UI framework, so we can use it as a source for icons
import FlashOnIcon from '@material-ui/icons/FlashOn';
export default () => (
<MiradorMenuButton
onClick={() => console.log('Hello World from the window title bar!')}
>
<FlashOnIcon />
</MiradorMenuButton>
);Step 3: Defining and registering the plugin
Now that we know our target component (WindowTopBarPluginArea) and we have our plugin
component in MyPlugin.jsx, all that's left is to write a small plugin definition and
tell Mirador 3 to load it!
import MyPlugin from './MyPlugin';
const myPlugin = {
component: MyPlugin,
target: 'WindowTopBarPluginArea',
mode: 'add'
}
const mirador = Mirador.viewer({ /* cfg goes here */ }, [myPlugin]);For this example, we'll replace Mirador's default branding with our own, a small flower icon:

The basic approach is the same as for the add case:.
- Digging around in the component tree shows that we need to replace the
Brandingcomponent - For our custom branding, we use the original component as a blueprint and simply replace the icon:
// MyBranding.jsx import React from 'react'; import IconButton from '@material-ui/core/IconButton'; import Typography from '@material-ui/core/Typography'; import LocalFloristIcon from '@material-ui/icons/LocalFlorist'; // The wrapping plugin components gets the same props as the component to be wrapped export default function MyBranding({ variant, ...ContainerProps }) { return ( <div {...ContainerProps}> { variant === 'wide' && ( <div> <Typography align="center" component="p" variant="h3">{t('mirador')}</Typography> </div> )} <Typography align="center"> <IconButton component="a" href="https://example.com" target="_blank" rel="noopener" > <LocalFloristIcon /> </IconButton> </Typography> </div> ); }
- For registering the plugin, we only need to swap out the components and the mode:
import Mirador from 'mirador' import MyBranding from './MyBranding' const myPlugin = { component: MyBranding, target: 'Branding', mode: 'wrap' }; const mirador = Mirador.viewer({ /* cfg goes here */ }, [myPlugin]);
For this exercise, we're going to implement a plugin that adds two new buttons to the canvas navigation, one to seek five canvases backwards and one to seek five canvases forward, to allow users to quickly skip through a manifest without having to use the thumbnail bar.
TODO: Add image of final plugin here
For this, we first locate the component responsible for rendering the existing forward/backward
buttons, namely ViewerNavigation. A quick look at the component's JSX tells us it does not
include a <PluginHook />, so we can't use the add mode for our plugin but have to wrap
the target component instead and supply our own replacement for it.
Since we intend to keep the existing forward/backward buttons, we'll start by simply copying
the existing ViewerNavigation component (not the container!) to e.g. ExtendedViewerNavigation.
The existing code uses relative imports to the Mirador code base, which we will have to convert to
absolute imports that target mirador/dist/es, so e.g. ../containers/MiradorMenuButton becomes
mirador/dist/es/src/containers/MiradorMenuButton.
Once this is out of the way, we can start writing our plugin definition and check if everything still looks as it did before, with the difference that the forward/backward buttons are now rendered by our plugin component:
import ExtendedViewerNavigation from './ExtendedViewerNavigation';
const extendedNavigation = {
component: ExtendedViewerNavigation,
target: 'ViewerNavigation',
mode: 'wrap'
}
const mirador = Mirador.viewer({ /* cfg goes here */ }, [extendedNavigation]);TODO: Image of canvas navigation plus component tree that shows the ExtendedViewerNavigation
Success! Now on to the heart of the plugin, the two new buttons.
- Plugin to skip 5 canvases forward/backwards
- Extends
ViewerNavigationcomponent with awrap-style plugin that adds two new buttons - Implements new selector
seekCanvasGroupings(amount: number)to seek forward/backward, logic very similar to existinggetNextCanvasGroupingaction creator - Uses
setCanvasaction creator - Uses
getCanvasGroupingsandgetCurrentCanvasselectors - Concepts/Patterns that should be introduced and explained:
- Finding and understanding an existing component and its state interactions, i.e. the action creators and selectors it uses
- Writing a custom selector that has dependencies
- Using
mapStateToPropsandmapDispatchToPropsfor plugins
- 'Slideshow' plugin that wraps over if the last page is reached and stops when the user changes the canvas manually
- Hardcoded interval, single button to start/stop in topbar
- Uses
setCanvas,setNextCanvasactions - Uses
getNextCanvasGroupingselector - Uses a saga to continuously advance the canvases after the interval
- If
SET_CANVASis called during this time, abort the slideshow - Concepts/Patterns that should be introduced and explained:
- Implementing a custom redux store state variable and actions and selectors to query/update it
- Concurrency with sagas:
take,put,delay,race - Reacting to M3 events with sagas