Skip to content

Instantly share code, notes, and snippets.

@joshuadutton
Last active October 23, 2025 23:34
Show Gist options
  • Select an option

  • Save joshuadutton/c6418a15bf3f51d97590f48ae63eeff1 to your computer and use it in GitHub Desktop.

Select an option

Save joshuadutton/c6418a15bf3f51d97590f48ae63eeff1 to your computer and use it in GitHub Desktop.
Vexflow React Native Component using Expo Dom Components
import VexflowComponent, {
NoteConfig,
StaveConfig,
} from "@/components/vexflow-component";
import React from "react";
import { View } from "react-native";
const staveConfig: StaveConfig = {
x: 10,
y: 0,
width: 190,
clef: {
name: "treble",
},
timeSignature: {
timeSpec: "4/4",
},
};
const noteConfigs: NoteConfig[] = [
{ keys: ["c/4"], duration: "q" },
{ keys: ["d/4"], duration: "q" },
{ keys: ["b/4"], duration: "qr" },
{ keys: ["c/4", "e/4", "g/4"], duration: "q" },
];
export default function HomeScreen() {
return (
<View style={{ flex: 1 }}>
<VexflowComponent
width={200}
height={200}
staveConfig={staveConfig}
noteConfigs={noteConfigs}
onRender={(svg) => console.log(svg)}
/>
</View>
);
}
import { FontInfo } from "vexflow";
import VexflowDOMComponent from "./vexflow-dom-component";
import { NoteConfig, StaveConfig } from "./vexflow-helpers";
export interface VexflowComponentProps {
width: number;
height: number;
staveConfig: StaveConfig;
noteConfigs: NoteConfig[];
font?: string | FontInfo;
fontSize?: number;
onRender?: (svg: string) => void;
}
export * from "./vexflow-helpers";
export default function VexflowComponent({
width,
height,
staveConfig,
noteConfigs,
font = "Arial",
fontSize = 10,
onRender,
}: VexflowComponentProps) {
return (
<VexflowDOMComponent
width={width}
height={height}
staveConfig={staveConfig}
noteConfigs={noteConfigs}
font={font}
fontSize={fontSize}
onRender={onRender}
dom={{ style: { width, height } }}
/>
);
}
// Uses Expo DOM Components https://docs.expo.dev/guides/dom-components/
// Needs serializable props, so I created StaveConfig and NoteConfig to pass
// to the component and to create the Vexflow objects.
"use dom";
import { useEffect, useRef } from "react";
import VexFlow, { FontInfo, Formatter } from "vexflow";
import {
NoteConfig,
notesFromConfig,
StaveConfig,
staveFromConfig,
} from "./vexflow-helpers";
export interface VexflowDOMComponentProps {
width: number;
height: number;
staveConfig: StaveConfig;
noteConfigs: NoteConfig[];
dom: import("expo/dom").DOMProps;
font?: string | FontInfo;
fontSize?: number;
onRender?: (svg: string) => void;
}
export default function DOMComponent({
width,
height,
staveConfig,
noteConfigs,
font,
fontSize,
onRender,
}: VexflowDOMComponentProps) {
const divRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const div = divRef.current;
if (div) {
const width = div.clientWidth;
const height = div.clientHeight;
const renderer = new VexFlow.Renderer(div, VexFlow.Renderer.Backends.SVG);
renderer.resize(width, height);
const context = renderer.getContext();
if (font && fontSize) {
context.setFont(font, fontSize);
}
const stave = staveFromConfig(staveConfig, context);
stave.setContext(context).draw();
const notes = notesFromConfig(noteConfigs);
Formatter.FormatAndDraw(context, stave, notes);
const svgElement = div.firstChild as SVGSVGElement;
const svgString = svgElement?.outerHTML;
if (svgString) {
onRender?.(svgString);
}
}
}, [font, fontSize, staveConfig, noteConfigs, onRender]);
return <div ref={divRef} style={{ width, height }}></div>;
}
import {
ElementStyle,
RenderContext,
Stave,
StaveNote,
StaveOptions,
} from "vexflow";
export interface StaveConfig {
x: number;
y: number;
width: number;
options?: StaveOptions;
style?: ElementStyle;
clef?: {
name: string;
size?: string;
annotation?: string;
position?: number;
};
endClef?: {
name: string;
size?: string;
annotation?: string;
};
keySignature?: {
keySpec: string;
cancelKeySpec?: string;
position?: number;
};
timeSignature?: {
timeSpec: string;
customPadding?: number;
position?: number;
};
endTimeSignature?: {
timeSpec: string;
customPadding?: number;
};
}
export interface NoteConfig {
/** Array of pitches, e.g: `['c/4', 'e/4', 'g/4']` */
keys?: string[];
/** The time length (e.g., `q` for quarter, `h` for half, `8` for eighth etc.). */
duration: string;
line?: number;
/** The number of dots, which affects the duration. */
dots?: number;
/** The note type (e.g., `r` for rest, `s` for slash notes, etc.). */
type?: string;
alignCenter?: boolean;
style?: ElementStyle;
stemStyle?: ElementStyle;
ledgerLineStyle?: ElementStyle;
}
export function staveFromConfig(
config: StaveConfig,
context: RenderContext
): Stave {
const stave = new Stave(config.x, config.y, config.width, config.options);
if (config.style) {
stave.applyStyle(context, config.style);
}
if (config.clef) {
stave.addClef(config.clef.name, config.clef.size, config.clef.annotation);
}
if (config.timeSignature) {
stave.addTimeSignature(
config.timeSignature.timeSpec,
config.timeSignature.customPadding
);
}
if (config.keySignature) {
stave.addKeySignature(
config.keySignature.keySpec,
config.keySignature.cancelKeySpec,
config.keySignature.position
);
}
if (config.endClef) {
stave.addEndClef(
config.endClef.name,
config.endClef.size,
config.endClef.annotation
);
}
if (config.endTimeSignature) {
stave.addEndTimeSignature(
config.endTimeSignature.timeSpec,
config.endTimeSignature.customPadding
);
}
return stave;
}
export function notesFromConfig(configs: NoteConfig[]): StaveNote[] {
return configs.map((config) => {
const note = new StaveNote({
keys: config.keys,
duration: config.duration,
line: config.line,
dots: config.dots,
type: config.type,
});
if (config.style) {
note.setStyle(config.style);
}
if (config.stemStyle) {
note.setStemStyle(config.stemStyle);
}
if (config.ledgerLineStyle) {
note.setLedgerLineStyle(config.ledgerLineStyle);
}
return note;
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment