npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@miadi/ava8-react

v0.3.1

Published

React components and hooks for ABC notation: engraving, playback, a playback cursor on the visible score, transport and volume, and SVG/print export — headless, unstyled, ESM only, over @miadi/ava8-core and @miadi/ava8-abcjs.

Readme

@miadi/ava8-react

React components and hooks for ABC notation — engraving, playback, a cursor on the score you can see, transport, volume and export — with no CSS and no UI-kit dependency.

<AbcScore> renders a tune. <AbcPlayer> renders it, plays it, and shows where it is playing. useAbcScore, useAbcPlayer, useAbcCursor, useAbcExport and useGlyphs are the same capabilities without any markup at all, for when you want to build the component yourself.

Everything musical lives one and two layers down: @miadi/ava8-abcjs owns the abcjs binding (one renderer, one playback lifecycle, a pluggable loader), and @miadi/ava8-core owns the ABC text model, notes↔MIDI, the instrument registry and the swappable glyph cosmology. This package restates none of them — it is the React lifecycle around them.

Install

npm i @miadi/ava8-react

ESM only. There is no CommonJS build and no require() entry point: the package is "type": "module" with a single import condition. In a CommonJS file, reach it with await import('@miadi/ava8-react').

react and react-dom are peer dependencies

"peerDependencies": {
  "react": ">=18",
  "react-dom": ">=18"
}

They are not dependencies and never will be: two copies of React in one tree is a broken hooks dispatcher, and the version is the host app's decision. Install React yourself; this package uses the one you already have. It is tested against React 19 and uses no API newer than React 18.

@miadi/ava8-abcjs and @miadi/ava8-core come along as ordinary dependencies — you do not install abcjs yourself.

0.2.0

Breaking: durationdurationSeconds. UseAbcPlayerResult.duration is now UseAbcPlayerResult.durationSeconds, which is what Ava8Player calls it one layer down. This layer had quietly renamed it on the way up, and a rename that exists in one layer and not the next is a trap for anyone reading both. Rename the property at your call sites; nothing else about it changed.

-const { duration } = useAbcPlayer(abc)
+const { durationSeconds } = useAbcPlayer(abc)

Additive in the same release:

  • The playback cursor works. <AbcPlayer> now hands its engraved tune to the player, so timing events address the score on the page. useAbcCursor writes a class onto the notes as they sound, on by default in <AbcPlayer>.
  • The score container is split in two. abcjs mutates the class and style of whatever element it paints into, so it now gets an inner div of its own and React keeps the outer one.
  • Transport and volume: pause, resume, seek, setVolume, isPaused, currentMs, volume.
  • useAbcExport: toSvgString(), toSvgBlob(), print().
  • renderError on <AbcScore>, and warnings / validation reported apart.
  • reflowOnResize — real responsive engraving.
  • scoreClassName / scoreStyle on <AbcPlayer>, which previously passed neither to the score inside it.
  • Every RenderOptions member is forwarded now, not eight of them.

This package ships no CSS and no UI kit

There is no styles.css in the published files, no import './x.css' anywhere in the build, and the only class name this package ever writes is the playback cursor's, which does nothing until you style it. The build imports exactly four things: react, react/jsx-runtime, @miadi/ava8-abcjs and @miadi/ava8-core — a test asserts that list, so it cannot drift.

