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

@effex/dom

v1.1.0

Published

DOM rendering for Effex - a reactive UI framework built on Effect.ts

Readme

@effex/dom

DOM rendering for Effex applications. This package provides elements, components, control flow primitives, animation, SSR/hydration, and mounting utilities.

Note: This package re-exports everything from @effex/core. You don't need to install both.

Installation

pnpm add @effex/dom effect

Subpath Exports

| Import Path | Purpose | |-------------|---------| | @effex/dom | Main export — elements, control flow, utilities, and all of @effex/core | | @effex/dom/server | Server-side rendering (renderToString) | | @effex/dom/hydrate | Client-side hydration (hydrate) |

Basic Usage

Simple Components

Components without state or context requirements can be plain functions:

import { $, collect } from "@effex/dom";

const Greeting = (props: { name: string }) =>
  $.div({ class: "greeting" }, collect(
    $.h1({}, $.of(`Hello, ${props.name}!`)),
    $.p({}, $.of("Welcome to Effex")),
  ));

Stateful Components

Components that need signals, context, or other Effects use Effect.gen:

import { Effect } from "effect";
import { $, collect, Signal } from "@effex/dom";

const Counter = () =>
  Effect.gen(function* () {
    const count = yield* Signal.make(0);

    return yield* $.div({}, collect(
      $.button({ onClick: () => count.update((n) => n - 1) }, $.of("-")),
      $.span({}, $.of(count)),
      $.button({ onClick: () => count.update((n) => n + 1) }, $.of("+")),
    ));
  });

Running Your App

Use runApp and mount to start your application:

import { Effect } from "effect";
import { mount, runApp } from "@effex/dom";

runApp(
  Effect.gen(function* () {
    yield* mount(App(), document.getElementById("root")!);
  }),
);

runApp handles boilerplate: scoping, SignalRegistry, and keeping the app alive. You can also pass a layer option for additional services:

runApp(
  Effect.gen(function* () {
    yield* mount(App(), document.getElementById("root")!);
  }),
  { layer: Navigation.makeLayer(router) },
);

Elements

The $ namespace contains factories for all HTML and SVG elements. Elements are Effects that produce DOM nodes:

yield* $.div({ class: "container", style: { color: "red" } }, collect(
  $.h1({}, $.of(name)),
  $.p({}, $.of(t`${count} items`)),
));

Children

Use $.of() to lift primitives and Readables into children, and collect() to combine multiple children:

// Single child
yield* $.h1({}, $.of("Hello World"));

// Multiple children
yield* $.div({}, collect(
  $.of("Hello"),
  $.span({}, $.of("World")),
));

// Empty child
yield* $.div({}, $.empty);

Attributes

Elements accept an optional attributes object as the first argument:

$.input({
  // Standard attributes
  type: "text",
  placeholder: "Enter name",
  disabled: true,
  id: "name-input",

  // Reactive attributes — UI updates automatically
  value: name,             // Readable<string>
  class: className,        // Readable<string>
  hidden: isHidden,        // Readable<boolean>

  // Style as object or string
  style: { color: "red", fontSize: "16px" },

  // Class as string, array, or Readable
  class: ["btn", isActive.pipe(Readable.map(a => a ? "btn-active" : ""))],

  // Data and ARIA attributes
  "data-testid": "name",
  "aria-label": "Name input",
  role: "textbox",

  // Ref binding
  ref: inputRef,
});

Event Handlers

Event handlers are functions that optionally return an Effect:

$.button({
  onClick: (e) => count.update((n) => n + 1),
  onKeyDown: (e) => {
    if (e.key === "Enter") return submit.run();
  },
  onSubmit: (e) => {
    e.preventDefault();
    return handleSubmit();
  },
});

