Writing a plugin using jbrowse-plugin-template
Plugins add new pluggable elements (views, tracks, adapters, etc.) and can modify application behavior by watching state. See the pluggable elements page for the full list of element types.
This tutorial walks through setting up a plugin and running a local JBrowse instance with it.
Prerequisites
- git
- A stable and recent version of node
- yarn or npm
- basic familiarity with the command line, React, package management, and npm
First we're going to install and set up the project for development.
Choose a plugin template
Two official templates are available:
| Template | Bundler | Package manager | Testing | | ------------------------------------------------------------------------------------------ | ------- | --------------- | ---------------------- | | jbrowse-plugin-template | rollup | yarn/npm | Jest | | jbrowse-plugin-esbuild-template | esbuild | pnpm | vitest + Puppeteer E2E |
The esbuild template has faster builds and includes end-to-end tests against JBrowse nightly builds. The rollup template is older and more widely referenced in existing examples. This tutorial uses the rollup template; the esbuild template follows the same plugin structure.
Clone the rollup template:
# change jbrowse-plugin-my-project to whatever you wish
git clone https://github.com/GMOD/jbrowse-plugin-template.git jbrowse-plugin-my-project
cd jbrowse-plugin-my-project
Initialize the project
Run:
yarn init
You'll be asked a few questions relating to your new project.
Most fields can be left blank, but make sure to enter a descriptive name for your plugin in the first field.
A typical naming convention for JBrowse plugins is "jbrowse-plugin-", or, if you are going to publish to an NPM organization, we advise "@myscope/jbrowse-plugin-".
You also need to install the dependencies:
yarn # or npm i
Setup JBrowse 2
Run:
yarn setup
which will grab the latest release version of JBrowse 2 (in the .jbrowse
directory) and make it easy for you to run within your plugin project.
To run JBrowse:
yarn browse
You should see something like the following:
yarn run v1.22.10
$ npm-run-all jbrowse:*
$ shx cp jbrowse_config.json .jbrowse/config.json
$ cross-var serve --listen $npm_package_config_browse_port .jbrowse
We still need to run the plugin though; we need both to be running to test our plugin.
In a new terminal tab, start the plugin:
cd jbrowse-plugin-my-project
yarn start
Now you can navigate to http://localhost:8999/, and see your running JBrowse instance!

At this point, you must be running your plugin on port 9000 to see a running
JBrowse instance, otherwise you will meet a screen asking you to configure your
instance.
If you'd like to change this port, you can edit the "port" fields under "config"
in the package.json file.
We can verify our plugin has been added to our JBrowse session by clicking the
first square on the splash screen "Empty," and then navigating Add ->
Hello View in the menu bar. This is the example pluggable element that is
added in the template plugin project.
For this tutorial, we're going to be creating a custom widget, and using a Jexl callback to open it when we click a chord on the circular genome view.

