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

@skirbi/sugar

v0.0.11

Published

Lightweight base layer for writing custom elements with declarative attributes and template sugar.

Readme

@skirbi/sugar

A lightweight base layer for building Web Components.

It provides:

  • Declarative attribute → config mapping
  • Template helpers
  • Alias utilities
  • Form-control wrapping primitives

TL;DR

You can create Web Components like so:

class MyNewElement extends HTMLElementSugar {
  static tag = 'my-new-element';
  static attributeDefs = {
    foo: { default: 'bar' },
    bar: { parser: parseBoolean },
  };
}

You can also create aliases:

TrackItem.alias('some-song', { type: 'song' });
YourBook.alias('a-book');

And to register them:

TrackItem.register();
YourBook.register();

Observable patterns

You can define attributes to be observable:

class MyNewElement extends HTMLElementSugar {
  static tag = 'my-new-element';
  static attributeDefs = {
    'foo': { default: 'bar' },
    'bar': { parser: parseBoolean },
    'baz': null,
    'qux': {},
    'toto': { observed: "" }, // not observed
    'titi': { observed: 0 }, // not observed
    'tata': { observed: false }, // not observed
    'tete': { observed: null }, // not observed
    'yes': { observed: true },
    'yez': { observed: 1 },
    'yep': { observed: "here be dragons" },
  };
}

Templates

Template via HTML template reference

class HTMLTemplate extends HTMLElementSugar {
  static tag = 'html-template';
  static HtmlTemplate = 'track-item-template'; // find in html
}

HTMLElementSugar asserts that it can find the template on registration. Which means you need to have the template ready in HTML. It is therefore essential that you register your component in window.addEventListener('DOMContentLoaded', () => { }.

Template via javascript element reference

const tpl = document.createElement('template');
tpl.id = 'inline';
tpl.innerHTML =
  `<div class="track-row"><span class="song">song</span></div>`;
document.body.appendChild(tpl);

class JSElement extends HTMLElementSugar {
  static tag = 'js-element';
  static HtmlTemplate = tpl;
}

Template via javascript function

class TemplateFunction extends HTMLElementSugar {
  static tag = 'template-function';

  static HtmlTemplate() {
    const t = document.createElement('template');
    t.innerHTML =
      `<div class="track-row"><div class="track-info">from fn</div></div>`;
    return t;
  }
}

We recently added a helper you can now do this too:

class TemplateFunction extends HTMLElementSugar {
  static tag = 'template-function';

  static HtmlTemplate = this.tpl(`
      <div class="track-row">
        <div class="track-info">from tpl</div>
      </div>
  `);
}

Custom template with fallback

class TemplateFunction extends HTMLElementSugar {
  static tag = 'template-function';

  static HtmlTemplate = [
    'some-id-in-html',
    () => {
      const t = document.createElement('template');
      t.innerHTML =
        `<div class="track-row"><div class="track-info">from fn</div></div>`;
      return t;
    }
  ];
}

No template (the ultimate minimalism)

class MinimalistComponent extends HTMLElementSugar {
  static tag = 'ultimate-minimalist';
  // Look at all this template code I'm not writing
}

You can register your Web Components by running:

MyComponent.register();

HTMLElementSugarInput

HTMLElementSugarInput extends HTMLElementSugar and is designed for components that wrap a real form control in the light DOM.

It provides:

  • Attribute forwarding to the real control
  • Native input and change re-emission
  • Optional attribute mirroring via data-sync
  • Proper value property handling

Contract

A subclass must:

  • Extend HTMLElementSugarInput
  • Render exactly one element matching [wc-control] (or override static controlSelector)

Example:

class MyInput extends HTMLElementSugarInput {
  static tag = 'my-input';

  static attributeDefs = {
    label: { default: '' },
    value: { default: '' },
  };

  static HtmlTemplate = this.tpl(`
    <div>
      <label wc-label></label>
      <input wc-control type="text">
    </div>
  `);

  connectedCallback() {
    super.connectedCallback();

    const frag = this.renderFromTemplate();
    const control = this.enhanceControl(frag);

    const { label, value } = this.getConfig();

    frag.querySelector('[wc-label]').textContent = label;
    control.value = value ?? '';

    this.replaceChildren(frag);
  }
}

MyInput.register();

Attribute Forwarding

All host attributes that are not defined in attributeDefs are forwarded to the real control element.

Example:

<my-input
  label="Name"
  wire:model.defer="name"
  aria-label="Name"
></my-input>

Results in:

<input
  wire:model.defer="name"
  aria-label="Name"
>

Not forwarded

  • Attributes defined in attributeDefs
  • id, class, style (kept on the host)

This keeps components framework-agnostic:

  • Livewire (wire:*)
  • Alpine (x-*)
  • HTMX (hx-*)
  • aria-*
  • data-*

Event Re-Emission

Native events from the control are re-emitted from the host:

  • input
  • change

Listeners may bind to either the host or the internal control.

data-sync (Optional Attribute Mirroring)

By default, attributes are forwarded once during connect.

If you enable data-sync, subsequent attribute changes on the host are mirrored to the control using a MutationObserver.

Example:

<my-input wire:model.defer="name" data-sync></my-input>

When data-sync is enabled:

  • Host attribute changes are mirrored to the control
  • value is mirrored as a property (not an attribute)
  • Removing attributes removes them from the control

This is particularly useful in reactive environments such as Livewire.

HTMLElementSugarSelect

HTMLElementSugarSelect extends HTMLElementSugarInput and provides a structured way to author <select>-based components.

It supports:

  • Static option lists (via JSON)
  • Remote option loading (via endpoint)
  • Flexible data mapping via jpath
  • Label/value templates
  • Optional search input
  • Attribute forwarding to the underlying <select>

It always renders a real <select> in the light DOM.

Contract

A subclass must:

  • Extend HTMLElementSugarSelect
  • Render exactly one <select wc-control>
  • Optionally include a search input if searchable is enabled

Example:

class MySelect extends HTMLElementSugarSelect {
  static tag = 'my-select';

  static HtmlTemplate = this.tpl(`
    <select wc-control></select>
  `);
}

MySelect.register();

Static Options

You may provide options via the options attribute as a JSON array:

<my-select
  options='[
    { "id": 1, "name": "Alice" },
    { "id": 2, "name": "Bob" }
  ]'
  jpath-label="name"
  jpath-value="id"
