Writing a plugin using jbrowse-plugin-template
JBrowse 2 plugins can be used to add new pluggable elements (views, tracks, adapters, etc.), and to modify behavior of the application by adding code that watches the application's state.
For the full list of what kinds of pluggable element types plugins can add, see the pluggable elements page.
The following tutorial will walk you through establishing your developer environment, spinning up a plugin, and running a local JBrowse instance with your custom plugin functionality.
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.
Use git to clone the plugin template
The easiest way to start developing your plugin for JBrowse 2 is to use the plugin template. There is also a lightweight alternative based on esbuild: jbrowse-plugin-esbuild-template.
To clone the plugin template project, on the command line run:
# 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
To initialize your 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.
:::note Tip
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
Finally, we're going to 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.
Open a new tab in your terminal and navigate again to your plugin project, then we're going to run our 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"
}
}
}
}
]
}
Take some time to dissect what's being added here:
- we're adding the assembly GRCh38
- it can be referenced either by its name (hg38) or its aliases (GRCh38)
- it has a sequence, which has a BgzipFastaAdapter from which the reference sequence is derived
- these FASTA's are hosted on jbrowse.org, referenced as a UriLocation
- there is also a refNameAliases text file being used to derive the reference names of the assembly
We're now going to add a track that will make use of our jexl function. As mentioned previously, you could add your jexl function programmatically to all tracks of this type, but for now we're just adding it to our assembly for this 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" }
}
]
}
]
Take some time to dissect what's being added here:
- this is a track that will appear in our track list when we run JBrowse against this assembly
- it's a VariantTrack called "demo_vcf"
- it derives its data from a given UriLocation, the file is a
.vcffile using the VcfAdapter - it declares its display, the
ChordVariantDisplay, and specifies itsonChordClickcallback function - the specified
onChordClickcallback function is that which we defined in our plugin class, the jexl function

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.

:::info Troubleshooting
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.
:::
Next steps
We have a complete and tested plugin, so now we're ready to publish it to NPM and request that it be added to the plugin store.
Sometimes you might write a plugin that is specific to you or your organization's needs, but you also might want to share it with the greater community. That's where the plugin store shows off its strengths.
As a plugin developer, you can publish your plugin to NPM, and then request that your plugin be added to the plugin store. After your plugin is successfully whitelisted, you will see it within the JBrowse app's plugin store widget and you and others can freely install the plugin into their JBrowse session. Any further publications you make to the plugin via NPM will automatically be updated for the plugin available through the plugin store.
The following document will describe how to accomplish this.
Publish your plugin to NPM
The following will guide you through publishing with NPM. You'll need an NPM account and token to do this, so please set that up first through the NPM site.
If you'd prefer not to publish to NPM, you can host your plugin files elsewhere, just ensure the link is accessible publicly.
When your plugin is in a publishable state, and you have NPM credentials, you
can run the following within your plugin's root directory (where package.json
is found):
yarn publish
Set the version to whatever you'd like, enter your credentials, and then complete the publication process. Once you can see your package on NPM, move on 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.

:::info Tip
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 development team will review your plugin to ensure that it is functional, then when it is merged in the plugin will be available on the plugin store.
In this tutorial, we set up a development environment for JBrowse 2 and added a custom pluggable element to a plugin.
We also published the plugin to NPM and requested that it be added to the JBrowse plugin store, so others can access our plugin.
To learn more about the various pluggable elements available in JBrowse (and thus more that you can do with plugins!) checkout our developer guide documentation.
If you have further questions about plugin development, or development with JBrowse in general, stop by the JBrowse team gitter channel, or start a discussion on the jbrowse-components discussions forum.