Add new files, stubs, and install dependencies
Add a new directory under src called CircularViewChordWidget with two files
CircularViewChordWidget.tsx, and index.tsx.
This component is essentially just a React component we're going to embed in a JBrowse widget.
A widget's index.tsx
The index file is going to export what our pluginManager needs to recognize
the widget: a ReactComponent, a configSchema, and a stateModelFactory.
CircularViewChordWidget/index.tsx
import { ConfigurationSchema } from '@jbrowse/core/configuration'
import PluginManager from '@jbrowse/core/PluginManager'
import { ElementId } from '@jbrowse/core/util/types/mst'
import { types } from '@jbrowse/mobx-state-tree'
export { default as ReactComponent } from './CircularViewChordWidget'
export const configSchema = ConfigurationSchema('CircularViewChordWidget', {})
export function stateModelFactory(pluginManager: PluginManager) {
const stateModel = types
.model('CircularViewChordWidget', {
id: ElementId,
type: types.literal('CircularViewChordWidget'),
featureData: types.frozen({}),
})
.actions(self => ({
setFeatureData(data: any) {
self.featureData = data
},
clearFeatureData() {
self.featureData = {}
},
}))
return stateModel
}
With @jbrowse/mobx-state-tree, we're defining
the properties of our widget and the actions (mutations) it can take. You can
add any model properties you need — they're accessible in your React component
via model.
If you have a particularly complex model, consider moving it into a separate
model.ts and exporting it from index.ts, similar to how the ReactComponent
is exported.
A widget's ReactComponent
Now that we have our model set up, let's build a simple widget that will open when we click the circular genome view chord.
CircularViewChordWidget.tsx
import { observer } from 'mobx-react'
import {
FeatureDetails,
BaseCard,
} from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail'
const CircularViewChordWidget = observer(({ model }: { model: any }) => {
const { featureData } = model
return (
<div>
<BaseCard title={featureData.name}>
<FeatureDetails feature={featureData} model={model} />
</BaseCard>
</div>
)
})
export default CircularViewChordWidget
The observer wrapper from mobx-react ensures the component re-renders when
model properties change. @jbrowse/core exports reusable components like
FeatureDetails and BaseCard — if you find something in the app you'd like to
reuse, check whether it's exported, and if not
make a request.
Now that we have our component built, we can install it into our plugin and test it out.
Register pluggable elements with JBrowse
The file src/index.ts exports your plugin and installs all the necessary
components to JBrowse at runtime such that it runs properly.
Your src/index.ts file is going to look something like the following right
now:
import Plugin from '@jbrowse/core/Plugin'
import PluginManager from '@jbrowse/core/PluginManager'
import { ViewType } from '@jbrowse/core/pluggableElementTypes'
import { SessionWithWidgets, isAbstractMenuManager } from '@jbrowse/core/util'
import { version } from '../package.json'
import {
ReactComponent as HelloViewReactComponent,
stateModel as helloViewStateModel,
} from './HelloView'
export default class SomeNewPluginPlugin extends Plugin {
name = 'SomeNewPluginPlugin'
version = version
install(pluginManager: PluginManager) {
pluginManager.addViewType(() => {
return new ViewType({
name: 'HelloView',
stateModel: helloViewStateModel,
ReactComponent: HelloViewReactComponent,
})
})
}
configure(pluginManager: PluginManager) {
if (isAbstractMenuManager(pluginManager.rootModel)) {
pluginManager.rootModel.appendToMenu('Add', {
label: 'Hello View',
onClick: (session: SessionWithWidgets) => {
session.addView('HelloView', {})
},
})
}
}
}
You'll notice we're already adding a new view type and configuring the rootModel in the template's project. We can use these patterns to add our widget.
src/index.ts
// imports
// ...
import { ViewType, WidgetType } from '@jbrowse/core/pluggableElementTypes'
// notice we're importing the components we exported from src/CircularViewChordWidget/index.ts
import {
configSchema as circularViewChordWidgetConfigSchema,
stateModelFactory as circularViewChordWidgetStateModelFactory,
ReactComponent as CircularViewChordWidgetComponent
} from './CircularViewChordWidget'
// ...
install(pluginManager: PluginManager) {
// ...
pluginManager.addWidgetType(() => {
return new WidgetType({
name: 'CircularViewChordWidget',
heading: 'Chord Details',
configSchema: circularViewChordWidgetConfigSchema,
stateModel: circularViewChordWidgetStateModelFactory(pluginManager),
ReactComponent: CircularViewChordWidgetComponent,
})
})
}
// ...
This is also where we'll add our Jexl callback function:
src/index.ts
// ...
import { getSession } from '@jbrowse/core/util'
// ...
// Jexl callback functions are adding inside configure in the plugin class
configure(pluginManager: PluginManager) {
// ...
/* .jexl.addFunction is the method to add a function
the first parameter is the name of your jexl function, and how you'll
call it
the second parameter is the supplementary properties the function
needs, here, we need these three properties for
the circular view's chord click function */
pluginManager.jexl.addFunction(
'openWidgetOnChordClick',
(feature: any, chordTrack: any) => {
// the session contains a ton of necessary information about the
// present state of the app, here we use it to call the
// showWidget function to show our widget upon chord click
const session = getSession(chordTrack)
if (session) {
// @ts-expect-error
session.showWidget(
// @ts-expect-error
session.addWidget(
'CircularViewChordWidget',
'circularViewChordWidget',
{ featureData: feature.toJSON() },
),
)
session.setSelection(feature)
}
},
)
}
// ...
Now that we've configured the jexl function to our JBrowse session, we can use it essentially anywhere.
While we could programmatically tell certain displays to use this jexl function when they perform an action, for our use case (clicking a chord on the circular view), we can simply write it into our config file.
Setup the configuration for proper testing
To open a view in JBrowse, we need an assembly configured, append the following
to your jbrowse_config.json file (i.e. after the "plugins" field):
{
"assemblies": [
{
"name": "hg38",
"aliases": ["GRCh38"],
"sequence": {
"type": "ReferenceSequenceTrack",
"trackId": "P6R5xbRqRr",
"adapter": {
"type": "BgzipFastaAdapter",
"fastaLocation": {
"uri": "https://jbrowse.org/genomes/GRCh38/fasta/hg38.prefix.fa.gz",
"locationType": "UriLocation"
},
"faiLocation": {
"uri": "https://jbrowse.org/genomes/GRCh38/fasta/hg38.prefix.fa.gz.fai",
"locationType": "UriLocation"
},
"gziLocation": {
"uri": "https://jbrowse.org/genomes/GRCh38/fasta/hg38.prefix.fa.gz.gzi",
"locationType": "UriLocation"
}
}
},
"refNameAliases": {
"adapter": {
"type": "RefNameAliasAdapter",
"location": {
"uri": "https://jbrowse.org/genomes/GRCh38/hg38_aliases.txt",
"locationType": "UriLocation"
}
}
}
}
]
}
Now add a track that uses the jexl function. You could add it programmatically to all tracks of this type, but here we configure it on a specific track:
"tracks": [
{
"type": "VariantTrack",
"trackId": "demo_vcf",
"name": "demo_vcf",
"assemblyNames": ["hg38"],
"category": ["Annotation"],
"adapter": {
"type": "VcfAdapter",
"vcfLocation": {
"locationType": "UriLocation",
"uri": "https://s3.amazonaws.com/jbrowse.org/genomes/hg19/skbr3/reads_lr_skbr3.fa_ngmlr-0.2.3_mapped.bam.sniffles1kb_auto_l8_s5_noalt.new.vcf"
}
},
"displays": [
{
"type": "ChordVariantDisplay",
"displayId": "demo_ch_v_disp",
"onChordClick": "jexl:openWidgetOnChordClick(feature, track, pluginManager)",
"renderer": { "type": "StructuralVariantChordRenderer" }
}
]
}
]
The key part is onChordClick on the ChordVariantDisplay, which calls the
jexl function we registered in the plugin.

