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

@fimbul-works/seidr

v0.3.1

Published

Reactive primitives and DOM utilities for building UIs

Readme

Seidr

Seiðr - the Old Norse magic of influence and causality. A lightweight reactive frontend library that weaves together DOM manipulation, reactive bindings, and component lifecycles into elegant, maintainable applications.

Build reactive user interfaces with minimal overhead and maximum control. No virtual DOM, no build step required, just pure functions and reactive primitives.

npm version TypeScript

Features

  • 🪄 Reactive Bindings - Automatic ObservableValue to DOM attribute binding
  • 🎯 Type-Safe Props - TypeScript magic for reactive HTML attributes
  • 🏗️ Component System - Lifecycle management with automatic cleanup
  • 📦 Tiny Footprint - 1.2KB minified + gzipped, no virtual DOM
  • 🔧 Functional API - Simple, composable functions for DOM creation
  • Zero Dependencies - Pure TypeScript, build step optional

Installation

npm install @fimbul-works/seidr
# or
yarn add @fimbul-works/seidr
# or
pnpm install @fimbul-works/seidr

Quick Start

import { component, mount, ObservableValue, DivEl, ButtonEl, SpanEl } from '@fimbul-works/seidr';

function Counter() {
  return component((scope) => {
    const count = new ObservableValue(0);
    const isDisabled = new ObservableValue(false);

    const container = DivEl({
      className: 'counter',
      style: 'padding: 20px; border: 1px solid #ccc;'
    }, [
      SpanEl({ textContent: `Count: ${count.value}` }), // Static initial text
      ButtonEl({
        textContent: 'Increment',
        disabled: isDisabled, // Reactive boolean binding!
        onclick: () => {
          count.value++;
          isDisabled.value = count.value >= 10; // Disable after 10 clicks
        }
      }),
      ButtonEl({
        textContent: 'Reset',
        onclick: () => {
          count.value = 0;
          isDisabled.value = false;
        }
      })
    ]);

    // Reactive binding for text content
    scope.track(
      bind(count, container.children[0] as HTMLSpanElement, (value, el) => {
        el.textContent = `Count: ${value}`;
      })
    );

    return container;
  });
}

// Mount component
const counter = Counter();
mount(counter, document.body);

Core Concepts

Reactive Props

State is stored in ObservableValue<T> which can be used as props.

import { ObservableValue, InputEl, ButtonEl, DivEl } from '@fimbul-works/seidr';

// Create reactive observables
const disabled = new ObservableValue(false);
const className = new ObservableValue('btn-primary');
const maxLength = new ObservableValue(50);
const placeholder = new ObservableValue('Enter text...');

// Automatic reactive bindings - no manual bind() needed!
const input = InputEl({
  type: 'text',
  className, // ObservableValue<string> → className property
  disabled, // ObservableValue<boolean> → disabled property
  maxLength, // ObservableValue<number> → maxLength property
  placeholder // ObservableValue<string> → placeholder property
});

// Any change to the observable automatically updates the DOM
disabled.value = true; // input.disabled becomes true
className.value = 'btn-disabled'; // input.className becomes 'btn-disabled'
maxLength.value = 100; // input.maxLength becomes 100

Manual Reactive Bindings

For complex transformations, use the bind function:

import { ObservableValue, bind, DivEl, SpanEl } from '@fimbul-works/seidr';

const count = new ObservableValue(0);
const container = DivEl();
const display = SpanEl();

// Custom transformation function
const cleanup = bind(count, display, (value, el) => {
  el.textContent = value > 5 ? 'Many clicks!' : `Count: ${value}`;
});

count.value = 3; // display shows "Count: 3"
count.value = 7; // display shows "Many clicks!"

// Later: cleanup() to remove binding

Components with Lifecycle

Components automatically manage cleanup of bindings, child components, and resources:

import { component, mount, ObservableValue, DivEl, SpanEl, ButtonEl, bind } from '@fimbul-works/seidr';

function UserProfile() {
  return component((scope) => {
    const name = new ObservableValue('Alice');
    const age = new ObservableValue(30);
    const isEditing = new ObservableValue(false);

    const container = DivEl({ className: 'user-profile' }, [
      // Reactive name display
      SpanEl({ textContent: name }),

      // Reactive age display
      SpanEl({ textContent: `Age: ${age.value}` }),

      // Edit button with reactive disabled state
      ButtonEl({
        textContent: 'Edit',
        disabled: isEditing,
        onclick: () => isEditing.value = !isEditing.value
      }),

      // Conditional save button
      isEditing.value ? ButtonEl({
        textContent: 'Save',
        onclick: () => isEditing.value = false
      }) : null
    ]);

    // Manual binding for complex transformation
    scope.track(
      bind(age, container.children[1] as HTMLSpanElement, (value, el) => {
        el.textContent = `Age: ${value}`;
      })
    );

    return container;
  });
}

