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

@xsynaptic/sonic-ui

v0.29.0

Published

Native custom elements for audio interfaces, skinned with CSS custom properties

Readme

@xsynaptic/sonic-ui

Native custom elements for audio interfaces. Each control renders in the light DOM, skinned with --sonic-* tokens (CSS custom properties). No framework, no runtime dependencies.

Early and unstable: any 0.x release may break the API, and the changelog is where a control's behaviour is described. This README holds setup and the contracts every control shares. For the rest, read the package:

  • Properties and methods: dist/dev/elements/<control>.d.ts. Every attribute has a property of the same name in camel case.
  • Tokens and their defaults: dist/styles/<control>.css and dist/styles/material/, where each token is read as var(--sonic-…, default).
  • Working markup for every control: playground/src/components/specimens/ in the repository.

Setup

pnpm add @xsynaptic/sonic-ui
import '@xsynaptic/sonic-ui/define';
@layer base, sonic, components;

@import 'reset.css' layer(base);
@import '@xsynaptic/sonic-ui/controls.css';
@import '@xsynaptic/sonic-ui/skins/slate.css';
<body class="sonic-skin-slate">
	<sonic-dial aria-label="Cutoff" value="40"></sonic-dial>
</body>
  • Every sheet sits in the sonic layer. A rule outside any layer beats every layered one, so an unlayered reset strips padding, borders and type from the controls: import a reset into a layer declared before sonic, and keep your own rules in one declared after.
  • A skin sheet does nothing until its class, sonic-skin-<name>, is on an ancestor. The skins are slate, amber, ivory, lime and flat. With no skin a control is bare, drawn in the text colour around it.
  • ./define registers every control and ./define/<control> registers one. . exports the classes and registers nothing.
  • Under a bundler that sets the development condition, as Vite's dev server does, the controls warn in the console about a missing sheet, a reset, a box property on a host and a child they do not use. Read the console before debugging.

Contracts

  • The host is display: contents and the control is an element inside it. A width, margin or transform on the host does nothing: size a control with its token (--sonic-dial-size, --sonic-slider-length), set on the host or any ancestor, and place it with its parent's layout.
  • The split is the one host with a box: <sonic-split> is inline until given a display, and laying it out is yours.
  • Register the classes under their stock tag names (sonic-dial, sonic-split). The sheets and the split key on them, so a split under another tag finds no members.
  • Name every control with aria-label or aria-labelledby on the host, or with a <label> for a form-associated one.
  • Every control that holds a value the user sets is form-associated, as a native input is, and that is deliberate: given a name it submits, a form reset returns it to its attributes, a disabled <fieldset> disables it, and a <label> names it. With no name it submits nothing. A value control submits its number and a segmented control its value; a latching button, and a switch or toggle with two positions, submit their value only while on, as a checkbox does; an XY pad and a region submit two fields (name.x and name.y, name.start and name.end). A momentary button and the keyboard are form-associated only so a disabled <fieldset> and a <label> reach them, and submit nothing; neither do the displays, the envelope or the split.
  • A boolean attribute is on when present, whatever its value, so pressed="false" latches a button. From a framework pass undefined, never false.
  • A control uses only the children it documents and adds one child of its own. Children it draws are copied, so a copy has none of your listeners; keep them static. Under a hydrating framework, register the definitions once hydration is done (in React, from an effect).
  • Events bubble. One that reports a value carries no detail: read the property when one fires. One that reports what no property holds, as a note going down does, carries it in detail and is composed, so it is heard outside a shadow root. A control knows nothing about playback, so wire its events to your own state and write the property back.
  • A value control fires input as its value moves and change once when a gesture ends. A drag owns the value: a write to value during one is dropped.
  • formatValue and parseValue are properties holding functions. Once parseValue is set it has to read what formatValue writes, or typed entry opens on text it cannot take.
  • To show a control without taking input, set inert on the host. It leaves the tab order and the accessibility tree and ignores presses, and it keeps its colours and keeps drawing; disabled dims it and is announced as disabled.
  • A display takes its signal as data, and nothing in the library fetches, decodes or opens an AudioContext.
  • A state a control sets itself is an attribute on its drawn part, data-sonic-<state>, selected as .sonic-slider[data-sonic-dragging].
  • The LED, the panel, the ring and the screen have no behaviour, so they are classes on your own elements (sonic-led, sonic-panel, sonic-ring, sonic-screen). A control draws no glass of its own: wrap one in a screen for the encased look.

Skinning

A skin is one rule that sets tokens on a class; copy dist/skins/flat.css to start one. Tokens, hook classes (.sonic-dial), data-sonic-* attributes and states are the public surface. Anything starting --_sonic- is private and changes without notice.

  • Set tokens, not rules on hook classes. If a look needs a hook-class rule, a token is missing.
  • A token ending in -ratio is a unitless share of the control's size, so one size token scales the whole control. A length token takes any CSS length except a percentage.

Loading less

controls.css loads everything. To load less, import each control's sheet and the material sheets it needs, in any order:

@import '@xsynaptic/sonic-ui/material/core.css';
@import '@xsynaptic/sonic-ui/material/lens.css';
@import '@xsynaptic/sonic-ui/material/groove.css';
@import '@xsynaptic/sonic-ui/meter.css';

| Sheet | Needs from material/ | | --------------- | ----------------------------------------------- | | button.css | core, cap, well, keycap | | dial.css | core, cap, glass, readout, value, arc, scale | | envelope.css | core, glass, readout, reticle | | keyboard.css | core, cap | | led.css | core, lens | | meter.css | core, lens, groove | | number.css | core, cap, value | | panel.css | core | | region.css | core, cap | | ring.css | core, arc | | screen.css | core, glass, pane, well | | segmented.css | core, cap, well, keycap | | slider.css | core, cap, glass, readout, value, groove, scale | | spectrum.css | core | | switch.css | core, cap | | ticker.css | | | toggle.css | core, cap, well, keycap | | waveform.css | core, cap, glass, readout, value | | wavestrip.css | core, cap, glass, readout, value | | xy.css | core, cap, glass, readout, reticle |

An LED inside a control also needs led.css, and a button inside a ring needs button.css. Only a missing core.css or control sheet is warned about, so check a control that draws wrong against this table.

Tested in current Chrome, Firefox and Safari. Known to work from Chrome 111, Safari 16.4 and Firefox 128; older versions are best effort. No control mirrors for right-to-left. MIT licensed.