Supported events include: onClick, onInput, onChange, onSubmit, onKeyDown, onKeyUp, onFocus, onBlur, onMouseDown, onMouseUp, onMouseEnter, onMouseLeave, onPointerDown, onPointerUp, onPointerMove, onScroll, onWheel, onDragStart, onDrag, onDragEnd, onDrop, onDragOver, onTouchStart, onTouchMove, onTouchEnd, onAnimationEnd, onTransitionEnd, and more.

SVG Elements

SVG elements are also available on $:

$.svg({ viewBox: "0 0 24 24", width: 24, height: 24 },
  $.path({ d: "M12 2L2 22h20L12 2z", fill: "currentColor" }),
);

Template Strings

The t tagged template creates reactive strings from Readables:

import { t } from "@effex/dom";

const name = yield* Signal.make("World");
const count = yield* Signal.make(0);

// Creates a Readable<string> that updates automatically
yield* $.p({}, $.of(t`Hello, ${name}! Count: ${count}`));

Defining Components

Simple (No State/Context)

Components without state or context requirements are plain functions that return an Element:

const Greeting = (props: { name: string }) =>
  $.h1({}, $.of(`Hello, ${props.name}!`));

// With children — generic over E and R to propagate types
const Card = <E, R>(props: { title: string }, children: Child<E, R>) =>
  $.div({ class: "card" }, collect(
    $.h2({}, $.of(props.title)),
    children,
  ));

With State or Context

Use Effect.gen when you need signals, context, or other Effects:

const Counter = () =>
  Effect.gen(function* () {
    const count = yield* Signal.make(0);
    return yield* $.div({}, collect(
      $.button({ onClick: () => count.update((n) => n - 1) }, $.of("-")),
      $.span({}, $.of(count)),
      $.button({ onClick: () => count.update((n) => n + 1) }, $.of("+")),
    ));
  });

const UserBadge = () =>
  Effect.gen(function* () {
    const user = yield* UserContext;  // Requires context
    return yield* $.span({}, $.of(user.name));
  });

Context Providers

import { Context, Effect } from "effect";
import { $, provide } from "@effex/dom";

class ThemeContext extends Context.Tag("ThemeContext")<ThemeContext, Theme>() {}

const ThemedButton = (props: { label: string }) =>
  Effect.gen(function* () {
    const theme = yield* ThemeContext;
    return yield* $.button(
      { style: { backgroundColor: theme.primary } },
      $.of(props.label),
    );
  });

// Provide context to children
$.div({}, provide(ThemeContext, theme, ThemedButton({ label: "Click" })));

Control Flow

when

Conditionally render based on a reactive boolean:

import { when } from "@effex/dom";

when(isLoggedIn, {
  onTrue: () => $.div({}, $.of("Welcome back!")),
  onFalse: () => $.div({}, $.of("Please log in")),
});

match

Pattern match on a reactive value:

import { match } from "@effex/dom";

match(status, {
  cases: [
    { pattern: "loading", render: () => Spinner() },
    { pattern: "error", render: () => ErrorMessage() },
    { pattern: "success", render: () => Content() },
  ],
  fallback: () => $.div({}, $.of("Unknown")),
});

each

Render a list with automatic keying and reconciliation:

import { each } from "@effex/dom";

each(todos, {
  key: (todo) => todo.id,
  render: (todo, index) => TodoItem({ todo, index }),
  container: () => $.ul({ class: "todo-list" }),
});

The render callback receives Readable<T> items and Readable<number> indices — item identity is preserved across reorders. Only the changed items are updated.

matchOption / matchEither

Match on Option or Either values. The inner value is unwrapped as a Readable:

import { matchOption, matchEither } from "@effex/dom";

// userData.value is Readable<Option<User>>
matchOption(userData.value, {
  onSome: (user) => UserCard({ user }),  // user is Readable<User>
  onNone: () => $.div({}, $.of("No user")),
});

matchEither(result, {
  onRight: (value) => SuccessView({ value }),  // value is Readable<A>
  onLeft: (error) => ErrorView({ error }),     // error is Readable<E>
});

