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

@fr0st/component

v1.0.0

Published

Native JavaScript stateful web components with reactive bindings, slots, shadow DOM, suspense, and HTML template autoloading.

Readme

FrostComponent

CI npm version npm downloads minzipped size license

Native JavaScript stateful web components with reactive bindings, slots, shadow DOM, suspense, and HTML template autoloading, with dynamic component loading and no compilation step.

Highlights

  • Default ESM export for browser projects and bundlers
  • Browser UMD bundle in dist/ exposed as globalThis.Component
  • No compilation step or virtual DOM
  • Reactive component state built on @fr0st/state
  • HTML template autoloading for x-* elements
  • Text, attribute, property, event, and input bindings
  • Control-flow directives with x:if, x:else-if, x:else, and x:each
  • Slots, shadow DOM templates, and built-in x-suspense
  • JSDoc-powered IntelliSense

Installation

Bundlers / browser projects

npm i @fr0st/component

FrostComponent is ESM-only. Import the default Component export into browser-targeted bundles and apps.

Browser (UMD)

Load the bundle from your own copy or a CDN:

<script src="/path/to/dist/frost-component.min.js"></script>
<!-- or -->
<script src="https://cdn.jsdelivr.net/npm/@fr0st/component@latest/dist/frost-component.min.js"></script>
<script>
    Component.bootstrap({ baseUrl: '/components', extension: 'html' });
</script>

The browser bundle exposes globalThis.Component. Call Component.bootstrap(...) to start the runtime and register built-ins such as x-suspense.

Quick Start

HTML autoloaded components

Point Component.bootstrap() at a folder of component templates and drop x-* elements into the page.

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <script src="https://cdn.jsdelivr.net/npm/@fr0st/component@latest/dist/frost-component.min.js"></script>
</head>
<body>
    <script>
        Component.bootstrap({ baseUrl: '/components', extension: 'html' });
    </script>

    <x-counter start="3"></x-counter>
</body>
</html>

/components/x-counter.html

<!-- shadow -->

<script>
    this.state.use('count', Number(this.state.start ?? 0));
</script>

<button @click="{ this.state.count++ }">Count: {count}</button>

See the examples/ folder for complete browser and component examples.

JavaScript-defined components

You can also define components directly with customElements.define(...).

import Component from '@fr0st/component';

class XGreeting extends Component {
    static get template() {
        return `
            <div>
                <h1>Hello {name}</h1>
                <button @click="{ this.state.count++ }">Clicked {count} times</button>
            </div>
        `;
    }

    initialize() {
        this.state.use('name', 'World');
        this.state.use('count', 0);
    }
}

customElements.define('x-greeting', XGreeting);

JS-defined classes can also opt into shadow DOM with static shadowMode = 'open' or static shadowMode = 'closed'.

TypeScript note: FrostComponent is written in JavaScript and uses JSDoc types, which most editors surface as IntelliSense.

Authoring Model

FrostComponent revolves around a small base class and declarative template bindings.

  • Component tag names must begin with x-
  • Components must render exactly one root element
  • Root <slot> elements are not allowed
  • this.state is a StateStore from @fr0st/state
  • Non-x: host attributes become initial state and are removed from the host
  • x:key exposes keyed descendants directly on the component instance

Host attributes are parsed as JavaScript when possible and otherwise kept as strings.

<x-profile
    name="Ada"
    age="37"
    active="true"
    state="{ theme: 'dark', compact: true }">
</x-profile>

Inside the component, that becomes state like:

this.state.name; // 'Ada'
this.state.age; // 37
this.state.active; // true
this.state.theme; // 'dark'
this.state.compact; // true

x:key lets you grab important nodes directly from the instance:

<div>
    <button x:key="saveButton">Save</button>
</div>
this.saveButton; // <button>

Bindings

Binding syntax is context-sensitive rather than "full JavaScript everywhere".

Text interpolation note: Use {key} for state lookups and {{ expression }} for full JavaScript expressions.

  • Most non-event bindings treat a bare value as a state lookup, not as arbitrary JavaScript. :title="label", .service="service", x:bind="text", and x:if="visible" all resolve through this.state.
  • Wrap non-event bindings in braces when you need real JavaScript, such as :class="({ active: this.state.active })" or x:else-if="{ this.state.items.length < 10 }".
  • Event bindings are different: a bare value is a component method name, not a state key. @click="save" resolves save on the component instance.
  • Event bindings also accept function expressions such as @remove="(event) => { ... }" and braced statement bodies such as @click="{ this.state.count++ }".

