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

@nouisi/orb

v0.1.1

Published

<noui-orb>: the living orange o of the noui mark. A voice surface and loading indicator as a framework-free Web Component.

Readme

@nouisi/orb

<noui-orb> is the living form of the orange o in the noui mark: a voice surface, a loading and processing indicator, and the place assembled interface comes from. It is a framework-free Web Component (Custom Elements v1, Shadow DOM) with no runtime dependencies, about 16 KB gzipped.

The noui logo becomes the orb, which listens, thinks, works through three steps and speaks

Try it: https://noui-orb-demo.pages.dev/lab/orb/ (it will also live at https://noui.si/lab/orb/). The demo has a Get started walkthrough and runs on the public API only.

Status: early release (0.1.x). It is a scripted prototype: it draws and reacts to audio, it does not understand speech. Your app listens and thinks and tells the orb what is happening. Tested in Chrome and Safari; not yet in Firefox. The full reference is docs/SPEC.md.

Quick start

npm i @nouisi/orb
<noui-orb id="orb" gestures intro="auto" style="width: 320px"></noui-orb>

<script type="module">
  import "@nouisi/orb"; // registers <noui-orb>

  const orb = document.getElementById("orb");
  orb.state = "thinking";                               // idle | listening | thinking | working | speaking | error
  orb.addEventListener("orb-statechange", (e) => console.log(e.detail)); // { from, to }

  orb.state = "working";
  orb.progress = { steps: 3, done: 1, current: 0.4 };   // determinate ring; null means the spinning arc
  orb.gesture("nod").catch(() => {});                   // rejects with "suppressed" when gestures are off or rate limited
</script>
  • Size the element from the outside; it fills its box and should be square.
  • Without the gestures attribute, character motion (springs, lean, breath, wait) is off. States, audio and the ring still work.
  • intro="manual" (the default) shows the logo and waits for orb.intro(). Use intro="auto" to play it on load or intro="none" to start as the orb.
  • Importing @nouisi/orb/element gives you the class without registering it.

API

Attributes

| Attribute | Values | Default | Notes | |---|---|---|---| | state | idle listening thinking working speaking error | idle | Same as the state property. | | placement | center room aside | center | Where the orb sits inside its box. | | gestures | boolean | absent (off) | Character motion: springs, lean, breath, wait. States, audio and the ring work without it. | | intro | auto manual none | manual | auto plays the logo intro on first connect, none starts as the orb, manual waits for intro(). | | reduced-motion | auto on off | auto | auto follows prefers-reduced-motion. | | label | string | noui | Used in the accessible name ("noui is listening"). |

Properties

| Property | Type | Notes | |---|---|---| | state | OrbState | Setting it fires orb-statechange. | | placement | Placement | Same as place() without the promise. | | gestures | boolean | | | progress | { steps, done, current? } \| null | Determinate ring while working. null shows the spinning arc, never fake progress. | | config | partial OrbConfig | Tuning (reactivity, swell, attack and release, flow, grain, geometry, placements). Assign a partial; read it back as a full copy. Defaults are in SPEC §13. | | levels | read-only | The smoothed signal the orb is showing: { amp, bright, bass, treble, pace, paceRate }. For your own meters. |

Methods

| Method | What it does | |---|---| | gesture(name, { target?, duration? }) | Plays nod, shake, huh, hop, interrupt, emit, point or wait. Resolves when the motion settles. Rejects with "suppressed" if gestures are off, reduced motion is on or the 1.2 s rate limit drops it. target is an element or DOMPoint (for point); duration is in ms. | | stopWaiting() | Ends a running wait. | | place(placement) | Moves the orb inside its box (650 ms, springy). Resolves when it arrives. | | originRect() | { x, y, r }: the orb's circle in viewport pixels, to animate your interface out of it or into it. | | attachInput(stream \| node) / detachInput() | The user's voice (a MediaStream or AudioNode). Drives listening. | | attachOutput(element \| node) / detachOutput() | The reply's audio. Drives speaking. | | setLevels({ amp, bright?, bass?, treble? }, "input" \| "output") | Push levels instead of attaching audio. Call it every frame or analysis tick; levels older than 300 ms are ignored. | | intro() / outro() | Logo to orb (about 3.4 s) and back (about 1.3 s). |

Events

All bubble and are composed. The payload is in event.detail.

| Event | detail | When | |---|---|---| | orb-statechange | { from, to } | After state changes. | | orb-gesture | { name, phase: "start" \| "end" } | Around every gesture, including automatic ones (the nod before thinking, the hop when a task finishes). | | orb-recall | {} | The user clicked, tapped or pressed Enter or Space on the orb while it was aside. You decide whether to bring it back. | | orb-intro-end, orb-outro-end | {} | A logo transition finished. | | orb-input-silent | { seconds } | Input attached, state listening, and under 3% loudness for 4 s; or the input's track ended (seconds: 0). | | orb-fallback | { reason } | WebGL is unavailable; the CSS fallback orb is in use. |

Theming

The orb's own colours are brand constants and never change. The ring and the mark follow the light or dark scheme, and you can override them with CSS custom properties on the element or any ancestor: --orb-accent, --orb-mark-ink, --orb-ring, --orb-ring-track, --orb-ring-broken.