redraw

Completely rebuild the component subtree whenever a Readable changes (use sparingly — when/match/each are usually better):

import { redraw } from "@effex/dom";

redraw(locale, {
  render: (currentLocale) => LocalizedApp({ locale: currentLocale }),
});

Async Boundaries

Suspense

Handle async rendering with loading states:

import { Boundary } from "@effex/dom";

Boundary.suspense({
  render: () =>
    Effect.gen(function* () {
      const user = yield* fetchUser(id);
      return yield* UserProfile({ user });
    }),
  fallback: () => $.div({}, $.of("Loading...")),
  catch: (error) => $.div({}, $.of(`Error: ${error.message}`)),
  delay: "200 millis", // Avoid loading flash for fast responses
});

Error Boundary

Boundary.error(
  () => RiskyComponent(),
  (error) => $.div({}, $.of(`Failed: ${error.message}`)),
);

Server-Side Rendering

renderToString

import { renderToString } from "@effex/dom/server";

const handler = Effect.gen(function* () {
  const html = yield* renderToString(App());
  return new Response(`
    <!DOCTYPE html>
    <html>
      <body>
        <div id="root">${html}</div>
        <script src="/app.js"></script>
      </body>
    </html>
  `);
});

Options:

  • hydrate (default: true) — include hydration markers in the output. Set to false for static rendering.

Hydration

import { hydrate } from "@effex/dom/hydrate";
import { App } from "./App";

hydrate(App(), document.getElementById("root")!);

With options:

hydrate(App(), document.getElementById("root")!, {
  onMismatch: (message, node) => console.warn("Mismatch:", message),
  layers: myAppLayer,  // Additional layers to provide during hydration
});

Hydration attaches to server-rendered HTML and sets up reactive bindings without re-rendering the DOM. The component tree must match what was rendered on the server.

Element Manipulation

The Element namespace provides pipeable functions for imperative DOM manipulation. These are useful for refs and one-off operations:

import { Element, ref } from "@effex/dom";

const buttonRef = yield* ref<HTMLButtonElement>();

// Pipe operations on the ref
yield* buttonRef.pipe(
  Element.addClass("active"),
  Element.setStyles({ color: "blue" }),
  Element.focus,
);

Attribute Operations

yield* el.pipe(Element.setAttribute("aria-expanded", "true"));
yield* el.pipe(Element.removeAttribute("disabled"));
yield* el.pipe(Element.toggleAttribute("hidden"));
yield* el.pipe(Element.hasAttribute("disabled")); // Effect<boolean>

// Reactive binding — attribute updates when readable changes
yield* el.pipe(Element.bindAttribute("aria-label", labelReadable));
yield* el.pipe(Element.bindBooleanAttribute("disabled", isDisabled));

Class Operations

yield* el.pipe(Element.addClass("active", "highlighted"));
yield* el.pipe(Element.removeClass("loading"));
yield* el.pipe(Element.toggleClass("expanded"));
yield* el.pipe(Element.replaceClass("old-class", "new-class"));
yield* el.pipe(Element.setClass("entirely-new-class"));

// Reactive binding
yield* el.pipe(Element.bindClass(classNameReadable));

Style Operations

yield* el.pipe(Element.setStyle("backgroundColor", "red"));
yield* el.pipe(Element.setStyles({ opacity: "1", fontSize: "16px" }));
yield* el.pipe(Element.removeStyle("color"));

// Reactive binding
yield* el.pipe(Element.bindStyle("color", colorReadable));

Data Attributes

yield* el.pipe(Element.setData("state", "open"));    // data-state="open"
yield* el.pipe(Element.removeData("state"));
yield* el.pipe(Element.getData("state"));             // Effect<string, DataAttributeNotFound>

// Reactive binding
yield* el.pipe(Element.bindData("state", stateReadable));

Content Operations