Testing it out
Run JBrowse with your new plugin and manually test
Everything is in place to test this widget we've added to the plugin project out.
If you shut your instance down, restart JBrowse and your plugin (yarn browse
and yarn start).
Navigate to localhost:8999/?config=localhost:9000/jbrowse_config.json or equivalent to see JBrowse running with your config.
Now navigate:
- Click
Start a new session→Empty - In the top menu bar, click
Add→Circular view - When the view is open, click
Openbeside the assembly - Click the far right three rows of rectangles icon in the top left of the
circular view,
Open track selector - Select the track we populated from our config
- Click any chord in the circular view
Expected result:
The widget opens on the right-hand side with two panels, one with our editable widget byline, and one with our feature data.

If you get to this point and note that nothing happens, open the developer tools in your browser and investigate the console errors. Also check your running process in your terminal for any errors. Review the code you added to ensure you didn't miss any imports or statements. Check over your config file to ensure that "plugins", "assemblies", and "tracks" are all present for the configuration to work properly.
Publish your plugin
Once the plugin is ready, publish it to NPM and submit it to the plugin store so others can install it from within JBrowse. Future NPM releases are picked up automatically by the store.
Publish your plugin to NPM
You'll need an NPM account. If you'd prefer not to use NPM, host the plugin files publicly elsewhere. From the plugin's root directory:
yarn publish
Once the package appears on NPM, proceed to the next step.
Request your plugin be added to the plugin store
To populate your plugin to the plugin store, it must be added to the plugin list, a whitelist of JBrowse plugins.
Navigate to the plugin list repository and use the GitHub UI to Fork the repository.

It's easy enough to edit the files required using the GitHub UI, but feel free to clone and push to the forked repo using your local environment as well.
Optional: create an image for your plugin
An image helps communicate the capabilities of your plugin to adopters at a
glance. Consider creating an 800 x 200 .png screenshot of a core feature of
your plugin to show off.
We recommend using pngquant to compress your image to keep the repo manageable.
Once your image is all set, you can upload it to your forked repo (ideally in ~/jbrowse-plugin-list/img/) using the Github UI or pushing the file from your computer.
Adding the details for your plugin to the list
Once forked, you can edit the plugins.json file to include the following
information regarding your new plugin:
plugins.json
{
"plugins": [
// ...other plugins already published,
{
// this plugin name needs to match what is in your package.json
"name": "SomeNewPlugin",
"authors": ["You, dear reader!"],
"description": "JBrowse 2 plugin that demonstrates adding a simple pluggable element",
// change this to your github repo for your plugin
"location": "https://github.com/ghost/jbrowse-plugin-some-new-plugin",
// assuming you published to NPM, this url is going to be mostly the same, other than the correct name of your project
"url": "https://unpkg.com/jbrowse-plugin-some-new-plugin/dist/jbrowse-plugin-some-new-plugin.umd.production.min.js",
// make sure the license is accurate, otherwise use "NONE"
"license": "MIT",
// the image url will be wherever you placed it in the repo earlier, img is appropriate
"image": "https://raw.githubusercontent.com/GMOD/jbrowse-plugin-list/main/img/plugin-screenshot-fs8.png"
}
]
}
Push your changes to the main branch of your forked repo when you're done.
Make a pull request
Now that your plugin's information is accurate, navigate again to the plugin list repository, and create a new pull request.
In the pull request UI, click "compare across forks" and select your fork as the
head repository to merge into the main of jbrowse-plugin-list. Your changes
should show in the editor, and you can create your PR.

Next steps
The JBrowse team will review your PR and merge it when the plugin is functional.
For more on pluggable element types, see the developer guide. For questions, use the Gitter channel or GitHub discussions.