JBrowse 2 · Linear Genome View examples

Theming & styling

A custom or dark Material UI theme, styling from the host page, or Shadow DOM isolation.

Custom theme

The view takes a Material UI theme on its configuration block. Four named palette colors — primary, secondary, tertiary, quaternary — drive most of the chrome: toolbars, buttons, highlights. The DNA base colors in the sequence display are configured separately.

Full options are in the theming guide.

View source — 42 lines
import {
  JBrowseLinearGenomeView,
  useCreateViewState,
} from '@jbrowse/react-linear-genome-view2'

export default function WithCustomTheme() {
  const state = useCreateViewState({
    assembly: {
      name: 'volvox',
      uri: 'https://jbrowse.org/genomes/volvox/volvox.2bit',
    },
    tracks: [
      {
        type: 'FeatureTrack',
        trackId: 'volvox_gff3',
        name: 'Volvox genes',
        assemblyNames: ['volvox'],
        adapter: {
          type: 'Gff3TabixAdapter',
          uri: 'https://jbrowse.org/code/jb2/main/test_data/volvox/volvox.sort.gff3.gz',
        },
      },
    ],
    configuration: {
      theme: {
        palette: {
          primary: { main: '#311b92' },
          secondary: { main: '#0097a7' },
          tertiary: { main: '#f57c00' },
          quaternary: { main: '#d50000' },
          bases: {
            A: { main: '#98FB98' },
            C: { main: '#87CEEB' },
            G: { main: '#DAA520' },
            T: { main: '#DC143C' },
          },
        },
      },
    },
  })
  return <JBrowseLinearGenomeView viewState={state} />
}

Dark theme

configuration.theme.palette.mode: 'dark' renders in dark mode. JBrowse is tuned to look reasonable under both palettes.

To override individual colors, see the custom theme example.

View source — 35 lines
import {
  JBrowseLinearGenomeView,
  useCreateViewState,
} from '@jbrowse/react-linear-genome-view2'

export default function WithDarkTheme() {
  const state = useCreateViewState({
    assembly: {
      name: 'volvox',
      uri: 'https://jbrowse.org/genomes/volvox/volvox.2bit',
    },
    tracks: [
      {
        type: 'FeatureTrack',
        trackId: 'volvox_gff3',
        name: 'Volvox genes',
        assemblyNames: ['volvox'],
        adapter: {
          type: 'Gff3TabixAdapter',
          uri: 'https://jbrowse.org/code/jb2/main/test_data/volvox/volvox.sort.gff3.gz',
        },
      },
    ],
    configuration: {
      theme: {
        palette: {
          mode: 'dark',
          primary: { main: '#333' },
          secondary: { main: '#444' },
        },
      },
    },
  })
  return <JBrowseLinearGenomeView viewState={state} />
}

Styling from outside the component

The view inherits CSS from its host — there is no shadow-DOM isolation in the default rendering path, so wrapping it in a styled container just composes.

For the opposite, guaranteed isolation from a host page’s global styles, render it inside a Shadow DOM instead.

View source — 31 lines
import {
  JBrowseLinearGenomeView,
  useCreateViewState,
} from '@jbrowse/react-linear-genome-view2'

export default function WithOutsideStyling() {
  const state = useCreateViewState({
    assembly: {
      name: 'volvox',
      uri: 'https://jbrowse.org/genomes/volvox/volvox.2bit',
    },
    tracks: [
      {
        type: 'FeatureTrack',
        trackId: 'volvox_gff3',
        name: 'Volvox genes',
        assemblyNames: ['volvox'],
        adapter: {
          type: 'Gff3TabixAdapter',
          uri: 'https://jbrowse.org/code/jb2/main/test_data/volvox/volvox.sort.gff3.gz',
        },
      },
    ],
    location: 'ctgA:1105..1221',
  })
  return (
    <div style={{ textAlign: 'center', fontFamily: 'monospace' }}>
      <JBrowseLinearGenomeView viewState={state} />
    </div>
  )
}

Package as a custom element