Text nodes interpolate state keys with single braces and full expressions with double braces:

<p>Hello {name}. Next: {{ this.state.count + 1 }}</p>

Attributes

Use :attr for dynamic attributes:

<button
    :class="({ active: this.state.active, disabled: this.state.disabled })"
    :style="({ color: this.state.urgent ? 'red' : '' })"
    :title="label">
    Save
</button>

Object literals need braces so they are parsed as JavaScript instead of as a bare state lookup.

You can also bind class and style from state keys such as :class="classes" or :style="styles". class bindings support strings, arrays, and object maps. style bindings support strings and object maps.

When the target is another component, :state merges object values into the child state, and other bound attributes become child state keys.

Properties

Use .prop to assign custom JavaScript properties on DOM elements:

<button .service="service"></button>

.prop is intentionally limited to custom properties. Built-in DOM properties such as .value are not supported.

Events

Use @event to attach handlers.

Valid handler forms are:

  • Component method names such as @click="save"
  • Function expressions such as @remove="(event) => { ... }"
  • Braced statement bodies such as @click="{ this.state.count++ }"

Supported modifiers:

  • .prevent
  • .stop
  • .once
  • .self
  • .capture
  • .passive
<button @click.prevent="{ this.dispatch('save') }">Save</button>
<x-item @remove="(event) => { this.removeItem(event.target.state.id) }"></x-item>

Form inputs

Use x:bind to keep form controls and state in sync. x:bind always takes a state key, not a general expression.

Supported behaviors:

  • Text inputs, textareas, and single selects read and write string values
  • Checkbox bindings become booleans by default
  • Checkbox bindings become arrays when the current state value is an array
  • Radio groups read and write the selected radio's value
  • Multi-selects read and write arrays of selected option values
<input type="text" x:bind="title" />
<textarea x:bind="body"></textarea>
<input type="checkbox" x:bind="enabled" />

<input type="checkbox" value="red" x:bind="colors" />
<input type="checkbox" value="blue" x:bind="colors" />

<input type="radio" name="size" value="s" x:bind="size" />
<input type="radio" name="size" value="m" x:bind="size" />

<select x:bind="status">
    <option value="draft">Draft</option>
    <option value="published">Published</option>
</select>

<select multiple x:bind="tags">
    <option value="news">News</option>
    <option value="docs">Docs</option>
</select>

For checkbox arrays and multi-selects, initialize the state as an array:

this.state.use('colors', []);
this.state.use('tags', []);

Control Flow

FrostComponent supports conditional and loop blocks directly in templates.

Conditionals

Conditionals follow the same expression rules as other non-event bindings: use a bare state key for simple lookups, or wrap JavaScript expressions in braces.

<x-empty x:if="empty"></x-empty>
<x-list x:else-if="{ this.state.items.length < 10 }"></x-list>
<x-list-large x:else></x-list-large>

Notes:

  • x:else-if and x:else belong to the immediately preceding conditional chain
  • Only the first matching branch is attached
  • Blocks can be nested inside other conditionals and loops
  • x:if and x:each cannot be used on the same element

Loops

x:each is used on component elements, not plain DOM elements. FrostComponent clones the component, assigns each item into its state, and reuses initialized instances when identifiers stay stable.

<x-todo-item x:each="items" x:id="id"></x-todo-item>
  • x:each defaults to items when left empty
  • x:id defaults to id
  • The iterable must resolve to an array
  • The loop target must be a component element
  • Each item must include the identifier property
  • Identifiers must be unique within the array
  • Existing component instances are reused when identifiers stay stable

Example:

<x-todo-item x:each="items" x:id="id"></x-todo-item>
<x-empty x:if="{ this.state.items.length === 0 }"></x-empty>

Slots

Slots work in both light DOM and shadow DOM components.

<div>
    <slot name="header"></slot>
    <slot></slot>
</div>

In light DOM components, FrostComponent replaces descendant <slot> elements with markers and moves matching children into place. In shadow mode, assigned children continue to behave like native slotted content.

HTML Template Components