Concretely, this package will never pull in next/*, @radix-ui/*, tailwindcss, lucide-react, sonner, react-hook-form, or anything from a components/ui/ folder. The prototype's renderers both hard-coded className="w-full overflow-x-auto bg-black/30 p-2 rounded-lg", which is one Tailwind app's opinion compiled into a library. Here every component takes className and style, and that is the whole styling story.

Data attributes are the only thing on the DOM you did not put there — they exist so a host stylesheet can target state without a class-name contract:

| attribute | on | values | | --- | --- | --- | | data-ava8-score | the score wrapper — yours | always present, empty | | data-ava8-status | the score wrapper | pending | ready | error | | data-ava8-canvas | the inner div — abcjs's | always present, empty | | data-ava8-player | the player wrapper | always present, empty | | data-ava8-playing | the player wrapper | empty while sounding, absent otherwise |

Who owns which element

abcjs writes display:inline-block; position:relative; width:100%; padding-bottom:<aspect>%; overflow:hidden onto the element you hand it, and appends abcjs-container to its class. React does not re-assert attributes it did not change, so an element shared between the two ends up with the consumer's classes and abcjs's classes in one attribute and a phantom aspect ratio that outlives every teardown.

So <AbcScore> renders two elements:

<div class="…yours…" style="…yours…" data-ava8-score data-ava8-status="ready">
  <div data-ava8-canvas class="abcjs-container" style="display:inline-block;…">
    <svg …>…</svg>
  </div>
</div>

The outer one is React's, byte for byte — a test asserts its class and style are unchanged across a full render/teardown cycle. The inner one is abcjs's outright, and is released on unmount and before every re-render through releaseScoreContainer, which puts back the class and style that clearing children cannot.

If you call useAbcScore(ref, …) yourself, ref is the abcjs element: give it a div you have nothing else planned for.

Every module is a client module

Every emitted file opens with "use client", so a Next.js App Router consumer imports these components from a Server Component with no wrapper of their own. Nothing here touches window or document at module scope, so an SSR pass that imports the package does not throw — it simply renders the empty container and engraves after hydration.

<AbcScore>

import { AbcScore } from '@miadi/ava8-react'

<AbcScore
  abc={"X:1\nT:The Spiral Awakens\nM:4/4\nL:1/8\nK:Cmin\nC2E2G2c2|G2E2C4|"}
  className="my-score"
  responsive
  wrap={{ preferredMeasuresPerLine: 4 }}
  reflowOnResize
  onRendered={(result) => console.log(result.svg)}
  onError={(message) => setBanner(message)}
  renderError={(message, validation) =>
    validation.valid ? <p>Could not draw this: {message}</p> : <ul>{validation.errors.map(…)}</ul>
  }
/>

AbcScoreProps extends the whole of RenderOptions from @miadi/ava8-abcjsresponsive, scale, staffWidth, addClasses, padding, clickListener, tuneIndex, format, selectionColor, dragColor, wrap, tablature, visualTranspose, ariaLabel, print, strict, abcjsParams, source — and adds abc, className, style, reflowOnResize, onError, onRendered and renderError.

  • Every option is forwarded. Until 0.2.0 five of them were, by name, and the rest were accepted from you and dropped. The options object is now compared and forwarded whole, and a test fails if a member of RenderOptions stops taking part — so the next option added one layer down is covered the day it lands.
  • A failed engraving never throws into your tree. It sets data-ava8-status="error", calls onError with the message, and renders renderError if you gave one.
  • warnings and validation are different things. validation is core's structural verdict on the ABC and is always present, even before the first engraving and even when engraving failed. warnings is abcjs's parse output. validation.valid === false is "this is not a tune"; validation.valid === true with an error message is "the tune is fine, abcjs is not here".
  • Changing abc tears the previous engraving down first, so a slow or failing re-render can never leave the old tune on screen.
  • Option objects are compared by meaning, not identity: padding={{ top: 0 }} written inline is a new object every render and does not cause a re-engrave. padding={8} and padding={{top:8,right:8,bottom:8,left:8}} are one engraving.

reflowOnResize

abcjs's responsive: 'resize' scales one fixed engraving to fit its container — it never re-breaks the lines, so a sixteen-bar tune on a phone becomes microscopic rather than becoming four lines. reflowOnResize installs a ResizeObserver and re-engraves with staffWidth set to the container's actual width, which is the only way to get a layout made for that width. Pair it with wrap for line reflow. It costs one extra engraving on mount, because the observer's first callback is the first time the width is known.

<AbcPlayer>

With no children, you get one unstyled, semantic <button> — no class names, no icon library:

import { AbcPlayer } from '@miadi/ava8-react'

<AbcPlayer abc={tune} instrumentId="triangle" tempo={96} scoreClassName="score" />

With children, it is a render prop over the whole player state, so the transport is yours:

<AbcPlayer abc={tune} instrumentId="sine" tempo={84} volume={0.8}>
  {({ isPlaying, isPaused, toggle, pause, resume, seek, ready, durationSeconds, error }) => (
    <MyTransport
      label={isPlaying ? 'Stop' : 'Play'}
      onPress={toggle}
      onHold={isPaused ? resume : pause}
      onScrub={(pct) => seek(pct, 'percent')}
      disabled={!ready}
      seconds={durationSeconds}
      error={error}
    />
  )}
</AbcPlayer>

AbcPlayerProps extends AbcScoreProps and adds instrumentId, program, tempo, soundFontUrl, volume, autoPlay, showScore, onEnded, scoreClassName, scoreStyle, highlightClassName and children. className/style dress the player wrapper; scoreClassName/scoreStyle dress the score inside it, which until 0.2.0 could not be styled at all.

One engraving, not two. <AbcPlayer> waits for its <AbcScore> to finish, then hands that engraved tune to the synth as PlayerOptions.visualObj. Before 0.2.0 the two ran independently off the same string, abcjs parsed the tune twice, and every timing event pointed at a copy that was never on the page.

The playback cursor

<AbcPlayer abc={tune} highlightClassName="ava8-cursor" />

On by default — highlightClassName defaults to 'ava8-cursor'. It is free until you style it, because this package ships no CSS. One line does that:

.ava8-cursor { fill: #e26d5c; stroke: #e26d5c; }

Standalone, over a player you built yourself:

import { useAbcPlayer, useAbcCursor } from '@miadi/ava8-react'

function Transport({ abc, visualObj }) {
  const player = useAbcPlayer(abc, { visualObj })
  useAbcCursor(player, { className: 'ava8-cursor' })
  return <button onClick={player.toggle}>{player.isPlaying ? 'Stop' : 'Play'}</button>
}
  • The class lands on the <g class="abcjs-note"> nodes of the engraving on the page, moves with playback, and comes off on stop and on unmount.
  • Pausing keeps it where the music stopped; stopping removes it.
  • Building the cursor is what turns abcjs's timing loop on. useAbcPlayer on its own — no cursor, no onEvent, no onBeat — never asks abcjs for one, so a player nobody is watching costs no animation frames.
  • useAbcCursor needs the engraved tune to be the one playing. Inside <AbcPlayer> that is arranged for you; standalone, pass visualObj from your score's RenderResult into useAbcPlayer.

useAbcPlayer

The playback half of the prototype's EchoComposer, with none of its form:

import { useAbcPlayer } from '@miadi/ava8-react'

function Transport({ abc }: { abc: string }) {
  const { play, stop, toggle, pause, resume, seek, setVolume,
          isPlaying, isPaused, currentMs, durationSeconds, volume, ready, error } =
    useAbcPlayer(abc, { instrumentId: 'triangle', tempo: 96, onEnded: () => log('done') })

  return (
    <button onClick={toggle} disabled={!ready}>
      {isPlaying ? `Stop (${durationSeconds}s)` : 'Play'}{error}
    </button>
  )
}
  • The player is destroyed on abc change, not only on unmount. Change the tune while it is sounding and the old synth is stopped and destroyed before the new one is built.
  • currentMs is a ref-backed getter, not state. abcjs's timing fires on every animation frame; publishing that as state would re-render your subtree sixty times a second. Reading it never schedules a render and never costs one. It is refreshed by every state change the hook does publish — play, pause, stop, seek, ready. For a moving readout, drive your own requestAnimationFrame and read currentMs each frame.
  • setVolume and the volume option move a playing tune's level through a gain node spliced in front of the speakers. Neither rebuilds the synth. volume is reported back already clamped to 0..1, so a UI shows the level it will get.
  • seek(position, units) takes 'seconds' (default), 'beats' or 'percent', and moves the audio and the cursor together.
  • play() pressed before the soundfont finishes loading is remembered, not dropped.
  • enabled: false builds no player at all and holds ready at false. This is how <AbcPlayer> waits for its engraving rather than building a synth it would immediately throw away.
  • No state is set after unmount: an internal liveness flag is cleared by an effect that is registered first, so it is already false by the time the player's own cleanup runs.
  • Callback props (onEnded, onError, onEvent, onBeat) never take part in a dependency array — a callback changing identity must not destroy a playing tune.

useAbcScore

For when you own the container:

import { useRef } from 'react'
import { useAbcScore } from '@miadi/ava8-react'

function Score({ abc }: { abc: string }) {
  const ref = useRef<HTMLDivElement>(null)
  const { ready, error, result, warnings, validation } = useAbcScore(ref, abc, {
    responsive: true,
    reflowOnResize: true,
  })
  return (
    <figure>
      <div ref={ref} />
      {validation.valid ? null : <ul>{validation.errors.map((e) => <li key={e}>{e}</li>)}</ul>}
    </figure>
  )
}

The element behind ref belongs to abcjs — its children, its class and its style. Render nothing of your own into it.

useAbcExport

import { useAbcExport } from '@miadi/ava8-react'

const { toSvgString, toSvgBlob, print } = useAbcExport(result)

<button onClick={() => download(toSvgBlob())}>Download SVG</button>
<button onClick={print}>Print</button>

Works from the live <svg> a render already produced — never a second engraving, which could differ from the one on screen — and never mutates it: everything happens on a clone. The markup it returns stands on its own: xmlns present, width/height recovered from the viewBox, and abcjs's position:absolute removed, so it survives being dropped into an <img src> or a file. print() writes the score into an off-screen <iframe> and prints that, so no popup blocker is involved and the app around the score is not printed with it. With nothing engraved, all three report nothing rather than throwing.

useGlyphs

One glyph cursor, reading through @miadi/ava8-core's accessors:

import { useGlyphs, AbcPlayer } from '@miadi/ava8-react'

function Journey() {
  const { glyph, glyphId, ids, next, prev, setGlyph } = useGlyphs()
  return (
    <>
      <h2>{glyph.emoji} {glyph.name}</h2>
      <AbcPlayer abc={glyph.abcNotation} />
      <button onClick={prev}>◀</button>
      <button onClick={next}>▶</button>
      {ids.map((id) => <button key={id} onClick={() => setGlyph(id)}>{id}</button>)}
    </>
  )
}

next() advances and wraps; prev() retreats and wraps. The direction words come from the prototype's swipe semantics: next() is core's nextGlyph(id, 'left'), a right-to-left swipe.

The ids are re-read from core on every render, so a loadMusicData() cosmology swap is reflected on the next render — including a swap to a cosmology with a different number of glyphs and no Ava8 glyph in it. If the current glyph is not in the new cosmology, the cursor resolves to its first glyph. ids keeps its identity while the cosmology does not change, so it is safe in a dependency array.

A cosmology with no glyphs at all has no honest Ava8Glyph to return, so core's UnknownGlyphError surfaces.

Ava8Provider / useAva8

One seat for the three decisions the prototype hard-coded in three different files:

import { Ava8Provider } from '@miadi/ava8-react'
import myCosmology from './my-cosmology.json'

<Ava8Provider
  source={{ strategy: 'cdn' }}
  soundFontUrl="https://my.cdn/soundfonts/"
  musicData={myCosmology}
>
  <App />
</Ava8Provider>
  • source — where abcjs comes from, for every descendant that does not name its own. A prop always beats the provider.
  • soundFontUrl — the default soundfont for descendant players.
  • musicData — swaps the glyph cosmology by calling core's loadMusicData() during render, before children render, so a child's first useGlyphs() pass already sees the new cosmology instead of flashing the built-in one. On unmount the shipped cosmology is restored via resetMusicData().

useAva8() returns { source?, soundFontUrl? }, and outside a provider it returns an empty object rather than throwing.

Testing without a network

Every hook and component accepts an abcjs source. strategy: 'inject' hands the library your own abcjs, which is how this package's own 71 tests run under jsdom with no network and no real synth:

const fake = { renderAbc: (el, abc, params) => [/* visualObj */], synth: { CreateSynth } }
render(<AbcScore abc={tune} source={{ strategy: 'inject', abcjs: fake }} />)