></my-select>

Mapping Options

Data mapping is controlled via:

  • jpath: path to the array in a nested JSON response
  • jpath-label: path for the option label
  • jpath-value: path for the option value
  • jpath-label-template: template string for label
  • jpath-value-template: template string for value

Templates take precedence over path mappings.

Example:

<my-select
  options='[
    { "id": 1, "name": "Alice", "email": "[email protected]" }
  ]'
  jpath-label-template="{name} ({email})"
  jpath-value-template="user:{id}"
></my-select>

Missing template fields resolve to empty strings.

Remote Options

Options can be loaded from a remote endpoint:

<my-select
  endpoint="/api/users"
  method="GET"
  param="q"
  searchable
  min-chars="2"
  debounce="300"
  nosearch-initial
></my-select>

Behavior:

  • Fetches JSON from endpoint
  • Adds search query via param
  • Applies mapping rules
  • Replaces options on each fetch

If searchable is enabled:

  • A search input is rendered

  • Input is debounced

  • Fetch is triggered after min-chars is reached

  • By default, a remote endpoint triggers an initial fetch with an empty query on connect. If nosearch-initial is enabled this behavior is negated.

  • No initial searches are

Search Behavior (Static Mode)

When using static options, enabling searchable:

  • Filters <option> elements in place
  • Uses case-insensitive substring matching
  • Does not modify underlying data

Value Handling

The value attribute behaves consistently with HTMLElementSugarInput:

  • Initial value is applied after options render
  • With data-sync, updates are mirrored to the <select> element
  • Native input and change events are re-emitted from the host

Framework Compatibility

Because it renders a real <select> in the light DOM, it works naturally with:

  • Livewire (wire:*)
  • Alpine (x-*)
  • HTMX (hx-*)
  • aria-*
  • data-*

Attributes not defined in attributeDefs are forwarded to the <select> element.

Why This Works

  • No shadow DOM
  • No custom dropdown UI
  • No CSS lock-in
  • Real <option> elements
  • Progressive enhancement friendly

Code of Conduct

Be human.

Developer notes about this package

Versioning

This project does not follow semver. It follows a Perl-style release philosophy centered on backward compatibility. This translates to the following hard guarantee: We do not intentionally break working code. If a release causes breakage, it will be addressed accordingly.

The x.y.z version number should not be used to infer stability. Consult the Changes file for important updates, deprecations, and breaking changes.

The current 0.x.z range does not imply alpha, beta, or instability. It is simply the starting point of the project.

In case we foresee breaking changes we'll add deprecation warnings. Giving you ample time to fix things before a breaking change will be introduced. When a change will be introduced is communicated in the Changes file. Security fixes may cause breakage at any given time without notice.

This package is released by @opndev/rzilla, changes to package.json will be overridden. In addition to a little bit of promotion, this also means that version numbers are autoincremented at release time and bumped in all relevant files: Versioning for humans, not machines.