Autoloaded HTML components can include one render root plus optional top-level scripts and styles. These scripts and styles must be direct children of the template, not nested inside the render root.

Scripts

  • <script src="...">: external scripts loaded once per source
  • <script connected>: runs on each connection
  • <script>: runs during initialize()

Styles

  • Light DOM templates append top-level <style> and <link rel="stylesheet"> tags to document.head
  • Shadow templates clone those styles into each shadow root

Shadow mode

Use a top-level comment directive in an HTML template:

<!-- shadow -->

or

<!-- shadow:closed -->

This keeps the host element and renders the template inside a shadow root. Without a shadow directive, a light DOM component replaces its host with the rendered root element.

Component API

FrostComponent exports a single default class:

import Component from '@fr0st/component';

Component.bootstrap(options)

Bootstraps built-in components, DOM observation, and optional autoloading for undefined x-* elements.

Component.bootstrap({
    baseUrl: '/components',
    extension: 'html',
});

Options:

  • options.baseUrl: folder used to fetch component templates
  • options.extension: optional file extension appended to component URLs

You can call Component.bootstrap() more than once. Omitted options keep the current autoload settings.

Instance properties

  • component.state: the component's reactive StateStore
  • component.element: the component's public DOM node and dispatch surface; the host element in shadow mode, otherwise the final rendered element exposed outside nested light-DOM wrappers
  • component.rootElement: the root element returned by render()
  • component.renderRoot: the container that holds rendered output; a ShadowRoot in shadow mode, otherwise rootElement
  • component.parentComponent: the nearest parent component instance, if any
  • component.childComponents: child component instances rendered inside this component
  • component.connected: whether the component has entered the connection lifecycle
  • component.mounted: whether the runtime currently considers the component mounted in the observed DOM
  • component.visible: whether the runtime currently considers the component visible
  • component.initialized: whether state parsing, binding, and initialize() have completed
  • component.loaded: whether child components and deferred loads have finished settling

Instance methods

  • component.initialize(): lifecycle hook after render and binding
  • component.effect(callback, options): register a reactive effect
  • component.dispatch(name, detail): dispatch a bubbling composed custom event
  • component.deferLoad(promise): hold back loaded until a promise settles
  • component.ready(callback): run a callback once the component is loaded
  • component.slot(name = ''): access a parsed light-DOM slot object with assign(node) and assigned(), or undefined when no slot exists

Effects

component.effect() tracks the state reads inside its callback and re-runs when those values change.

class XCounter extends Component {
    static get template() {
        return '<div>{count}</div>';
    }

    initialize() {
        this.state.use('count', 0);

        this.effect(() => {
            console.log('count =', this.state.count);
        });
    }
}

Effects are always deferred until the component is mounted. By default they also wait until the component is visible, and any skipped re-runs are flushed on the next visible event.

Pass { waitForVisible: false } when the effect should continue to run while the component is mounted but off-screen:

this.effect(() => {
    localStorage.setItem('draft', this.state.text ?? '');
}, { waitForVisible: false });

Deferred loading

Use component.deferLoad() when a component should not be considered loaded until some async work finishes:

class XLoader extends Component {
    static get template() {
        return '<div>{label}</div>';
    }

    initialize() {
        this.deferLoad(
            fetch('/api/data')
                .then((response) => response.text())
                .then((label) => {
                    this.state.label = label;
                }),
        );
    }
}

x-suspense

x-suspense is registered when you call Component.bootstrap(...). It renders fallback content until child components finish loading, then unwraps the real content.

Using the XLoader example above:

<x-suspense>
    <template slot="fallback">
        <div>Loading...</div>
    </template>

    <x-loader></x-loader>
</x-suspense>

The fallback stays visible until the child components finish loading, including any promises passed to deferLoad().

Behavior Notes

  • Light DOM components replace their custom-element host with the rendered root. Shadow components keep the host element.
  • dispatch() emits from the component's public DOM node. In light DOM that is the rendered root element. In shadow mode that is the host element.
  • .prop bindings support custom properties only.
  • Event handlers must be a method name, a function expression, or a braced statement body.
  • x:each can only be used on component elements.
  • Slots must be descendants of the render root, not the root element itself.
  • effect(callback, { waitForVisible }) always waits until mount, and defers invisible re-runs by default.

Development

npm test
npm run js-lint
npm run build

License

FrostComponent is released under the MIT License.