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

@verajs/core

v0.3.1

Published

A user-friendly JavaScript framework for effortless reactive state and rendering using web components.

Readme

@verajs/core

The heart of VeraJS: reactive state, an effect system, template tags, and the lifecycle glue that ties them to a custom element. 3.26 KB gzipped, no base class, no build step required, and one dependency — @verajs/inserts, the extension registry, which the production bundle inlines.

There is no Component to extend and no compiler to run. A VeraJS component is a custom element that calls init() and then render(); everything reactive follows from the store it reads.

npm i @verajs/core @verajs/renderer

Core does not write to the DOM itself — a renderer does, and it is a separate install. That is the one piece of wiring VeraJS asks for: wire([renderer]), once, at your app entry.

It is also what makes the renderer replaceable — a string renderer for tests, or your own — but that is a door, not a step. @verajs/renderer and core's html need nothing configured between them. (The seam is real and continuously asserted — tests/foreign-renderer.test.mjs drives a foreign renderer through it — but no alternative renderer is offered as a supported mode.)

A component, whole

import { init, createStore, render, wire, html, useEffect } from '@verajs/core';
import { renderer } from '@verajs/renderer';

wire([renderer]);   // once, at your app entry, before any component defines itself

customElements.define(
  'click-counter',
  class extends HTMLElement {
    connectedCallback() {
      init(this, { mode: 'open' });               // shadow DOM; omit the second argument for light DOM
      const state = createStore({ count: 0 });

      useEffect(() => {
        document.title = `${state.count} clicks`; // re-runs whenever what it read changes
      });

      render(() => html`<button @click=${() => state.count++}>Clicked ${state.count} times</button>`);
    }
  }
);

document.body.append(document.createElement('click-counter'));

Nothing here declares a dependency. render and useEffect subscribe to whatever they read while they run, so a write to state.count schedules exactly the work that read it.

Props — what a parent passes in

A parent binds properties; the component reads them off this. Nothing is declared on either side — no static properties, no props argument to init():

import { init, render, wire, html } from '@verajs/core';
import { renderer, renderInto } from '@verajs/renderer';
import { props } from '@verajs/renderer/spread';

wire([renderer]);

customElements.define(
  'order-summary',
  class extends HTMLElement {
    connectedCallback() {
      init(this, { mode: 'open' });
      render(() => html`<p>${this.customer} — ${this.items.length} items</p>`);
    }
  }
);

renderInto(
  html`<order-summary ${props({ customer: 'Ada', items: [{ sku: 'a' }, { sku: 'b' }] })}></order-summary>`,
  document.body
);

