In this article I will go over how to set up a [Lit](https://lit.dev) web component and use it to render [musicxml](https://www.musicxml.com/) from a src attribute or inline xml using [opensheetmusicdisplay](https://github.com/opensheetmusicdisplay/opensheetmusicdisplay).
![](attachments/gifs_nod_yes.gif)
Now any sheet music can be rendered based on the browser width as an svg or canvas (and will resize when the viewport changes).
> **TLDR** The final source [here](https://github.com/rodydavis/lit-sheet-music) and an online [demo](https://rodydavis.github.io/lit-sheet-music/).
## Prerequisites
- Vscode
- Node >= 16
- Typescript
## Getting Started
We can start off by navigating in terminal to the location of the project and run the following:
```bash
npm init @vitejs/app --template lit-ts
```
Then enter a project name `lit-sheet-music` and now open the project in vscode and install the dependencies:
```bash
cd lit-sheet-music
npm i lit opensheetmusicdisplay
npm i -D @types/node
code .
```
Update the `vite.config.ts` with the following:
```js
import { defineConfig } from "vite";
import { resolve } from "path";
export default defineConfig({
base: "/lit-sheet-music/",
build: {
lib: {
entry: "src/lit-sheet-music.ts",
formats: ["es"],
},
rollupOptions: {
input: {
main: resolve(__dirname, "index.html"),
},
},
},
});
```
## Template
Open up the `index.html` and update it with the following:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lit Sheet Music</title>
<script type="module" src="/src/sheet-music.ts"></script>
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<sheet-music
src="https://raw.githubusercontent.com/opensheetmusicdisplay/opensheetmusicdisplay/develop/demo/BrahWiMeSample.musicxml"
>
</sheet-music>
</body>
</html>
```
If local [musicxml](https://www.musicxml.com/) is intended to be used update `index.html` with the following:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lit Sheet Music</title>
<script type="module" src="/src/sheet-music.ts"></script>
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<sheet-music>
<script type="text/xml">
<?xml version="1.0" standalone="no"?>
<!DOCTYPE score-partwise PUBLIC
"-//Recordare//DTD MusicXML Partwise//EN"
"http://www.musicxml.org/dtds/partwise.dtd">
<score-partwise>
<part-list>
<score-part id="P1">
<part-name>Voice</part-name>
</score-part>
</part-list>
<part id="P1">
<measure number="0" implicit="yes">
<attributes>
<divisions>4</divisions>
<key>
<fifths>-3</fifths>
<mode>major</mode>
</key>
<time>
<beats>2</beats>
<beat-type>4</beat-type>
</time>
<clef>
<sign>G</sign>
<line>2</line>
</clef>
<directive>Langsam, innig.</directive>
</attributes>
<note>
<pitch>
<step>G</step>
<octave>4</octave>
</pitch>
<duration>2</duration>
<type>eighth</type>
<stem>up</stem>
<notations>
<dynamics>
<p/>
</dynamics>
</notations>
<lyric>
<syllabic>single</syllabic>
<text>Wärst</text>
</lyric>
</note>
</measure>
<measure number="1">
<note>
<pitch>
<step>F</step>
<octave>4</octave>
</pitch>
<duration>3</duration>
<type>eighth</type>
<dot/>
<stem>up</stem>
<lyric>
<syllabic>single</syllabic>
<text>du</text>
</lyric>
</note>
<note>
<pitch>
<step>E</step>
<alter>-1</alter>
<octave>4</octave>
</pitch>
<duration>1</duration>
<type>16th</type>
<stem>up</stem>
<lyric>
<syllabic>single</syllabic>
<text>nicht,</text>
</lyric>
</note>
<note>
<pitch>
<step>E</step>
<alter>-1</alter>
<octave>4</octave>
</pitch>
<duration>2</duration>
<type>eighth</type>
<stem>up</stem>
<lyric>
<syllabic>begin</syllabic>
<text>heil</text>
</lyric>
</note>
<note>
<pitch>
<step>B</step>
<alter>-1</alter>
<octave>4</octave>
</pitch>
<duration>1</duration>
<type>16th</type>
<stem>up</stem>
<beam number="1">begin</beam>
<beam number="2">begin</beam>
<notations>
<slur type="start" number="1"/>
</notations>
<lyric>
<syllabic>end</syllabic>
<text>ger</text>
<extend/>
</lyric>
</note>
<note>
<pitch>
<step>G</step>
<octave>4</octave>
</pitch>
<duration>1</duration>
<type>16th</type>
<stem>up</stem>
<beam number="1">end</beam>
<beam number="2">end</beam>
<notations>
<slur type="stop" number="1"/>
</notations>
<lyric>
<extend/>
</lyric>
</note>
</measure>
</part>
</score-partwise>
</script>
</sheet-music>
</body>
</html>
```
We are passing a src attribute to the web component for this example but we can also add a script tag with the type attribute set to `text/xml` with the contents containing the json.
## Web Component
Before we update our component we need to rename `my-element.ts` to `sheet-music.ts`
Open up `sheet-music.ts` and update it with the following:
```js
import { html, css, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators.js";
import { IOSMDOptions, OpenSheetMusicDisplay } from "opensheetmusicdisplay";
type BackendType = "svg" | "canvas";
type DrawingType = "compact" | "default";
@customElement("sheet-music")
export class SheetMusic extends LitElement {
_zoom = 1.0;
@property({ type: Boolean }) allowDrop = false;
@property() src = "";
@query("main") canvas!: HTMLElement;
controller?: OpenSheetMusicDisplay;
options: IOSMDOptions = {
autoResize: true,
backend: "canvas" as BackendType,
drawingParameters: "default" as DrawingType,
};
static styles = css`
main {
overflow-x: auto;
}
`;
render() {
return html`<main></main>`;
}
async renderMusic(content: string) {
if (!this.controller) return;
await this.controller.load(content);
this.controller.zoom = this._zoom;
this.controller.render();
this.requestUpdate();
}
private async getMusic(): Promise<string> {
// Check if src attribute is set and prefer it over the slot
if (this.src.length > 0) return fetch(this.src).then((res) => res.text());
// Check if slot children exist and return the xml
const elem = this.parentElement?.querySelector(
'script[type="text/xml"]'
) as HTMLScriptElement;
if (elem) return elem.innerHTML;
// Return nothing if neither is found
return "";
}
async firstUpdated() {
this.controller = new OpenSheetMusicDisplay(this.canvas, this.options);
this.requestUpdate();
// Check for any music and update if found
const music = await this.getMusic();
if (music.length > 0) this.renderMusic(music);
}
}
declare global {
interface HTMLElementTagNameMap {
"sheet-music": SheetMusic;
}
}
```
Run `npm run dev` and the following should appear if all went well:
![](attachments/sheet-music_finished.webp)
## Conclusion
If you want to learn more about building with Lit you can read the docs [here](https://lit.dev).
The source for this example can be found [here](https://github.com/rodydavis/lit-sheet-music).
![](attachments/sheet-music_preview.gif)