Mounting the view inside a Shadow DOM and registering it with @r2wc/react-to-web-component turns it into one HTML tag a host page can drop in with no React of its own:

<jbrowse-linear-view></jbrowse-linear-view>

Shadow DOM is what makes that safe to hand out: styles can’t leak in from the host page or out into it, which matters in a CMS, a LIMS, or any page whose global CSS you don’t control.

The component needs no special configuration. The one thing you must do is point Material UI’s emotion cache and its portal containers (menus, tooltips, dialogs) at a target inside the shadow root — otherwise MUI appends them to document.body, outside the boundary, where the cache’s styles don’t reach and they render unstyled.

To compose with parent styles rather than isolate from them, see styling from outside.

View source — 107 lines
import { createElement, useEffect, useRef, useState } from 'react'

import createCache from '@emotion/cache'
import { CacheProvider } from '@emotion/react'
import {
  JBrowseLinearGenomeView,
  createViewState,
  destroyViewState,
} from '@jbrowse/react-linear-genome-view2'
import r2wc from '@r2wc/react-to-web-component'
import { createPortal } from 'react-dom'

import type { EmotionCache } from '@emotion/cache'
import type { ViewModel } from '@jbrowse/react-linear-genome-view2'

const ShadowComponent = () => {
  const nodeRef = useRef<HTMLDivElement>(null)
  const nodeForPinRef = useRef(null)
  const [rootNode, setRootNode] = useState<ShadowRoot>()
  const [cacheNode, setCacheNode] = useState<EmotionCache>()
  const [config, setConfig] = useState<ViewModel>()
  useEffect(() => {
    if (!nodeRef.current) {
      return
    }
    const root = nodeRef.current.attachShadow({ mode: 'open' })
    setRootNode(root)
    setCacheNode(
      createCache({ key: 'react-shadow', prepend: true, container: root }),
    )
    const engine = createViewState({
      assembly: {
        name: 'volvox',
        uri: 'https://jbrowse.org/genomes/volvox/volvox.2bit',
      },
      tracks: [
        {
          type: 'FeatureTrack',
          trackId: 'volvox_gff3',
          name: 'Volvox genes',
          assemblyNames: ['volvox'],
          adapter: {
            type: 'Gff3TabixAdapter',
            uri: 'https://jbrowse.org/code/jb2/main/test_data/volvox/volvox.sort.gff3.gz',
          },
        },
      ],
      location: 'ctgA:1105..1221',
      configuration: {
        theme: {
          palette: { primary: { main: '#4400a6' } },
          components: {
            MuiPopover: {
              defaultProps: { container: () => nodeForPinRef.current },
            },
            MuiPopper: {
              defaultProps: { container: () => nodeForPinRef.current },
            },
            MuiTooltip: {
              defaultProps: {
                slotProps: {
                  popper: { container: () => nodeForPinRef.current },
                },
              },
            },
            MuiModal: {
              defaultProps: { container: () => nodeForPinRef.current },
            },
            MuiMenu: {
              defaultProps: { container: () => nodeForPinRef.current },
            },
          },
        },
      },
    })
    // eslint-disable-next-line @eslint-react/set-state-in-effect -- shadow DOM setup requires setState in effect
    setConfig(engine)
    // the engine is not owned by React: unmounting the custom element leaves
    // its RPC worker threads and its autoruns running unless it is torn down
    return () => {
      destroyViewState(engine)
    }
  }, [])
  return (
    <div ref={nodeRef}>
      {rootNode && config && cacheNode
        ? createPortal(
            <CacheProvider value={cacheNode}>
              <JBrowseLinearGenomeView viewState={config} />
              <div ref={nodeForPinRef} />
            </CacheProvider>,
            rootNode,
          )
        : null}
    </div>
  )
}

const JBrowseCustom = () => createElement(ShadowComponent, null, null)

export default function ShadowDOMOneLinearGenomeView() {
  if (customElements.get('jbrowse-linear-view') === undefined) {
    customElements.define('jbrowse-linear-view', r2wc(JBrowseCustom))
  }
  // @ts-expect-error
  return <jbrowse-linear-view />
}