init() adopts what the parent's property bindings delivered: each bound key becomes a store-backed accessor on the element, so a read in a render is tracked and the parent's next commit re-renders — reactivity in both directions, with values arriving by identity (an array, a Date, a store or a ref() passed down stays itself, and stays live). Three things worth knowing:

  • Values come from property bindings.date=${…} in a template, props({ date }) in either surface, or a sigil-keyed spread() bag. An attribute (date="…") is a string in markup, not a prop; the renderer README draws the full property/attribute line.
  • Lazy modules are safe. A value bound before the component's module ran would be destroyed by class-field initializers at upgrade; the renderer records it and init() re-applies — a bound value outranks a class default, in both field spellings (item; and item = default).
  • A prop's default belongs where the prop is born: bind props({ date: date ?? defaultDate }) in the parent, or read this.date ?? defaultDate in the component — a class field initializer is not a default for a bound key, because bound always wins.
  • A class that declares its own accessors keeps them. A get item()/set item() pair receives bound values through the setter — adoption never shadows it — which also means the pair owns its reactivity: back it with your own store (set item(v) { this.#state.item = v }) and later commits re-render exactly as adopted props do. A getter with no setter refuses the binding by name in development instead of silently losing the value.
  • SSR delivers them too. Under @verajs/ssr, a property bound on a rendered component tag reaches that child's server render by identity, so the server's output comes from the same data the client render gets.

State

| | | | --- | --- | | createStore(obj) | deep reactive proxy — nested objects are tracked too | | ref(value) | a deep reactive box for a single value, read and written as .value | | shallowRef(value) | .value is tracked; the contents are not proxied | | untrack(fn) | read current state without subscribing to it | | deps(...values) | touch values explicitly, to register them as dependencies | | store._delete() | sever every subscription for an object store at once |

Reactive Map, Set, WeakMap and WeakSet need @verajs/store/collections: put one in a store, wire that, and mutating methods notify like any other write. Without it core says so the first time one is read.

import { createStore, ref, shallowRef, untrack, deps } from '@verajs/core';

const state = createStore({ filter: 'all', rows: [] });
const focus = ref(null);                    // read and written as focus.value, deeply tracked
const frame = shallowRef(new Float32Array(64)); // .value tracked; the contents deliberately not

const total = () => untrack(() => state.rows.length);  // read without subscribing
deps(state.filter);                          // subscribe explicitly, without using the value yet

One more, for servers: setStaticStores(true) makes every store created from then on a plain object — no proxy, no tracking, reads at raw property speed. It exists for @verajs/ssr, which turns it on around a render that declared itself static; in a browser it would give you a framework that never updates (development throws on any write to such a store to say so).

import { setStaticStores } from '@verajs/core';
setStaticStores(true);   // server-side, around a static render — never in a browser

What "deep" reaches, and what it does not

A store proxies plain objects, arrays, class instances, Object.create(null) objects, and the four collections. Everything else is handed back exactly as it was put in:

Date, RegExp, Promise, Error, URL, URLSearchParams, typed arrays, ArrayBuffer, DataView, functions and DOM nodes.

That is deliberate, and it is the same reason collection methods have to be re-bound: these types carry state in internal slots rather than in properties, so a proxy cannot see a change and in several cases cannot even be called on one. Reading state.when gives you the real Date, and state.when.setHours(9) changes it — but nothing re-renders, because no property was written.

Replace them instead of mutating them, which is what makes the change visible:

state.when = new Date(state.when.setHours(9));   // a write to `when`, so it renders
state.pixels = new Uint8Array(next);             // not state.pixels[0] = …

There is no warning for this. A Date read to format it is far more common than a Date read to mutate it, so a warning would be noise on the ordinary case — which is why it is written down here instead.

A frozen source object stays frozen

A store is a proxy, and a proxy has to respect its target's rules. createStore(Object.freeze(…)) reads fine and throws on any write, because JavaScript says the object is not writable and no amount of proxying changes that. The same goes for a sealed object gaining a new key, an object under Object.preventExtensions, a property defined writable: false, and a getter with no setter — and it applies to a frozen object nested inside an ordinary store, which is the way it usually turns up: a constants table sitting in state.

Everything that is not forbidden works, which is the larger half. Sealed objects take writes to existing keys, setters run with this bound through the proxy, class instances keep their prototype getters, Object.create(null) objects and symbol keys both round-trip.

In development the error names the rule that refused — "the object is frozen, so n cannot be changed". In production you get the engine's own TypeError: 'set' on proxy: trap returned falsish, which is a message about the proxy's internals; the development build exists to tell you what it actually means. It throws either way.

Adding and removing keys counts as a change. A component that enumerates — Object.keys, for…in, { ...state.filters }, JSON.stringify, or key in state.form — depends on the set of keys rather than on any one of them, and hears about a key arriving or leaving. That is what makes state.byId[newId] = row and Object.assign(state.filters, patch) render, and it is checked as a matrix: every container kind crossed with every way of mutating it, against the data itself (tests/core-reactivity-matrix.test.mjs).

Use shallowRef for list data. Putting 1 000 row objects through createStore proxies every one of them, and reading every field back through those proxies measures 20–30× the cost of the same read on a plain array (three runs, 2026-09-05 — an earlier pass of this doc said 60×, which the store has since outgrown). When rows are replaced rather than mutated — which is the usual case — shallowRef is the right tool.

Effects

| | Runs | Batching | | --- | --- | --- | | useLayoutEffect | before render | coalesced, microtask | | useEffect | after render | coalesced, animation frame | | useSyncEffect | immediately on every change | not batched |

All three take (callback, element?) and treat a returned function as cleanup — run before the next pass, and on element removal. No disconnectedCallback is needed for it; if the component has one of its own, it still runs first.

useEffect(() => {
  const id = setInterval(tick, 1000);
  return () => clearInterval(id);
});

useLayoutEffect(() => {
  // runs before the render pass commits — measure here, and writes cannot cause a visible flash
  height = list.getBoundingClientRect().height;
});

The difference between coalesced and sync is what they observe:

state.n = 1; state.n = 2; state.n = 3;
// useEffect     → one run, sees 3
// useSyncEffect → three runs, sees 1, 2 and 3

useSyncEffect can infinite-loop if it unconditionally writes state it also reads. Guard the write, or use useEffect. In development the recursion is stopped and named at depth 50.

useEffect and a template can loop too, and there the loop is real but not always a mistake: the default scheduler is an animation frame, so an effect that writes what it reads simply runs once per frame — which is also how you write an animation. So development warns and does not stop it, after 50 consecutive frames in which the pass fed itself:

[vera] useEffect has re-run for 50 consecutive frames because it writes state it also reads …

A write that lands outside the pass — from your own requestAnimationFrame, a timer, an event — never trips it at any threshold, because the count resets on the first pass that does not feed itself. Only a pass whose own body writes what it reads climbs. If that is deliberate, say so:

init(this);
allowRenderLoop(this);           // an animation: one store write per frame, on purpose
useEffect(() => { state.t = state.t + 1 });

allowRenderLoop(element) silences the warning for that component, and is a no-op in production — where none of this exists.

Every callback receives a signal describing the change: signal.prop, signal.value, signal.prevValue, and on coalesced runs signal.changed — a Map of every property in the batch, holding its value at the start and at the end.

Rendering

| | | | --- | --- | | init(element, shadowProps?) | call first in connectedCallback. { mode: 'open' } for shadow DOM — see ARIA and the shadow boundary | | render(template?, ...args) | draw, and commit the setup. See below | | html | the template tag. @verajs/renderer takes what it produces with no configuration | | svg / mathml | for content inside <svg> / <math> | | css | for static styles, with @verajs/styles | | mount() | commit the setup for a component that draws nothing | | useRender(template, element, ...args) | the lower-level half of render, for driving a render outside the setup window | | wire([renderer]) | choose what writes to the DOM | | setRenderScheduler(fn) | defaults to requestAnimationFrame; pass microtask for Lit/Vue-style timing | | setHtml / setCss | swap the template tags |

import { init, mount, useRender, useEffect, mathml, html,
         setRenderScheduler, microtask, setHtml, setCss } from '@verajs/core';

class TickerLogger extends HTMLElement {
  connectedCallback() {
    init(this);                          // light DOM: no second argument
    useEffect(() => console.log(state.tick));
    mount();                             // commits the setup — this component draws nothing
  }
}

const formula = html`<math>${mathml`<mi>x</mi><mo>=</mo><mn>${x}</mn>`}</math>`;

useRender(() => html`<p>${state.n}</p>`, element);  // re-declare a render OUTSIDE the setup window

setRenderScheduler(microtask);           // Lit/Vue-style timing instead of requestAnimationFrame
setHtml(myHtml); setCss(myCss);          // swap the template tags a whole app resolves through

init() opens a component's setup and one of two calls closes it. mount() commits: it runs the first pass of every hook registered since init() and clears the instance. render(template) is exactly useRender(template) followed by that same commit — a compound over the base operation, not a second way to do the same thing, which is why a component only ever calls one of them.

Use mount() when a component has no markup of its own. Hooks that are never committed never run: no error, no render, an effect that simply does not happen — so in development a component that finishes connectedCallback without reaching either call warns and names both.

Setup is one synchronous block, which matters for async connectedCallback(). Only one component is being set up at a time, so a second component's init() takes the slot from the first — and an await between init() and render() hands it over. One component alone is fine; two on a page, each fetching, and whichever resumes second renders nothing. Await before init():

async connectedCallback() {
  const data = await fetch(this.dataset.url).then((r) => r.json());   // await first
  init(this, { mode: 'open' });                                       // then set up, synchronously
  const state = createStore({ data });
  render(() => html`<p>${state.data.title}</p>`);
}

Server-side this is already handled for you: renderToStringAsync awaits connectedCallback.

Taking input from an attribute. Attributes are the other half of how a web component receives anything — Props is the half that carries values, and an attribute carries a string that is visible in markup, which makes it the right channel for CSS hooks, static markup and anything a person may write by hand in HTML. Reach for a prop for data (objects, arrays, stores, dates) and an attribute for the rest.

Unlike props, the wiring here is yours: write the new value into a store the template reads. The one sharp edge is the platform's ordering — attributeChangedCallback runs before connectedCallback for any attribute already in the markup, so the store does not exist yet on that first call. Guard it, and read the initial value in connectedCallback:

static observedAttributes = ['label'];

attributeChangedCallback(name, previous, value) {
  if (!this.state) return;          // upgrade: setup has not run yet
  this.state.label = value;
}

connectedCallback() {
  init(this, { mode: 'open' });
  this.state = createStore({ label: this.getAttribute('label') });   // the initial value
  render(() => html`<p>${this.state.label}</p>`);
}

Without the guard the component still renders, because a custom-element reaction that throws is reported rather than rethrown — so the cost is an uncaught TypeError in the console of every page using one, and nothing here can warn about it.

svg and mathml are not stylistic. A namespace is decided when markup is parsed and cannot be fixed afterwards, so html alone produces an HTMLUnknownElement named circle — which parses fine and never draws anything.

render(() => html`
  <svg viewBox="0 0 10 10">
    ${svg`<circle cx=${x} cy=${y} r=${r} fill=${color} />`}
  </svg>
`);

Extending it

Core dispatches seven extension points and knows nothing about what is registered on them. Renderers, autoloaders, static styles adoption, reactive collections, error boundaries and write batching are all built this way, outside core, on the same public surface you have.

| | | | --- | --- | | wire({ on: name, fn: callback, priority: priority }) | register on an extension point — priority is required | | inserts | the registry itself | | createHook({ callback, priority, element? }) | build your own hook type |

The points are 'render', 'init', 'proxy-handler' (a store read), 'set-handler' (a store write — return false to hold the default propagation back), 'error' (a hook threw), 'collection' (a Map/Set method read in a store — how @verajs/store/collections attaches) and 'value' (a child-position value the renderer has no built-in answer for). @verajs/inserts documents each one, with signatures.

import { wire, inserts, createHook } from '@verajs/core';

wire({ on: 'error', fn: (error, element) => report(error, element.localName), priority: 50 });

const errorChain = inserts.get('error');            // the registry itself: name -> ordered chain

createHook({                                        // your own hook type, scheduled like the rest
  callback: (change, first) => { if (!first) log(change); },
  priority: 60,                                     // after render (50), before useEffect (75)
});

Take wire from @verajs/core, not from @verajs/inserts. A production bundle inlines the registry, so registering through a separately imported copy writes to a map core never reads — it works in development and silently does nothing in production.

ARIA and the shadow boundary

Every ID-based ARIA relationship resolves within a single tree, so a shadow root breaks it silently. aria-labelledby, aria-describedby and <label for> all match by ID, and IDs do not cross a shadow boundary — there is no error and no warning, just an element with no accessible name. Verified in Chromium, Firefox and WebKit (tests/browser/aria-shadow-boundary.test.js); it is the platform's rule, not this framework's.

// Broken: the label is in the page, the input is in the shadow root.
<label for="email">Email</label>
<my-field></my-field>            //  init(this, { mode: 'open' }); render(() => html`<input id="email">`)

Three ways through, in the order worth reaching for:

  1. Keep the relationship inside one root. Render the label and the control in the same template. This is the common case and needs nothing special.

  2. Put the ARIA on the host with ElementInternals. The host lives in the outer tree, so a role and an accessible name set there are visible to the page and need no ID at all:

    connectedCallback() {
      this._internals ??= this.attachInternals();
      this._internals.role = 'button';
      this._internals.ariaLabel = 'Save';
      init(this, { mode: 'open' });
    }
  3. Use light DOM — omit the second argument to init — when a component's whole job is to participate in relationships the page owns. Style isolation is what you give up; static styles still works, hoisted once per class.

delegatesFocus: true is the related shadow option: it makes the host focusable and forwards focus to the first focusable child, which is what a custom control usually wants.

Two things that will bite you

Custom-element fields must be declared in TypeScript. At ES2022 a class field is a definition, not an assignment: item?: Item emits item;, which runs during element upgrade and overwrites whatever a parent already bound there. Write declare item?: Item — it emits nothing, and the binding survives. In development the renderer warns when it observes this happening.

Prefer a stable template shape over swapping subtrees. Rendering the same elements every pass and toggling ?hidden keeps template identity, so values update in place instead of the subtree being torn down and rebuilt:

// fragile
html`<section>${items.length ? html`<ul>${rows}</ul>` : html`<p>empty</p>`}</section>`;

// preferred
html`<section>
  <ul ?hidden=${!items.length}>${rows}</ul>
  <p ?hidden=${items.length > 0}>empty</p>
</section>`;

The rest

The complete API reference lives in the repository's llms.txt — written to be pasted into an AI context window, and just as readable by people. It carries the full export list, the buildless CDN and JSX recipes, what VeraJS deliberately does not support, and the mistakes that come up most.

License

MIT