Absorbed from

Prototype: jgwill/Ava8@3cf080a (branch main), read only. Nothing in that repository was modified.

| This package | Came from | What changed | | --- | --- | --- | | AbcScore | components/abc-notation-renderer.tsx (84 lines, variant A) and components/music-notation.tsx (37 lines, variant B) | Two components that rendered the same thing and disagreed became one. Variant A's next/script CDN load and variant B's import("abcjs") became the source prop over @miadi/ava8-abcjs's loader. | | AbcScore teardown | abc-notation-renderer.tsx:36-70, music-notation.tsx:12-34 | Both cleared the container at the start of the next render (innerHTML = "" inside the effect) and neither had a cleanup function. Teardown now happens in the effect cleanup — on abc change as well as unmount — and through releaseScoreContainer, because clearing children leaves abcjs's class and style behind. | | AbcScore error path | abc-notation-renderer.tsx:29-33, 65-69, 80 | Variant A's onError prop survives; its inline <div className="text-red-500"> does not — renderError is the seat for it, and the message arrives with core's validation verdict beside it. Variant B had no error path at all: a rejected import left the container silently empty. | | container styling | abc-notation-renderer.tsx:81, music-notation.tsx:36 | className="w-full overflow-x-auto bg-black/30 p-2 rounded-lg" was hard-coded in both. Now className/style passthrough on an element React keeps, and the package ships no CSS. | | useAbcPlayer | components/echo-composer.tsx:29-56 (synth ref + state), 86-120 (stopAllAudio + unmount cleanup), 187-248 (togglePlay) | The playback half of a 403-line form. The form half — react-hook-form, sonner toasts, lucide-react icons, @/components/ui/*, the melody-fetch useEffect and the tempo slider — stayed in the app, where it belongs. | | player teardown on tune change | echo-composer.tsx:86-90 | The prototype's cleanup was useEffect(() => () => stopAllAudio(), [])unmount only. Changing the tune mid-playback left the old synth sounding. Now the player is destroyed in the effect cleanup keyed on the tune. | | the defensive stop sequence | echo-composer.tsx:93-120 | Preserved verbatim in @miadi/ava8-abcjs (synth.stop, synth.midiBuffer.stop, ABCJS.synth.stopAll, each in its own try/catch) — it is the residue of f75b0e1 fix: properly stop audio playback in Echo Composer. | | the end timer | echo-composer.tsx:229-235 | setTimeout(endsAt * 1000) whose handle was discarded on the line that created it. Fixed one layer down: the timer is held and cleared in stop()/destroy(). | | AbcPlayer transport | echo-composer.tsx:351-368 | A @/components/ui/button with two lucide-react icons and a bg-red-600 hover:bg-red-700 conditional became a render prop, falling back to one bare <button>. | | the master volume | app/page.tsx:86-88 | A GainNode and a mute toggle that did not survive the first absorption pass. Reached here through volume / setVolume, applied to a playing tune. | | instrument → MIDI program | echo-composer.tsx:251-266 | Lifted to @miadi/ava8-core's instrumentMidiProgram — it was data trapped inside a React component. Reached here through PlayerOptions.instrumentId. | | soundfont URL | echo-composer.tsx:215 | Hard-coded inside a click handler. Now Ava8Provider's soundFontUrl, AbcPlayer's prop, or the default in @miadi/ava8-abcjs. | | useGlyphs | lib/music-data.ts:52-63 (getNextGlyph), lib/gesture-utils.ts:42-53 (a second getNextGlyph with ["spiral","puzzle","feather","crystal"] hard-coded), and app/page.tsx importing the hard-coded one | Three forks of one idea, one of which a music-data.json swap could not reach. This hook adds no fourth copy: it calls core's nextGlyph(). | | swipe semantics | hooks/use-swipe.tsx, lib/gesture-utils.ts:19-39 | Gesture detection stays in the app — it is input handling, not music. useGlyphs keeps the vocabulary it fed: next() is a right-to-left swipe. | | useAbcCursor | nothing — new | The prototype had no cursor. Neither did 0.1.0 of this package, and it could not have had one: the score and the player engraved the tune separately. | | useAbcExport | nothing — new | The prototype could not get a score back out of the page. | | Ava8Provider | nothing — new | The prototype had no seat for these decisions: the loader was hard-coded in two components, the soundfont in a click handler, and the cosmology in a module-level import. |

Deviations from the frozen contract

All additive, all documented at their definition:

  1. useAbcScore(ref, …) takes RefObject<HTMLElement | null> where the contract writes RefObject<HTMLElement>. Under @types/react 19, RefObject<T> is { current: T }, so the bare form would reject the useRef<HTMLDivElement>(null) every consumer actually writes.
  2. AbcPlayerProps adds an optional soundFontUrl. Without it, changing the soundfont for a single player would mean wrapping it in its own Ava8Provider.
  3. UseAbcPlayerOptions adds enabled. Without it <AbcPlayer> cannot wait for its own engraving, and the cursor's whole mechanism — reusing the visible tune — has nothing to reuse.
  4. UseAbcScoreOptions adds reflowOnResize.

Tests

npm test    # builds, then runs node --test over test/*.test.mjs

71 cases under jsdom + react-dom/client + act, driving a fake abcjs through strategy:'inject'. The fake paints real SVG, keeps real synth state, holds a real gain node and mutates its container exactly the way abcjs does — so the assertions are about engraved DOM, about which node carries the cursor, about whether anything is still sounding and about what level it is sounding at. Never about whether a function was called.