Embedding genome browsers
Put a genome browser in a teaching page, open at the gene you are teaching, showing the tracks you choose. igv.js suits most pages. Use JBrowse 2 when students need richer detail, and link out to UCSC and Ensembl for everything else.
Which browser to use
| Feature | igv.js | JBrowse 2 linear genome view | Links to UCSC or Ensembl |
|---|---|---|---|
| What it is | The web version of IGV, as one script | JBrowse’s track view, as one script | The full browsers, opened in a new tab |
| Download | 1.5 MB | 5.7 MB (1.4 MB compressed) | Nothing |
| Best for | Most teaching pages, and exercises that react to what students click | Rich detail, such as every field of a VCF record in a table | Every track and tool the big browsers offer |
| Limits | Its toolbar needs about 750 pixels of width | Heavier, and no simple events to build exercises from | Neither site can be shown inside your page |
| Licence | MIT | Apache 2.0 (the view package is labelled MIT on npm) | Free to link to |
The examples below use the human β-globin gene, HBB, and the sickle-cell variant rs334, so that you can compare the tools on the same view. All of them were tested on 15 October 2026, with igv.js 3.8.9 and JBrowse 2 version 4.3.0.
Start with a basic browser
One script tag and one call to igv.createBrowser give you a working browser [1]. The second argument says which genome to use and where to start.
This example downloads igv.js and the hg38 genome, about 10 MB, when it starts.
Code igv-basic.html
<!doctype html>
<html lang="en-GB">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>igv.js: a genome browser open at HBB</title>
<!-- igv.js 3.8.9 from jsDelivr. The integrity hash stops a changed file from running. -->
<script src="https://cdn.jsdelivr.net/npm/igv@3.8.9/dist/igv.min.js"
integrity="sha384-J2SJJANecDBpqn++zIO/AJY/Fpk64lYDp43ibq7bmQONxd9b80ZpvLKBrKdCtYIu"
crossorigin="anonymous"></script>
<style>
body { font: 15px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; margin: 0; padding: 12px 16px 16px; color: #172033; background: #fff; }
h1 { font-size: 1.05rem; margin: 0 0 8px; }
</style>
</head>
<body>
<h1>The human β-globin gene (HBB) on GRCh38</h1>
<div id="igv"></div>
<p id="status" role="status" aria-live="polite"></p>
<script>
const say = text => { document.getElementById("status").textContent = text; };
// The element with id="igv" also makes a global called igv, so test for the function itself
if (typeof window.igv?.createBrowser !== "function") {
say("igv.js did not load. Check the connection and reload the page.");
} else {
igv.createBrowser(document.getElementById("igv"), {
genome: "hg38", // GRCh38 as hosted by the IGV team: sequence, chromosome bands and RefSeq genes
locus: "HBB" // a gene name, or coordinates such as "chr11:5,225,464-5,229,395"
}).catch(err => say("The genome could not be loaded (" + err.message + "). Check the connection, or try again later."));
}
</script>
</body>
</html>genome: "hg38" asks the IGV team’s servers for the GRCh38 sequence, chromosome bands and RefSeq genes. That is convenient, but the first view downloads about 10 MB, 8 MB of it from igv.org, and the page stops working if that service is down. For a class, define the genome yourself from UCSC’s files, as the next example does. Nothing then depends on IGV’s list of genomes, and the reference costs far less: the activity below uses it, and downloads about 0.7 MB in all, igv.js included.
Choose the tracks, add buttons, mark the variant
Most teaching pages need a starting view, a few tracks that make the point, and buttons that take students to particular places. This example adds all three, plus a shaded region on the sickle-cell base.
This example downloads igv.js and its tracks, about 3 to 4 MB, when it starts.
The tracks come from UCSC’s public files. The ENCODE and 1000 Genomes tracks load only when asked, because the 1000 Genomes panel takes 15 to 20 seconds to draw 3,202 people.
Code igv-tracks.html
<!doctype html>
<html lang="en-GB">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>igv.js: public tracks, navigation buttons and a highlighted variant</title>
<script src="https://cdn.jsdelivr.net/npm/igv@3.8.9/dist/igv.min.js"
integrity="sha384-J2SJJANecDBpqn++zIO/AJY/Fpk64lYDp43ibq7bmQONxd9b80ZpvLKBrKdCtYIu"
crossorigin="anonymous"></script>
<style>
body { font: 15px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; margin: 0; padding: 12px 16px 16px; color: #172033; background: #fff; }
h1 { font-size: 1.05rem; margin: 0 0 8px; }
.controls { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 0 0 8px; }
button, input { font: inherit; font-size: 14px; padding: 4px 9px; }
form { display: inline-flex; gap: 6px; align-items: center; }
#status { font: 13px/1.4 ui-monospace, Menlo, Consolas, monospace; color: #3b4659; margin: 0 0 6px; }
</style>
</head>
<body>
<h1>HBB with transcripts, ClinVar and conservation</h1>
<div class="controls" role="group" aria-label="Places to go">
<button type="button" data-locus="HBB">HBB gene</button>
<button type="button" data-locus="chr11:5,226,930-5,227,071">HBB exon 1</button>
<button type="button" data-locus="chr11:5,226,982-5,227,022">Sickle-cell variant</button>
<button type="button" data-locus="chr11:5,225,000-5,310,000">β-globin cluster</button>
<form id="goto">
<label for="where">Gene or position</label>
<input id="where" value="HBD" size="18">
<button type="submit">Go</button>
</form>
</div>
<div class="controls" role="group" aria-label="Extra tracks">
<button type="button" id="add-ccre">Add ENCODE regulatory elements</button>
<button type="button" id="add-1kg">Add 1000 Genomes genotypes (slow to draw)</button>
</div>
<p id="status" aria-live="polite">Loading…</p>
<div id="igv"></div>
<script>
const UCSC = "https://hgdownload.soe.ucsc.edu";
// GRCh38 defined from UCSC's public files, so the page does not depend on IGV's genome list.
// The MANE files make gene names searchable.
const reference = {
id: "hg38",
name: "Human (GRCh38/hg38)",
twoBitURL: UCSC + "/goldenPath/hg38/bigZips/hg38.2bit",
chromSizesURL: UCSC + "/goldenPath/hg38/bigZips/hg38.chrom.sizes",
cytobandURL: UCSC + "/goldenPath/hg38/database/cytoBandIdeo.txt.gz",
chromosomeOrder: "chr1,chr2,chr3,chr4,chr5,chr6,chr7,chr8,chr9,chr10,chr11,chr12,chr13,chr14,chr15,chr16,chr17,chr18,chr19,chr20,chr21,chr22,chrX,chrY",
maneBbURL: UCSC + "/gbdb/hg38/mane/mane.1.0.bb",
maneTrixURL: UCSC + "/gbdb/hg38/mane/mane.1.0.ix"
};
const tracks = [
{
name: "MANE Select transcripts",
url: UCSC + "/gbdb/hg38/mane/mane.1.0.bb",
format: "bigBed",
displayMode: "EXPANDED",
labelField: "geneName2", // label transcripts with the gene name
color: "#1f4e9c"
},
{
name: "ClinVar short variants", // UCSC updates this file every week at the same address
url: UCSC + "/gbdb/hg38/bbi/clinvar/clinvarMain.bb",
format: "bigBed",
displayMode: "SQUISHED",
visibilityWindow: 500000 // only fetch when 500 kb or less is in view
},
{
name: "Conservation (phyloP, 100 vertebrates)",
url: UCSC + "/goldenPath/hg38/phyloP100way/hg38.phyloP100way.bw",
format: "bigWig",
color: "#2b8a3e",
altColor: "#c92a2a",
min: -4,
max: 8,
height: 60
}
];
// Regions of interest use 0-based starts: this shades the single base chr11:5,227,002
const roi = [{
name: "HbS: rs334, chr11:5,227,002 T>A",
color: "rgba(220, 0, 0, 0.25)",
features: [{ chr: "chr11", start: 5227001, end: 5227002 }]
}];
igv.createBrowser(document.getElementById("igv"), {
reference,
loadDefaultGenomes: false,
locus: "HBB",
tracks,
roi
}).then(function (browser) {
const status = document.getElementById("status");
// locuschange fires whenever the view moves, with one "frame" per region on screen
browser.on("locuschange", frames => {
status.textContent = "Showing " + frames.map(f => f.getLocusString()).join(", ");
});
status.textContent = "Showing " + browser.currentLoci();
document.querySelectorAll("button[data-locus]").forEach(b =>
b.addEventListener("click", () => browser.search(b.dataset.locus)));
document.getElementById("goto").addEventListener("submit", e => {
e.preventDefault();
browser.search(document.getElementById("where").value);
});
document.getElementById("add-ccre").addEventListener("click", async e => {
e.target.disabled = true;
await browser.loadTrack({
name: "ENCODE candidate regulatory elements",
url: UCSC + "/gbdb/hg38/encode3/ccre/encodeCcreCombined.bb",
format: "bigBed",
displayMode: "COLLAPSED"
});
});
// A public VCF with its tabix index: 3,202 people from the 1000 Genomes 30x panel
const KG = "https://ftp.1000genomes.ebi.ac.uk/vol1/ftp/data_collections/1000G_2504_high_coverage/working/" +
"20220422_3202_phased_SNV_INDEL_SV/1kGP_high_coverage_Illumina.chr11.filtered.SNV_INDEL_SV_phased_panel.vcf.gz";
document.getElementById("add-1kg").addEventListener("click", async e => {
e.target.disabled = true;
await browser.loadTrack({
name: "1000 Genomes 30x (3,202 people)",
type: "variant",
format: "vcf",
url: KG,
indexURL: KG + ".tbi",
displayMode: "SQUISHED",
visibilityWindow: 20000, // only fetch when 20 kb or less is in view
height: 250
});
});
}).catch(err => { // the reference or a track could not be read
document.getElementById("status").textContent =
"The genome browser could not start (" + err.message + "). Check the connection, or try again later.";
});
</script>
</body>
</html>How it works:
- The reference is built from UCSC’s
hg38.2bit, chromosome sizes and bands. The two MANE files make gene names such as HBD searchable.loadDefaultGenomes: falsestops igv.js fetching its genome list. - Tracks are listed in
tracks. igv.js works out the format from the file name, but sayingformat: "bigBed"or"bigWig"avoids surprises.visibilityWindowstops a track loading data until the view is narrower than that many bases. - Navigation uses
browser.search(), which takes a gene name or a position such aschr11:5,226,930-5,227,071. - Events:
browser.on("locuschange", …)runs whenever the view moves. Here it keeps the line above the browser up to date. - More tracks can be added later with
browser.loadTrack(), which returns a promise. - The highlight is a region of interest (
roi). Itsstartcounts from 0, so the base at position 5,227,002 isstart: 5227001, end: 5227002.
These public files answered range requests and allowed use from other websites when we tested them:
| Data | Address |
|---|---|
| GRCh38 sequence, sizes and bands | https://hgdownload.soe.ucsc.edu/goldenPath/hg38/bigZips/hg38.2bit, hg38.chrom.sizes, and …/hg38/database/cytoBandIdeo.txt.gz |
| MANE Select transcripts | https://hgdownload.soe.ucsc.edu/gbdb/hg38/mane/mane.1.0.bb |
| ClinVar, updated weekly | https://hgdownload.soe.ucsc.edu/gbdb/hg38/bbi/clinvar/clinvarMain.bb |
| Conservation (phyloP, 100 vertebrates) | https://hgdownload.soe.ucsc.edu/goldenPath/hg38/phyloP100way/hg38.phyloP100way.bw |
| ENCODE candidate regulatory elements | https://hgdownload.soe.ucsc.edu/gbdb/hg38/encode3/ccre/encodeCcreCombined.bb |
| 1000 Genomes 30x panel, chromosome 11 | The 1kGP_high_coverage_Illumina.chr11…vcf.gz file and its .tbi index on ftp.1000genomes.ebi.ac.uk (full address in the example) |
ClinVar’s own VCF works too, but its large deletions fill every row in igv.js, so UCSC’s ClinVar bigBed is easier to read in class. Files on the ENCODE portal need their public Amazon S3 address (the cloud_metadata.url in the portal’s data), because the portal’s download links refuse requests from other websites.
Turn the browser into an activity
igv.js tells your page what students are doing. locuschange reports every move, and trackclick reports every click on a feature, with the feature’s fields. With those two events a page can check each step of a task and answer straight away, which is hard to do with a worksheet.
The variants are real ClinVar records, stored next to the page. The four people and their genotypes are made up for teaching.
Code igv-find-the-variant.html
<!doctype html>
<html lang="en-GB">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>igv.js activity: find the sickle-cell variant</title>
<script src="https://cdn.jsdelivr.net/npm/igv@3.8.9/dist/igv.min.js"
integrity="sha384-J2SJJANecDBpqn++zIO/AJY/Fpk64lYDp43ibq7bmQONxd9b80ZpvLKBrKdCtYIu"
crossorigin="anonymous"></script>
<style>
body { font: 15px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; margin: 0; padding: 12px 16px 16px; color: #172033; background: #fff; }
h1 { font-size: 1.05rem; margin: 0 0 6px; }
p { margin: 0 0 6px; }
ol { margin: 4px 0 8px; padding-left: 1.4rem; }
li { margin: 2px 0; }
li.done { color: #0b6b50; }
li.done::after { content: " ✓ done"; font-weight: 700; }
#feedback { min-height: 1.5em; font-weight: 600; }
</style>
</head>
<body>
<h1>Activity: find the sickle-cell variant</h1>
<p>Sickle-cell disease is caused by one base change in the β-globin gene, <i>HBB</i>:
<code>NM_000518.5:c.20A>T</code>, which changes glutamate to valine.</p>
<ol>
<li id="t1">Type <b>HBB</b> in the search box above the tracks and press Enter.</li>
<li id="t2">Zoom in until you can read single DNA letters (less than 150 bases across).</li>
<li id="t3">In the <b>Teaching variants</b> track, click the variant that causes sickle-cell disease.</li>
<li id="t4">Which person has two copies of the sickle-cell allele? Click their genotype under that variant.</li>
</ol>
<p id="feedback" role="status" aria-live="polite"></p>
<div id="igv"></div>
<script>
const UCSC = "https://hgdownload.soe.ucsc.edu";
const HBB = { chr: "chr11", start: 5225463, end: 5229395 }; // 0-based start, as igv.js reports
const HBS = { pos: 5227002, id: "rs334" }; // 1-based, as in the VCF
const feedback = document.getElementById("feedback");
const done = id => document.getElementById(id).classList.add("done");
const say = text => { feedback.textContent = text; };
igv.createBrowser(document.getElementById("igv"), {
reference: {
id: "hg38",
name: "Human (GRCh38/hg38)",
twoBitURL: UCSC + "/goldenPath/hg38/bigZips/hg38.2bit",
chromSizesURL: UCSC + "/goldenPath/hg38/bigZips/hg38.chrom.sizes",
cytobandURL: UCSC + "/goldenPath/hg38/database/cytoBandIdeo.txt.gz",
maneBbURL: UCSC + "/gbdb/hg38/mane/mane.1.0.bb",
maneTrixURL: UCSC + "/gbdb/hg38/mane/mane.1.0.ix"
},
loadDefaultGenomes: false,
showSampleNames: true, // label genotype rows with the people's names
locus: "chr11:1,000,000-1,100,000", // away from HBB, so that step 1 needs a search
tracks: [
{ name: "MANE Select transcripts", url: UCSC + "/gbdb/hg38/mane/mane.1.0.bb", format: "bigBed", labelField: "geneName2" },
{
name: "Teaching variants",
type: "variant",
format: "vcf",
// seven ClinVar variants with four fictional people (made-up genotypes), stored next to this page
url: "data/hbb-variants.grch38.vcf.gz",
indexURL: "data/hbb-variants.grch38.vcf.gz.tbi",
displayMode: "EXPANDED",
height: 90
}
]
}).then(function (browser) {
// Fires whenever the view moves. Each frame has chr, start (0-based) and end.
browser.on("locuschange", function (frames) {
const f = frames[0];
const onHBB = f.chr === HBB.chr && f.start < HBB.end && f.end > HBB.start;
if (onHBB && f.end - f.start < 100000) done("t1"); // a search for HBB shows about 4 kb
if (onHBB && f.end - f.start < 150) done("t2");
});
// Fires when a feature is clicked. Return false to hide igv.js's own pop-up,
// a string of HTML to show your own, or undefined to keep the default.
browser.on("trackclick", function (track, data, genomicLocation) {
if (track.name !== "Teaching variants" || !data) return undefined;
const field = name => (data.find(d => d && d.name === name) || {}).value;
const onHbS = Math.floor(genomicLocation) + 1 === HBS.pos;
if (field("Sample")) { // a genotype was clicked
if (!onHbS) { say("That genotype is not at the sickle-cell variant. Click the column under rs334."); return false; }
const zygosity = field("Zygosity");
if (zygosity === "Homozygous variant") {
done("t4");
say(field("Sample") + " has two copies of the HbS allele (" + field("Genotype") + "): sickle-cell anaemia, the commonest form of sickle-cell disease.");
} else if (zygosity === "Heterozygous") {
say(field("Sample") + " has one copy of HbS, which on its own gives sickle-cell trait, not disease. " +
"But check their other HBB variants: HbS with HbC, or with a β-thalassaemia variant, does cause sickle-cell disease.");
} else {
say(field("Sample") + " has no copy of HbS. Try another row.");
}
return false;
}
const id = field("ID");
if (id === HBS.id) {
done("t3");
say("Correct. rs334 (chr11:5,227,002 T>A) changes codon 7 from GAG (glutamate) to GTG (valine).");
return false;
}
if (id) {
say("Not this one: that is " + id + " (" + (field("LEGACY") || "").replace(/_/g, " ") + "). Look a few bases away.");
return false;
}
return undefined;
});
}).catch(err => { // the reference or a track could not be read
say("The genome browser could not start (" + err.message + "). Check the connection, or try again later.");
});
</script>
</body>
</html>If you build your own version, these are the parts to reuse:
- Checking where students are.
locuschangereceives a list of frames, one for each region on screen. Each frame haschr,start(counted from 0) andend. The browser starts away from HBB, so step 1 is done only when the frame overlaps the gene and is narrower than 100 kb, as it is after a search; step 2 when it is narrower than 150 bases. - Checking what they clicked.
trackclickreceives the track, the clicked feature’s fields as a list of{ name, value }pairs, and the position. For a genotype, the fields includeSample,GenotypeandZygosity; for a variant,IDand the VCF’s INFO fields. - Controlling the pop-up. Return
falseto hide igv.js’s own pop-up, a string of HTML to show your own, orundefinedto keep the default. - Feedback goes into an element with
aria-live="polite", so screen readers announce it.
To use the activity with another variant, change HBB, HBS and the wording, and point the track at your own VCF.
Let students open their own files
A file input can hand files from the student’s computer straight to igv.js. The browser reads them in place, so nothing is uploaded. This suits practicals where students produce their own VCF or BAM files in an earlier step.
Try it with the files in this guide’s data folder on GitHub: download hbb-region.fa, hbb-region.fa.fai, hbb-variants.vcf.gz and hbb-variants.vcf.gz.tbi, then choose them here.
Code igv-own-files.html
<!doctype html>
<html lang="en-GB">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>igv.js: open files from your own computer</title>
<script src="https://cdn.jsdelivr.net/npm/igv@3.8.9/dist/igv.min.js"
integrity="sha384-J2SJJANecDBpqn++zIO/AJY/Fpk64lYDp43ibq7bmQONxd9b80ZpvLKBrKdCtYIu"
crossorigin="anonymous"></script>
<style>
body { font: 15px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; margin: 0; padding: 12px 16px 16px; color: #172033; background: #fff; }
h1 { font-size: 1.05rem; margin: 0 0 6px; }
p { margin: 0 0 6px; }
fieldset { margin: 0 0 8px; border: 1px solid #c3cbd8; border-radius: 6px; padding: 6px 10px 8px; }
legend { font-weight: 600; padding: 0 4px; }
label { display: block; font-size: 14px; margin-bottom: 4px; }
#log { font: 13px/1.4 ui-monospace, Menlo, Consolas, monospace; white-space: pre-line; color: #3b4659; }
</style>
</head>
<body>
<h1>View your own files</h1>
<p>Your browser reads the files. Nothing is uploaded.</p>
<fieldset>
<legend>1. Reference sequence (optional)</legend>
<label for="ref-files">A FASTA file and its .fai index</label>
<input id="ref-files" type="file" multiple accept=".fa,.fasta,.fna,.fai">
</fieldset>
<fieldset>
<legend>2. Data tracks</legend>
<label for="track-files">VCF, BED, GFF3, bigWig, bigBed or BAM files, with their index files (.tbi, .csi, .bai)</label>
<input id="track-files" type="file" multiple>
</fieldset>
<p id="log" role="status" aria-live="polite"></p>
<div id="igv"></div>
<script>
const INDEX_EXTENSIONS = [".tbi", ".csi", ".bai", ".crai", ".fai", ".idx"];
const isIndex = f => INDEX_EXTENSIONS.some(ext => f.name.toLowerCase().endsWith(ext));
// "x.vcf.gz.tbi" belongs to "x.vcf.gz"; "x.bai" can also belong to "x.bam"
const findIndex = (f, files) => files.find(i => isIndex(i) && i !== f &&
(i.name.startsWith(f.name + ".") || i.name.replace(/\.[^.]+$/, "") === f.name.replace(/\.[^.]+$/, "")));
const log = msg => { document.getElementById("log").textContent += msg + "\n"; };
const container = document.getElementById("igv");
let browser = null;
// Start with GRCh38 from UCSC's files, which costs far less to download than genome: "hg38".
// If UCSC cannot be reached, students can still start from their own FASTA file in step 1.
const UCSC = "https://hgdownload.soe.ucsc.edu";
const ready = igv.createBrowser(container, {
reference: {
id: "hg38",
name: "Human (GRCh38/hg38)",
twoBitURL: UCSC + "/goldenPath/hg38/bigZips/hg38.2bit",
chromSizesURL: UCSC + "/goldenPath/hg38/bigZips/hg38.chrom.sizes",
cytobandURL: UCSC + "/goldenPath/hg38/database/cytoBandIdeo.txt.gz",
maneBbURL: UCSC + "/gbdb/hg38/mane/mane.1.0.bb",
maneTrixURL: UCSC + "/gbdb/hg38/mane/mane.1.0.ix"
},
loadDefaultGenomes: false,
locus: "HBB",
tracks: [
{ name: "MANE Select transcripts", url: UCSC + "/gbdb/hg38/mane/mane.1.0.bb", format: "bigBed", labelField: "geneName2" }
]
}).then(b => { browser = b; })
.catch(err => log("GRCh38 could not be loaded from UCSC (" + err.message + "). You can still choose your own FASTA file in step 1."));
document.getElementById("ref-files").addEventListener("change", async e => {
const files = Array.from(e.target.files);
const fasta = files.find(f => !isIndex(f));
if (!fasta) { log("Choose a FASTA file, and its .fai index if you have one."); return; }
const fai = findIndex(fasta, files);
const reference = {
id: fasta.name, name: fasta.name,
fastaURL: fasta, // a File object works wherever igv.js expects a URL
indexURL: fai // without a .fai the whole FASTA is read into memory
};
await ready; // let the first start finish, or fail, before going on
try {
if (browser) {
await browser.loadGenome(reference);
} else { // UCSC could not be reached: start the browser from this file
igv.removeAllBrowsers(); // clears the browser that failed (igv.js draws inside a shadow root)
browser = await igv.createBrowser(container, { reference, loadDefaultGenomes: false });
}
log("Reference: " + fasta.name + (fai ? " (indexed)" : " (not indexed)"));
} catch (err) { log("Could not load " + fasta.name + ": " + err.message); }
});
document.getElementById("track-files").addEventListener("change", async e => {
await ready;
if (!browser) { log("Choose a reference sequence in step 1 first."); e.target.value = ""; return; }
const files = Array.from(e.target.files);
for (const f of files.filter(f => !isIndex(f))) {
const index = findIndex(f, files);
const config = { name: f.name, url: f };
if (index) config.indexURL = index;
else config.indexed = false; // small files can be read whole, without an index
try {
await browser.loadTrack(config);
log("Loaded " + f.name + (index ? " with " + index.name : ""));
} catch (err) { log("Could not load " + f.name + ": " + err.message); }
}
e.target.value = ""; // so the same files can be chosen again
});
</script>
</body>
</html>A File object works wherever igv.js expects a URL. Index files (.tbi, .bai, .fai) have to be chosen at the same time as the data, and the example pairs them up by name. Small files without an index can be read whole with indexed: false.
The page itself still loads igv.js, and the genome it starts with, from the internet. For sensitive files, keep a copy of igv.min.js next to the page and point the script tag at it, and start the browser from a FASTA file hosted beside the page, so that the page works with no connection at all.
Host a small dataset next to the page
For a class, the most reliable data are files you host yourself: they cannot move, change or go offline when a public server does. This example uses a 20 kb slice of chromosome 11 around HBB, about 26 kB in all, stored in a data folder next to the examples (see it on GitHub).
Code igv-self-hosted.html
<!doctype html>
<html lang="en-GB">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>igv.js: a 20 kb region around HBB, with every file hosted next to the page</title>
<script src="https://cdn.jsdelivr.net/npm/igv@3.8.9/dist/igv.min.js"
integrity="sha384-J2SJJANecDBpqn++zIO/AJY/Fpk64lYDp43ibq7bmQONxd9b80ZpvLKBrKdCtYIu"
crossorigin="anonymous"></script>
<style>
body { font: 15px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; margin: 0; padding: 12px 16px 16px; color: #172033; background: #fff; }
h1 { font-size: 1.05rem; margin: 0 0 6px; }
p { margin: 0 0 8px; font-size: 14px; color: #3b4659; }
</style>
</head>
<body>
<h1>A 20 kb slice of chromosome 11 around HBB</h1>
<p>Position 1 of <code>HBB_region</code> is GRCh38 chr11:5,217,001, so add 5,217,000 to convert. Try typing HBD in the search box.</p>
<div id="igv"></div>
<script>
const DATA = "data/"; // the folder next to this page
igv.createBrowser(document.getElementById("igv"), {
reference: {
id: "hbb-region",
name: "HBB region (GRCh38 chr11:5,217,001-5,237,000)",
fastaURL: DATA + "hbb-region.fa",
indexURL: DATA + "hbb-region.fa.fai",
wholeGenomeView: false
},
loadDefaultGenomes: false, // do not fetch IGV's list of hosted genomes
search: false, // do not send unknown names to IGV's search service
locus: "HBB_region:8,000-10,600", // start with coordinates: names are searchable once tracks load
tracks: [
{
name: "Genes (RefSeq Select)",
url: DATA + "hbb-region.gff3",
format: "gff3",
indexed: false, // a small file, read whole
displayMode: "EXPANDED",
searchableFields: ["name", "gene"] // so HBB, HBD or NM_000518.5 can be typed in the search box
},
{
name: "Variants (ClinVar, with four fictional people)",
type: "variant",
format: "vcf",
url: DATA + "hbb-variants.vcf.gz",
indexURL: DATA + "hbb-variants.vcf.gz.tbi",
height: 90
}
],
roi: [{
name: "Sickle-cell variant (rs334)",
color: "rgba(220, 0, 0, 0.25)",
features: [{ chr: "HBB_region", start: 10001, end: 10002 }]
}],
showSampleNames: true
});
</script>
</body>
</html>The folder holds these files:
| File | Contents |
|---|---|
hbb-region.fa and .fai |
GRCh38 chr11:5,217,001–5,237,000 from UCSC, as one sequence called HBB_region. Position 1 here is chr11:5,217,001. |
hbb-region.gff3 and .bed |
HBB (NM_000518.5) and HBD (NM_000519.4) from RefSeq Select: genes, transcripts, exons and coding sequence. Exons are numbered along each gene, which runs on the reverse strand, so exon 1 is the one furthest to the right. The BED names contain a space, which is why the command below uses -tab. |
hbb-variants.vcf.gz and .tbi |
Seven ClinVar variants, including HbS (rs334), HbC, HbE and β-thalassaemia variants, with four fictional people. |
hbb-variants.grch38.vcf.gz and .tbi |
The same variants at their real chromosome 11 positions, for use over the full GRCh38 genome. |
Index files come from the standard command-line tools. FASTA lines must all be the same length, VCF and BED files must be sorted, and VCF files must be compressed with bgzip, not ordinary gzip:
samtools faidx hbb-region.fa # makes hbb-region.fa.fai
bgzip -c hbb-variants.vcf > hbb-variants.vcf.gz # block-compressed, so it can be read in pieces
tabix -p vcf hbb-variants.vcf.gz # makes hbb-variants.vcf.gz.tbi
sort -k1,1 -k2,2n genes.bed > genes.sorted.bed # bedToBigBed refuses unsorted files
bedToBigBed -tab genes.sorted.bed hg38.chrom.sizes genes.bb # UCSC tool, for large annotation files
GitHub Pages answers the range requests these files need, and allows other websites to read them. Python’s built-in test server does not handle range requests, so to try indexed files on your own computer use a server that does; this site’s repository has one in tools/serve.py.
In a self-hosted page, start the view with coordinates rather than a gene name, because names in your own tracks become searchable only once the track has loaded. search: false stops igv.js sending unknown names to IGV’s search service.
Use JBrowse 2 when students need more detail
JBrowse 2 [2] is heavier than igv.js, but clicking a feature opens a full table of its details, and clicking a VCF record shows every field with a genotype count. It also has six-frame translation and its own gene search. The linear genome view comes as a single script that includes React.
This example downloads JBrowse 2 (1.4 MB compressed) when it starts.
Code jbrowse-basic.html
<!doctype html>
<html lang="en-GB">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>JBrowse 2: a linear genome view open at HBB</title>
<!-- One bundle that includes React. It defines the global JBrowseReactLinearGenomeView. -->
<script src="https://cdn.jsdelivr.net/npm/@jbrowse/react-linear-genome-view2@4.3.0/dist/react-linear-genome-view.umd.production.min.js"
integrity="sha384-J8EFPOshnppmv2fT8oYTTjrGpanve6mVThvdPS/++mu+OLoZla3BKDnAe0UyN27B"
crossorigin="anonymous"></script>
<style>
body { font: 15px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; margin: 0; padding: 12px 16px 16px; color: #172033; background: #fff; }
h1 { font-size: 1.05rem; margin: 0 0 8px; }
</style>
</head>
<body>
<h1>HBB in JBrowse 2. Click a transcript to see its details.</h1>
<div id="jbrowse"></div>
<script>
const { React, createRoot, createViewState, JBrowseLinearGenomeView } = JBrowseReactLinearGenomeView;
const state = createViewState({
assembly: {
name: "hg38",
sequence: {
type: "ReferenceSequenceTrack",
trackId: "hg38-reference",
adapter: { type: "BgzipFastaAdapter", uri: "https://jbrowse.org/genomes/GRCh38/fasta/hg38.prefix.fa.gz" }
},
// This FASTA calls chromosome 11 "11" and UCSC's files call it "chr11". The alias table links the two.
refNameAliases: {
adapter: { type: "RefNameAliasAdapter", uri: "https://hgdownload.soe.ucsc.edu/goldenPath/hg38/bigZips/hg38.chromAlias.txt" }
}
},
tracks: [{
type: "FeatureTrack",
trackId: "mane",
name: "MANE Select transcripts",
assemblyNames: ["hg38"],
adapter: { type: "BigBedAdapter", uri: "https://hgdownload.soe.ucsc.edu/gbdb/hg38/mane/mane.1.0.bb" }
}],
location: "chr11:5,225,464..5,229,395"
});
state.session.view.showTrack("mane");
createRoot(document.getElementById("jbrowse")).render(
React.createElement(JBrowseLinearGenomeView, { viewState: state })
);
</script>
</body>
</html>Two details are easy to get wrong:
- Chromosome names. The JBrowse project’s GRCh38 sequence calls chromosome 11
11, while UCSC’s files call itchr11. Without therefNameAliasestable, the view quietly shows the assembly picker instead of HBB. - Reacting to students. JBrowse has no
trackclickevent. Instead,createViewStateaccepts anonChangefunction that receives a description of every change. A change to/session/view/offsetPxmeans the view moved, and a change under/session/widgets/…/featureDatameans a feature was clicked.
If you would rather not write code, the whole of JBrowse Web can sit in an iframe. The project’s own copy (https://jbrowse.org/code/jb2/v4.3.0/) allows this and takes its settings from the address. Or unzip the release from GitHub (24 MB) into your own site.
Link to UCSC and Ensembl
The UCSC Genome Browser [3] and Ensembl [4] have far more tracks than any page you build, but neither can be shown inside your page: UCSC puts a check for automated visitors in front of its browser, and Ensembl tells browsers not to show it in a frame. Link to them in a new tab instead, show a picture of the view, or take data from their APIs.
Code ucsc-ensembl-links.html
<!doctype html>
<html lang="en-GB">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Links into UCSC and Ensembl, a picture of the view, and live data from their APIs</title>
<style>
body { font: 15px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; margin: 0; padding: 12px 16px 16px; color: #172033; background: #fff; }
h1 { font-size: 1.05rem; margin: 0 0 6px; }
h2 { font-size: 0.95rem; margin: 14px 0 6px; }
ul { margin: 0; padding-left: 1.3rem; }
img { max-width: 100%; height: auto; border: 1px solid #c3cbd8; }
pre { font: 13px/1.45 ui-monospace, Menlo, Consolas, monospace; background: #f4f6fa; border: 1px solid #dde2ea; border-radius: 6px; padding: 8px 10px; margin: 0 0 8px; white-space: pre-wrap; }
</style>
</head>
<body>
<h1>HBB in UCSC and Ensembl</h1>
<h2>Open a genome browser at the right place</h2>
<ul>
<li><a id="ucsc-link" target="_blank" rel="noopener">UCSC Genome Browser: HBB with MANE transcripts, ClinVar and conservation</a></li>
<li><a id="ensembl-gene" target="_blank" rel="noopener">Ensembl: the HBB gene</a></li>
<li><a id="ensembl-variant" target="_blank" rel="noopener">Ensembl: the sickle-cell variant, rs334</a></li>
</ul>
<h2>A picture of the same UCSC view, for slides and handouts</h2>
<img id="ucsc-img" alt="UCSC Genome Browser view of HBB exon 1, with the sickle-cell position highlighted in orange" loading="lazy">
<h2>Live data from the APIs</h2>
<pre id="gene" aria-live="polite">Asking Ensembl for the HBB gene record…</pre>
<pre id="vep" aria-live="polite">Asking Ensembl VEP for the effect of rs334…</pre>
<pre id="seq" aria-live="polite">Asking UCSC for the DNA around the variant…</pre>
<script>
// ----- Links into the UCSC Genome Browser -----
function ucscURL(page, { db, position, tracks = {}, highlight, hideTracks = true, extra = {} }) {
const p = new URLSearchParams({ db, position });
if (hideTracks) p.set("hideTracks", "1"); // start from an empty display…
for (const [track, vis] of Object.entries(tracks)) p.set(track, vis); // …then show these (hide, dense, squish, pack, full)
if (highlight) p.set("highlight", highlight); // assembly.chrom:start-end#RRGGBB, 1-based
for (const [k, v] of Object.entries(extra)) p.set(k, v);
return "https://genome.ucsc.edu/cgi-bin/" + page + "?" + p;
}
const view = {
db: "hg38",
position: "chr11:5225464-5229395",
tracks: {
mane: "pack",
clinvar: "pack", clinvarCnv_sel: "0", clinvarSubLolly_sel: "0", // ClinVar without two of its sub-tracks
cons100way: "full", cons100wayViewalign: "hide", cons100wayViewelements: "hide", cons100wayViewphastcons: "hide"
},
highlight: "hg38.chr11:5227002-5227002#FFA500" // 6-digit colour: UCSC draws it pale
};
document.getElementById("ucsc-link").href = ucscURL("hgTracks", view);
// hgRenderTracks returns a PNG of the same view. Save it into your site for anything that must last.
document.getElementById("ucsc-img").src = ucscURL("hgRenderTracks",
{ ...view, position: "chr11:5226930-5227071", extra: { pix: "900" } });
// ----- Links into Ensembl -----
// Ensembl's new site names assemblies by accession: GCA_000001405.29 is GRCh38.
const ENSEMBL = "https://www.ensembl.org";
document.getElementById("ensembl-gene").href = ENSEMBL + "/genome-browser/GCA_000001405.29?focus=gene:ENSG00000244734";
document.getElementById("ensembl-variant").href = ENSEMBL + "/genome-browser/GCA_000001405.29?focus=variant:11:5227002:rs334";
// ----- Data from the APIs -----
// Fetch JSON with a time limit and two retries. Busy services answer 429 (too many requests) or 5xx;
// an error without CORS headers reaches the page only as "Failed to fetch".
async function getJSON(url, { tries = 3, timeoutMs = 15000 } = {}) {
for (let attempt = 1; ; attempt++) {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
let r;
try {
r = await fetch(url, { signal: ctrl.signal });
if (r.ok) return await r.json();
} catch (err) { // no answer in time, or none at all
if (attempt >= tries) throw err.name === "AbortError" ? new Error("timed out") : err;
} finally {
clearTimeout(timer);
}
// Retry only when the service is busy or failing: other answers, such as 404, will not change
if (r && !r.ok && (attempt >= tries || ![429, 500, 502, 503, 504].includes(r.status))) throw new Error("HTTP " + r.status);
await new Promise(res => setTimeout(res, 1000 * 2 ** attempt)); // wait 2 s, then 4 s
}
}
const REST = "https://rest.ensembl.org";
const JSONQ = "content-type=application/json"; // in the address rather than a header, which avoids a CORS pre-flight
getJSON(REST + "/lookup/symbol/homo_sapiens/HBB?" + JSONQ).then(g => {
document.getElementById("gene").textContent =
g.display_name + " (" + g.id + "): " + g.description + "\n" +
"GRCh38 " + g.seq_region_name + ":" + g.start + "-" + g.end + ", " + (g.strand === 1 ? "forward" : "reverse") + " strand\n" +
"Canonical transcript: " + g.canonical_transcript;
}).catch(err => {
document.getElementById("gene").textContent = "Ensembl did not answer (" + err.message + "). Try again later.";
});
getJSON(REST + "/vep/human/id/rs334?" + JSONQ + "&canonical=1", { timeoutMs: 30000 }).then(([v]) => {
// rs334 has more than one alternative allele; A is the sickle-cell (HbS) allele
const t = v.transcript_consequences.find(c => c.canonical && c.gene_symbol === "HBB" && c.variant_allele === "A");
document.getElementById("vep").textContent =
"rs334 (" + v.allele_string + ") at " + v.seq_region_name + ":" + v.start + ", most severe effect: " + v.most_severe_consequence + "\n" +
"HbS allele A on " + t.transcript_id + ": codon " + t.codons + ", amino acids " + t.amino_acids + " at protein position " + t.protein_start;
}).catch(err => {
document.getElementById("vep").textContent = "Ensembl VEP did not answer (" + err.message + "). Try again later.";
});
// The UCSC API counts from 0 and excludes the end: this asks for chr11:5,226,990-5,227,014 in 1-based terms
getJSON("https://api.genome.ucsc.edu/getData/sequence?genome=hg38;chrom=chr11;start=5226989;end=5227014").then(d => {
const dna = d.dna.toUpperCase();
const i = 5227002 - 5226990; // where the sickle-cell base sits in the string
document.getElementById("seq").textContent =
"chr11:5,226,990-5,227,014 (+ strand): " + dna.slice(0, i) + "[" + dna[i] + "]" + dna.slice(i + 1) + "\n" +
"HBB runs on the reverse strand, so CTC here reads GAG (Glu) in the gene. T>A here makes GTG (Val).";
}).catch(err => {
document.getElementById("seq").textContent = "The UCSC API did not answer (" + err.message + ").";
});
</script>
</body>
</html>UCSC builds the view from parameters in the address:
| To do this | Add |
|---|---|
| Choose the assembly and position | db=hg38&position=chr11:5225464-5229395 |
| Start with no tracks showing | hideTracks=1 (this also hides any tracks you add) |
| Show tracks | mane=pack&clinvar=pack&cons100way=full (or dense, squish, hide) |
| Turn off part of a track | clinvarCnv_sel=0, or cons100wayViewalign=hide for one of a track’s views |
| Highlight a region | highlight=hg38.chr11:5227002-5227002%23FFA500 (positions count from 1; give a 6-digit colour, which UCSC draws pale: the 8-digit #FFA50080 came out magenta in our tests) |
| Add a track from a file | hgt.customText= followed by an encoded track line pointing at your file |
| Load a track hub | hubUrl=https://…/hub.txt |
Track names for these parameters are listed by https://api.genome.ucsc.edu/list/tracks?genome=hg38. The same parameters on hgRenderTracks return a PNG image, which is handy for slides and handouts; save a copy into your site rather than relying on UCSC to draw it every time.
Ensembl moved to a new website in August 2026. Its addresses now name the assembly by accession (GCA_000001405.29 is GRCh38), as in https://www.ensembl.org/genome-browser/GCA_000001405.29?focus=gene:ENSG00000244734. Older addresses still work, because they redirect to the new ones.
Both APIs work from a web page. The UCSC API returns sequence and track data. The Ensembl REST API returns gene records and variant effect predictions, and allows 55,000 requests an hour, but it was slow and often failed while we tested it. The example therefore waits, retries twice, and says so if there is still no answer.
Things that catch people out
- Phones. igv.js’s toolbar needs about 750 pixels. Turning off the optional buttons (
showChromosomeWidget,showCursorTrackingGuideButton,showCenterGuideButton,showTrackLabelButton,showSVGButtonandshowMultiSelectButton, allfalse) brings that down to about 470, which is still wider than most phones. For phone users, add a picture of the view and a text summary. - Counting from 0 or 1. Regions of interest and the positions igv.js passes to your code count from 0. VCF positions and the search box count from 1. The educators’ page has a table of the conventions.
chr11or11. UCSC names chromosomeschr11, while Ensembl and many VCF files say11. igv.js matches the two names by itself. JBrowse needs an alias table, as in its example above. Many analysis tools match neither, and skip mismatched lines without an error.- Big files. The 1000 Genomes panel downloads only 0.4 MB for one view, but drawing 3,202 genotype rows takes 15 to 20 seconds. Load large tracks on request, and set
visibilityWindow. - Screen readers. Tracks are drawn as images on a canvas, so screen readers cannot read them. Give every task a text version, and do not rely on ClinVar’s red and green alone. See accessibility.
- Versions. Each example names an exact version of its library. When you update, read the release notes and test the page again.
Licences and credits
igv.js is under the MIT licence. JBrowse 2 is under Apache 2.0, although its linear genome view package is labelled MIT on npm. If you publish teaching material built on them, cite their papers [1, 2]. The UCSC, ClinVar, ENCODE and 1000 Genomes data are free to use, each under its provider’s terms; check those before you redistribute copies of the files.
References
- Robinson JT, Thorvaldsdóttir H, Turner D, Mesirov JP (2023). igv.js: an embeddable JavaScript implementation of the Integrative Genomics Viewer (IGV). Bioinformatics 39(1): btac830. doi:10.1093/bioinformatics/btac830
- Diesh C, Stevens GJ, Xie P, et al. (2023). JBrowse 2: a modular genome browser with views of synteny and structural variation. Genome Biology 24: 74. doi:10.1186/s13059-023-02914-z
- Nassar LR, Barber GP, Benet-Pagès A, et al. (2023). The UCSC Genome Browser database: 2023 update. Nucleic Acids Research 51(D1): D1188–D1195. doi:10.1093/nar/gkac1072
- Yates AD, Austine-Orimoloye O, Azov AG, et al. (2026). Ensembl 2026. Nucleic Acids Research 54(D1): D1053–D1060. doi:10.1093/nar/gkaf1239