const profile = UserProfile();
mount(profile, document.body);

// When done:
profile.destroy(); // Cleans up all reactive bindings automatically

Conditional Rendering

Show/hide components based on observable conditions:

import { mountConditional, ObservableValue, DivEl, ButtonEl, component } from '@fimbul-works/seidr';

const isVisible = new ObservableValue(false);

function DetailsPanel() {
  return component((scope) => {
    return DivEl({ className: 'details-panel' }, [
      DivEl({ textContent: 'User Details' }),
      DivEl({ textContent: 'Some additional information...' }),
      ButtonEl({
        textContent: 'Close',
        onclick: () => isVisible.value = false
      })
    ]);
  });
}

const conditional = mountConditional(
  isVisible,
  () => DetailsPanel(), // Only created when needed
  document.body
);

isVisible.value = true; // Component automatically mounts and becomes visible
isVisible.value = false; // Component automatically unmounts and cleans up

List Rendering

Efficiently render lists from observable arrays:

import { mountList, ObservableValue, DivEl, SpanEl, ButtonEl, component } from '@fimbul-works/seidr';

const todos = new ObservableValue([
  { id: 1, text: 'Learn Seidr', completed: false },
  { id: 2, text: 'Build amazing apps', completed: false }
]);

function TodoItem({ todo }: { todo: any }) {
  return component((scope) => {
    const isCompleted = new ObservableValue(todo.completed);

    return DivEl({
      className: 'todo-item',
      style: 'display: flex; align-items: center; gap: 10px; margin: 5px 0;'
    }, [
      // Reactive checkbox
      ButtonEl({
        textContent: isCompleted.value ? '✅' : '⭕',
        onclick: () => {
          isCompleted.value = !isCompleted.value;
          todo.completed = isCompleted.value;
        }
      }),

      // Reactive text styling
      SpanEl({
        textContent: todo.text,
        style: isCompleted.value ? 'text-decoration: line-through;' : ''
      })
    ]);
  });
}

const list = mountList(
  todos,
  (item) => item.id, // Key function for efficient updates
  (item) => TodoItem({ todo }), // Component factory
  document.body
);

// Updates efficiently handle additions, removals, and reordering
todos.value = [...todos.value, { id: 3, text: 'Master reactive programming', completed: false }];
todos.value = todos.value.filter(todo => todo.id !== 1); // Remove item

Advanced Patterns

Derived Values

Create derived observables that update automatically when the original value changes:

import { ObservableValue } from '@fimbul-works/seidr';

const isActive = new ObservableValue(false);
const activeClass = isActive.derive((v) => v ? 'active' : '');

isActive.value = true;
// activeClass.value is now 'active'

Computed Values

Create aggregating and derived observables that automatically update when dependencies change:

import { ObservableValue, computed, DivEl, SpanEl } from '@fimbul-works/seidr';

const firstName = new ObservableValue('John');
const lastName = new ObservableValue('Doe');

// Computed full name that updates when either first or last name changes
const fullName = computed(
  () => `${firstName.value} ${lastName.value}`,
  [firstName, lastName]
);

const profile = DivEl([
  SpanEl({ textContent: 'First Name:' }),
  SpanEl({ textContent: firstName }),
  SpanEl({ textContent: 'Last Name:' }),
  SpanEl({ textContent: lastName }),
  SpanEl({ textContent: 'Full Name:' }),
  SpanEl({ textContent: fullName }) // Automatically updates!
]);

firstName.value = 'Jane'; // fullName becomes "Jane Doe"

Two-Way Binding

Bind form inputs to observables with automatic synchronization:

import { ObservableValue, InputEl, SpanEl, bind, DivEl } from '@fimbul-works/seidr';

const searchText = new ObservableValue('');

const searchComponent = DivEl([
  // Input that updates the observable
  InputEl({
    type: 'text',
    placeholder: 'Search...',
    value: searchText, // Reactive initial value
    oninput: (e) => searchText.value = e.target.value
  }),

  // Display that shows the current search text
  SpanEl({ textContent: searchText }) // Reactive display!
]);

// Manual binding for bidirectional sync
const cleanup = bind(searchText, searchComponent.children[0] as HTMLInputElement, (value, el) => {
  if (el !== document.activeElement) { // Don't update while user is typing
    el.value = value;
  }
});

License

MIT License - See LICENSE file for details.


Built with ⚡ by FimbulWorks