yield* el.pipe(Element.setTextContent("Hello"));
yield* el.pipe(Element.setInnerHTML("<em>bold</em>"));
yield* el.pipe(Element.setInputValue("new value"));  // Without cursor reset

// Reactive bindings
yield* el.pipe(Element.bindTextContent(textReadable));
yield* el.pipe(Element.bindInputValue(valueReadable));

Focus & Interaction

yield* el.pipe(Element.focus);
yield* el.pipe(Element.blur);
yield* el.pipe(Element.click);
yield* el.pipe(Element.focusFirst("[data-item]"));  // Focus first matching descendant
yield* el.pipe(Element.focusLast("[data-item]"));

yield* el.pipe(Element.getBoundingClientRect); // Effect<DOMRect>
yield* el.pipe(Element.getId);                 // Effect<string>
yield* el.pipe(Element.contains(childNode));   // Effect<boolean>

Event Listeners

yield* el.pipe(Element.on("click", (e) => handleClick(e)));
yield* el.pipe(Element.once("transitionend", (e) => afterTransition()));

Utility

yield* el.pipe(Element.tap((node) => console.log(node)));
yield* el.pipe(Element.tapEffect((node) => Effect.log("mounted")));

Animation

CSS-based animations for control flow transitions:

import { when, each, stagger } from "@effex/dom";

// Enter/exit animations
when(isOpen, {
  onTrue: () => Modal(),
  onFalse: () => $.span(),
  animate: {
    enterFrom: "opacity-0 scale-95",
    enterTo: "opacity-100 scale-100",
    exit: "fade-out",
  },
});

// Staggered list animations
each(items, {
  key: (item) => item.id,
  render: (item) => ListItem(item),
  animate: {
    enter: "slide-in",
    exit: "slide-out",
    stagger: stagger(50), // 50ms between items
  },
});

Stagger Functions

| Function | Description | |----------|-------------| | stagger(delayMs) | Fixed delay between items | | staggerFromCenter(delayMs) | Items animate outward from center | | staggerEased(totalDurationMs, easingFn) | Custom easing curve | | delay(delayMs) | Fixed delay for all items | | sequence(...delays) | Sequential delays | | parallel() | All items animate simultaneously |

Virtual List

For large lists (1000+ items), use virtualEach to only render visible items:

import { virtualEach, VirtualListRef } from "@effex/dom";

// Basic usage
virtualEach(items, {
  key: (item) => item.id,
  itemHeight: 48,
  height: 400,
  render: (item) => $.li({}, $.of(item.pipe(Readable.map((i) => i.text)))),
});

With scroll control:

const listRef = yield* VirtualListRef.make();

yield* virtualEach(items, {
  key: (item) => item.id,
  itemHeight: 60,
  height: 400,
  overscan: 5,        // Render 5 extra items above/below viewport
  ref: listRef,
  render: (item, index) => ListItem({ item, index }),
});

// Scroll to item 100
yield* listRef.ready.pipe(
  Effect.flatMap((control) => control.scrollTo(100))
);

// Other controls
control.scrollToTop();
control.scrollToBottom();
control.visibleRange;   // Readable<{ start: number, end: number }>
control.totalItems;     // Readable<number>

Portal

Render children into a different DOM node:

import { Portal } from "@effex/dom";

// Render into document.body (default)
Portal(() => Modal({ title: "Hello" }));

// Render into specific element
Portal({ target: "#modal-root" }, () => Dropdown());
Portal({ target: existingElement }, () => Tooltip());

DOM Utilities

Ref

Create refs to DOM elements for imperative access:

import { ref } from "@effex/dom";

const inputRef = yield* ref<HTMLInputElement>();

// Pass to element
yield* $.input({ ref: inputRef, type: "text" });

// Use later — waits until element is mounted
yield* inputRef.pipe(Element.focus);

// Check connection status
inputRef.isConnected;  // Readable<boolean>

FocusTrap

Trap focus within a container (for modals, dialogs):

