Render sheet music in the browser. Try it at https://vexml.dev.
npm install @stringsync/vexmlimport { render } from '@stringsync/vexml';
const score = await render(musicXML, document.getElementById('score'));Building with a coding agent? Add the vexml skill, then ask your agent for what you want:
npx skills add stringsync/vexml@vexmlThe skill is the full guide, also readable at skills/vexml/SKILL.md.
- Notation and tablature
- Playback cursor
- Pointer events
- Editing
- Sticky clef and key
- Fonts and colors
- Gap measures
- Layouts and printing
- Canvas layers
- Snapshots
Standard notation, guitar tab, lyrics, chord symbols and diagrams, repeats and voltas. The score scales to fit its container.
vexml engraves an mdom document, so it
reads whatever mdom does: MusicXML text (.musicxml, .xml), a compressed .mxl Blob, or
a document mdom parsed from another format, such as Guitar Pro (.gp).
import { GuitarProParser } from '@stringsync/mdom';
const document = await new GuitarProParser().parseFromBlob(gpFile);
await render(document, element);A cursor walks the timeline with repeats and voltas unrolled, reporting which notes start, sustain and stop, so you can drive audio or light up notes.
const cursor = score.createCursor();
cursor.sync(score.createPlayhead());
cursor.events.on('change', (e) => e.highlighted.forEach((note) => note.color.on('red')));
cursor.seekMs(audio.currentTime * 1000);Every note, measure and fret is hit-testable. Events report what's under the pointer, and notes can be colored or haloed.
import { Note } from '@stringsync/vexml';
score.events.on('click', (e) => {
if (e.target instanceof Note) e.target.halo.on('rgba(41, 98, 255, 0.35)');
});EditingSession adds keyboard and pointer selection with undo and redo over an editable
document.
const editor = new EditingSession(document);
score.createEditingController(editor);
editor.setPitch({ step: 'G', octave: 5 });A single scrolling line keeps its clefs and key signatures pinned at the left edge, switching when a change scrolls under them.
await render(musicXML, element, {
layout: { type: 'panoramic', stickySignatures: true },
width: 420,
});Swap the music font (Bravura by default) and the text font, and recolor notation, text and background for any theme.
await render(musicXML, element, {
backgroundColor: '#0f172a',
fonts: {
notation: { family: 'Petaluma', url: '/fonts/petaluma.woff2', color: '#e2e8f0' },
text: { family: 'Inter', url: '/fonts/inter.woff2', color: '#fbbf24' },
},
});Insert empty, labeled measures that last a fixed time, to line the score up with a video or recording that pauses between passages.
await render(musicXML, element, {
gaps: [{ beforeMeasureIndex: 0, durationMs: 8000, label: 'Intro' }],
});Fit the container, scroll one long line, or lay the score out on fixed-size pages and export each page as a PNG for print.
const score = await render(musicXML, element, {
layout: { type: 'paged', pageWidth: 816, pageHeight: 1056, margin: 48 },
});
const pngs = await Promise.all(score.getPages().map((page) => page.toBlob()));Draw your own annotations in front of or behind the engraving on a 2D canvas that stays aligned with the score.
const layer = score.addLayer('content', -1);
layer.ctx.fillRect(50, 50, 100, 80);Cache a rendered score as JSON and render it again roughly 10x faster, make snapshots on a server, or paint one with an 8 KB script before your app loads.
const snapshot = score.snapshot();
await render(snapshot, element);Needs bun and docker. The library is
packages/vexml; the rest of the repo builds, tests and demos it.
./bin/vex dev # run the playground
./bin/vex render --input song.musicxml # render a MusicXML file to a PNG./bin/vex readme re-records this README's images and GIFs (it needs ImageMagick).