Accessibility

The element has role="img" and an accessible name that tracks the state ("noui is working, step 2 of 3"). Name changes are not announced, so show the reply as text or captions. It is not focusable, except in aside, where it becomes a button named "Bring back noui". prefers-reduced-motion turns gestures off, makes the intro a 200 ms crossfade and slows the flow to 0.35x.

Microphone and audio

The component never asks for the microphone. You do, and hand it the stream:

const stream = await navigator.mediaDevices.getUserMedia({
  audio: { echoCancellation: false, noiseSuppression: false, autoGainControl: true },
});
orb.attachInput(stream);       // the user's voice (drives `listening`)
orb.state = "listening";

orb.attachOutput(audioElement); // the reply's audio (drives `speaking`); an AudioNode works too

If you analyse audio yourself (a native bridge, server-side audio), push levels instead: orb.setLevels({ amp, bright, bass, treble }, "input" | "output"). The orb fires orb-input-silent when an attached input stays quiet for 4 s or its track ends. Call getUserMedia from a click or tap, and serve the page over HTTPS.

Driving it from a voice pipeline

The component stays dumb on purpose: it has no speech recognition and makes no network calls. Your app owns those and maps its events onto the orb. This is the pattern (vad, stt, tts and backend stand for whatever you use):

vad.on("speechStart", () => {
  if (orb.state === "speaking") { tts.stop(); orb.gesture("interrupt").catch(() => {}); }
  else orb.state = "listening";
});
vad.on("speechEnd", () => { orb.state = "thinking"; });        // the orb nods by itself
stt.on("final", (text) => backend.send(text));

backend.on("progress", (p) => { orb.state = "working"; orb.progress = p; });
backend.on("reply", (r) => {
  if (r.gesture && r.confidence >= 0.7) orb.gesture(r.gesture, { target: r.targetEl }).catch(() => {});
  if (r.ui) { orb.gesture("emit").catch(() => {}); renderCardFrom(orb.originRect(), r.ui); }
  if (r.audio) { orb.attachOutput(r.audio); orb.state = "speaking"; }
});
tts.on("ended", () => {
  orb.state = cardShown ? "idle" : "listening";
  if (cardShown) orb.gesture("wait").catch(() => {});
});

userStartsEditingCard(() => orb.place("aside"));
orb.addEventListener("orb-recall", () => orb.place(cardShown ? "room" : "center"));

Only send a gesture from your backend when it is confident (0.7 or more). A wrong nod is worse than no nod. While an on-device speech model downloads, use the ring as the progress bar: orb.state = "working"; orb.progress = { steps: 1, done: 0, current: bytes / total }.

Using it with a framework

It is a standard custom element, so it works anywhere a custom element does. Set object-valued things (progress, config) as properties, listen to events with addEventListener, and import the package only in code that runs in the browser or is safe to run on the server (see below).

Verified in this repo: plain JavaScript, React 19, and importing and server-rendering under Node. The other frameworks below follow the same custom element rules but have not been tried here.

React 19

Props are set as properties when the element has them, and on<event-name> attaches an event listener, so this works directly:

import "@nouisi/orb";

function Orb({ state, progress }) {
  return (
    <noui-orb
      state={state}
      progress={progress}                       // an object, set as a property
      gestures=""                               // boolean attribute: present means on
      onorb-statechange={(e) => console.log(e.detail)}
      style={{ width: 320 }}
    />
  );
}

Call methods through a ref: ref.current.gesture("hop"), ref.current.originRect().

React 18 and earlier

React 18 turns object props into strings and has no event props for custom elements. Use a ref:

const ref = useRef(null);
useEffect(() => { ref.current.progress = progress; }, [progress]);
useEffect(() => {
  const el = ref.current;
  const on = (e) => console.log(e.detail);
  el.addEventListener("orb-statechange", on);
  return () => el.removeEventListener("orb-statechange", on);
}, []);
return <noui-orb ref={ref} state={state} gestures="" />;

Next.js and other server-rendered apps

The package is safe to import on a server: it only registers the element when customElements exists, so there is no HTMLElement is not defined. The server renders an empty <noui-orb> tag, and the browser upgrades it. In the Next.js App Router, use it from a client component ("use client") as in the React example. I verified importing under Node and rendering with react-dom/server; I have not run a full Next.js app.

Vue 3

Tell the compiler it is a custom element (compilerOptions.isCustomElement: (tag) => tag === "noui-orb"), then use .prop for objects: <noui-orb :state="state" :progress.prop="progress" gestures @orb-statechange="onChange" />.

Svelte

Svelte sets props on custom elements as properties and supports on: for their events: <noui-orb {state} {progress} gestures on:orb-statechange={onChange} />.

Not provided yet

  • Type declarations for the <noui-orb> tag in React JSX, Vue templates and Svelte. The element class and its types are exported (NouiOrbElement, OrbState, OrbProgress, ...), so you can write the declaration for your framework yourself.
  • CommonJS and a plain <script> tag build. The package is ESM only.
  • Thin framework wrapper packages.

Licence

The code is MIT licensed (see LICENSE). The noui name and the noui mark are not licensed for reuse.