import { FocusTrap } from "@effex/dom";

yield* FocusTrap.make({
  container: dialogElement,
  initialFocus: firstInput,        // Optional: focus this element first
  returnFocus: triggerElement,     // Optional: focus returns here on release
});
// Focus is trapped until scope closes

ScrollLock

Prevent body scrolling (for modals). Accounts for scrollbar width to prevent layout shift:

import { ScrollLock } from "@effex/dom";

yield* ScrollLock.lock;
// Body scroll is locked until scope closes

UniqueId

Generate unique IDs for ARIA relationships:

import { UniqueId } from "@effex/dom";

const labelId = yield* UniqueId.make("label");
const inputId = yield* UniqueId.make("input");

yield* $.div({}, collect(
  $.label({ id: labelId, htmlFor: inputId }, $.of("Name")),
  $.input({ id: inputId, "aria-labelledby": labelId }),
));

API Reference

Elements

| Export | Description | |--------|-------------| | $.<element>(attrs?, children?) | Create an HTML/SVG element | | $.of(value) | Lift a primitive or Readable into a child | | $.empty | Empty child (produces no DOM nodes) | | collect(...children) | Combine multiple children | | t\template`| Create reactive template string | |provide(tag, value, children)` | Provide context to children |

Mounting

| Export | Description | |--------|-------------| | mount(element, container) | Mount an element into a DOM container | | runApp(program, options?) | Run an application with scoping and lifecycle | | renderToString(element, options?) | SSR — from @effex/dom/server | | hydrate(element, container, options?) | Hydration — from @effex/dom/hydrate |

Control Flow

| Export | Description | |--------|-------------| | when(condition, config) | Conditional rendering | | match(value, config) | Pattern matching | | each(items, config) | Keyed list rendering | | matchOption(option, config) | Match on Option | | matchEither(either, config) | Match on Either | | redraw(readable, config) | Full redraw on change |

Boundaries

| Export | Description | |--------|-------------| | Boundary.suspense(options) | Async loading boundary with fallback | | Boundary.error(render, catch) | Error boundary |

Animation

| Export | Description | |--------|-------------| | stagger(delayMs) | Linear stagger between items | | staggerFromCenter(delayMs) | Center-out stagger | | staggerEased(totalMs, easingFn) | Easing-based stagger | | delay(delayMs) | Fixed delay | | sequence(...delays) | Sequential delays | | parallel() | Simultaneous animation |

Virtual List

| Export | Description | |--------|-------------| | virtualEach(items, config) | Virtualized list rendering | | VirtualListRef.make() | Create scroll control ref |

DOM Utilities

| Export | Description | |--------|-------------| | ref<T>() | Create element ref | | FocusTrap.make(options) | Trap focus in container | | ScrollLock.lock | Lock body scroll | | UniqueId.make(prefix?) | Generate unique ID | | Portal(options?, children) | Render to different DOM node |

Element Namespace

The Element namespace contains all pipeable manipulation functions. Key categories:

| Category | Functions | |----------|-----------| | Attributes | setAttribute, removeAttribute, toggleAttribute, bindAttribute, bindBooleanAttribute | | Classes | setClass, addClass, removeClass, toggleClass, replaceClass, bindClass | | Styles | setStyle, setStyles, removeStyle, bindStyle | | Data | setData, removeData, getData, bindData | | Content | setTextContent, setInnerHTML, setInputValue, bindTextContent, bindInnerHTML, bindInputValue | | Focus | focus, blur, click, focusFirst, focusLast | | Events | on, once, addEventListener, removeEventListener | | Query | getBoundingClientRect, getId, hasAttribute, contains | | Children | appendChild, clearChildren | | Misc | setRef, tap, tapEffect |

Renderer

| Export | Description | |--------|-------------| | DOMRenderer | DOM implementation of the Renderer interface | | DOMRendererLive | Layer providing DOMRenderer |