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

@taipa/ui

v0.2.0

Published

Direct-DOM islands, server rendering, and progressive forms for server-authored HTML.

Readme

@taipa/ui

Direct-DOM islands and progressive forms for server-rendered applications.

Taipa renders safe HTML once, then attaches events and signal-driven bindings to declared DOM refs. Server HTML remains authoritative: hydration does not rerender, replace, or re-parent it.

[!WARNING] @taipa/ui is in alpha. Use it to validate server-authored islands and progressive forms, but expect the contract to tighten before a stable release.

Install

pnpm add @taipa/ui alien-signals
npm install @taipa/ui alien-signals

Pick your starting point

| Goal | API | | ------------------------------------ | ------------------------------------------ | | Render into an empty browser element | mount() from @taipa/ui/client | | Activate server-authored islands | renderIsland() + bootstrap() | | Produce server-only HTML | renderToString() from @taipa/ui/server | | Enhance an existing native form | createForm() from @taipa/ui/forms |

Client-side

Simple: mount a component

Add an empty host to the page:

<div id="counter"></div>

Keep the component definition DOM-free in counter.ts so both browser and server code can import it:

import { component, html } from "@taipa/ui";

export const Counter = component<{ initial: number }>("Counter")
  .state("count", ({ props }) => props.initial)
  .bind("count", ({ element, state }) => {
    element.textContent = String(state.count());
  })
  .on("decrement@click", ({ state }) => {
    state.count(state.count() - 1);
  })
  .on("increment@click", ({ state }) => {
    state.count(state.count() + 1);
  })
  .render(
    ({ state }) => html`
      <button type="button" data-taipa-ref="decrement">−</button>
      <output data-taipa-ref="count" aria-live="polite">${state.count()}</output>
      <button type="button" data-taipa-ref="increment">+</button>
    `,
  );

Mount it from a browser-only entry such as counter.client.ts:

import { mount } from "@taipa/ui/client";
import { Counter } from "./counter";

const host = document.querySelector<HTMLElement>("#counter");

if (host !== null) {
  await mount(host, Counter, { props: { initial: 0 } });
}

The view runs once. After mounting, writes to state.count update the retained <output> through its binding without rendering the view again. A non-empty host requires { replace: true }.

Advanced: hydrate lazy islands

Use bootstrap() when the server already produced <taipa-island> hosts. The registry explicitly approves which module can activate each component:

import { bootstrap } from "@taipa/ui/client";

bootstrap({
  registry: {
    Counter: {
      load: () => import("./counter.js"),
      exportName: "Counter",
    },
  },
  observe: true,
  onError(error, host) {
    host.setAttribute("data-taipa-failed", "");
    reportError(error);
  },
});

bootstrap() respects the island's load, idle, visible, or only policy. Before activation, Taipa validates serialized props, state overrides, and required refs; then it attaches listeners, bindings, effects, and lifecycle hooks to the existing nodes. observe: true includes islands added later by navigation or streamed fragments. Keep the returned handle when you need to rescan a root or destroy the runtime.

For one known host and component, import hydrate() directly instead of scanning a document.

Server-side

Basic: render a string

The server entrypoint is DOM-free at import time:

import { component, html } from "@taipa/ui";
import { renderToString } from "@taipa/ui/server";

const Greeting = component<{ name: string }>("Greeting").render(
  ({ props }) => html`<p>Hello, ${props.name}!</p>`,
);

const markup = await renderToString(Greeting, { name: "Ada" });
// <p>Hello, Ada!</p>

Dynamic text and attributes are escaped by default. To send an interactive component, render an island and approve the same component name in the browser registry:

import { renderIsland } from "@taipa/ui/server";
import { Counter } from "./counter";

const markup = await renderIsland(
  Counter,
  { initial: 3 },
  { hydrate: "visible", visibleRootMargin: "200px" },
);

Props and state overrides are serialized as inert JSON. Hydration later attaches to the exact DOM created from this response.

Render repeated markup

repeat() composes a synchronous iterable into safe initial HTML:

import { html, repeat } from "@taipa/ui";

const rows = repeat(products, (product) => html`<li>${product.name}</li>`);
const markup = html`<ul>
  ${rows}
</ul>`;

Every callback must return SafeHtml from html or raw. repeat() does not reconcile items after hydration; use direct DOM code when a live list must change.

Forms

createForm() adds reactive state and validation to a real form while preserving native controls, browser constraints, accessibility, and the normal server POST path.

<form id="signup" action="/signup" method="post">
  <label for="email">Email</label>
  <input id="email" name="email" type="email" required aria-describedby="email-error" />
  <p id="email-error" data-taipa-error-for="email"></p>

  <p data-taipa-error-for="$form" role="status"></p>
  <button type="submit">Create account</button>
</form>
import { createForm } from "@taipa/ui/forms";

const form = document.querySelector<HTMLFormElement>("#signup");

if (form !== null) {
  const controller = createForm(form, {
    read: ({ formData }) => ({
      email: String(formData.get("email") ?? ""),
    }),
    validate: ({ values }) =>
      values.email.includes("@") ? undefined : { email: ["Enter a valid email address."] },
    mode: "blur",
  });

  // Signals are available when surrounding UI needs them.
  controller.valid();
  controller.errors();
}

Without a custom submit handler, Taipa validates and then replays the native submission. Provide submit for enhanced requests, or use standardSchema(schema) with any Standard Schema V1 validator. Server-side validation remains required in every case.

Entrypoints

| Entrypoint | Purpose | | ------------------ | ------------------------------------------------------------------------- | | @taipa/ui | Components, safe templates, safe URLs, repeat(), and reactivity exports | | @taipa/ui/client | mount, hydrate, unmount, and bootstrap | | @taipa/ui/server | renderToString and renderIsland without DOM globals | | @taipa/ui/forms | Progressive forms and Standard Schema adapters |

All entrypoints are ESM. Client and forms imports are side-effect free until you call an explicit runtime API.