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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@linttrap/oem

v2.2.6

Published

A novel UI library for writing reactive html with vanilla javascript

Readme

OEM - The Roll Your Own Framework Framework

Full Docs at oem.js.org

OEM is a minimal, convention-driven toolkit for crafting your own reactive, component-based UI layer—fully local, fully understood, and entirely yours.

TOC

Philosophy

Transparency by Design

OEM’s primary goal is transparency. It strips away the “black box” complexity of modern frameworks and replaces it with small, local behaviors you can actually reason about.

  • Readable Core: ~300 lines of plain TypeScript you can grasp in a single sitting
  • Local Behavior: Features are implemented as Traits—small files that live next to your markup and are meant to be copied, edited, and extended
  • No Hidden Magic: Everything reduces to simple reactive patterns (pub/sub, observers, state flows) that you can inspect and reshape

Why OEM?

  • ✓ ~2.7KB minified core, zero dependencies
  • ✓ Reactive DOM with no virtual DOM layer
  • ✓ Locality of behavior makes reasoning—and debugging—trivial
  • ✓ AI can generate Traits directly, and you can understand and refine every line
  • ✓ Full TypeScript types without framework overhead
  • ✓ Copy only what you need; no bulk, no lock-in

OEM is small enough for humans to master and AI to extend—a feedback loop where you understand the code, adjust it, and let the AI build on your exact patterns.

Installation

Download

ke it yours! You don't need to "install" anything. Use the OEM Download Generator to customize and download a package with only the Traits and States you need.

Using npm

npm install @linttrap/oem

🎯 Quick Start

This is the simplest way to show how State and Templating work together to create a reactive component.

// 1. Configure the template with needed traits
const [tag, trait] = Template({
  event: useEventTrait,
  style: useStyleTrait,
});

// 2. Create reactive state
const count = State(0);

// 3. Generate DOM with reactive bindings
const app = tag.div(
  // Reactive text: auto-updates when count changes
  tag.h1(count.$val),

  // Style trait: applies CSS styles
  // Event trait: uses the $ pattern for clean syntax
  tag.button(
    trait.style('padding', '10px'),
    trait.style('font-size', '16px'),
    trait.event(
      'click',
      count.$reduce((n) => n + 1),
    ),
    'Increment',
  ),
);

document.body.appendChild(app);

How It Works

Understanding the complete reactive loop:

  1. Create State: const count = State(0);
  2. Configure Template with Traits: const [tag, trait] = Template({ event: useEventTrait });
  3. Build Elements with Reactive Bindings:
    const app = tag.div(
      tag.h1(count.$val), // Template sees $val and subscribes
      tag.button(
        trait.event(
          'click',
          count.$reduce((n) => n + 1),
        ),
        'Increment',
      ),
    );
  4. Behind the Scenes:
    • Template detects count.$val and automatically subscribes to state changes
    • When button is clicked, count.$reduce updates the state
    • State notifies all subscribers (including the h1's text node)
    • UI updates automatically without manual DOM manipulation

This is the entire reactive loop: No virtual DOM diffing, no complex lifecycle hooks. Just pub/sub with smart subscription management via WeakMap and MutationObserver for cleanup.

State

The State object provides simple reactive state management using the pub/sub pattern.

| Method | $ Version | Description | | :----------- | :---------------- | :-------------------------------------------------- | | val() | $val() | Get the value | | set(v) | $set(v) | Set a new value | | reduce(fn) | $reduce(fn) | Update value based on the previous value | | sub(cb) | N/A | Subscribe to state changes (returns unsubscribe fn) | | test(p) | $test(p) | Test if the value matches a predicate/condition | | call(m) | $call(m) | Call methods on boxed primitives |

Understanding the $ Pattern

The dollar sign ($) prefix on State methods is essential for reactivity and clean syntax:

  1. Reactive UI Updates: When you use a $ method inside a template function (e.g., tag.h1(count.$val)), the template automatically subscribes to that state. When the state changes, the UI updates.
  2. Clean Event Handlers: It returns a closure (a function that executes later), allowing for clean, wrapper-free event binding.
// Verbose: Needs an arrow function wrapper
trait.event('click', () => count.set(0));

// Clean: Use the $ pattern
trait.event('click', count.$set(0));

Ready-Made States

state utilities are NOT bundled - you copy ready-made implementations from src/states/ into your project or build your own.

Available States

| State | Description | | :------------------- | :------------------------------------------------------- | | useMediaQueryState | Reactive media query state that updates on window resize |

Example Usage

import { useMediaQueryState } from '@linttrap/oem/states/MediaQuery';

// Create reactive state for mobile breakpoint
const isMobile = useMediaQueryState({ maxWidth: 768 });

// Use in your UI
const nav = tag.nav(trait.style('display', 'block', isMobile.$test(true)), 'Mobile Navigation');

// Desktop breakpoint
const isDesktop = useMediaQueryState({ minWidth: 1024 });

More coming soon! We're adding router state, form state, async data state, and more. Check src/states/ for updates.

Templating

The Template function creates the DOM-building tools you need by configuring available Traits.

Configuration:

import { Template } from '@linttrap/oem';
import { useStyleTrait } from '@linttrap/oem/traits/Style';
import { useEventTrait } from '@linttrap/oem/traits/Event';

// This returns two proxies:
const [tag, trait] = Template({
  style: useStyleTrait,
  event: useEventTrait,
});
  • The tag Proxy: Creates standard HTML and SVG elements (tag.div(), tag.h1(), tag.svg(), etc.) with full TypeScript support
  • The trait Proxy: Provides access to the configured trait functions (trait.style(...), trait.event(...), etc.)

Components and Children:

// Components are just functions that return elements
function Button(text: string, onClick: () => void) {
  return tag.button(trait.event('click', onClick), text);
}

const app = tag.div(Button('Click Me', () => console.log('Hi')));

Storage

The Storage utility automatically manages and syncs state objects with web storage (localStorage, sessionStorage) or custom sync methods.

import { Storage, State } from '@linttrap/oem';

const storage = Storage({
  data: {
    username: {
      key: 'app-username',
      state: State(''),
      storage: 'localStorage', // Persists across sessions
    },
  },
  sync: {
    // Custom method to load data from an API
    fetchTodos: async () => {
      // ... API fetch logic ...
      storage.data.todos.set(todos);
    },
  },
});

// Access state directly
storage.data.username.set('Alice'); // Auto-saves to localStorage

Traits

A Trait is a function that applies behavior to a DOM element.

Key Concept: Localized Behavior

Traits keep behavior directly alongside your markup, preserving Locality of Behavior. You can attach multiple traits—even multiple of the same kind—to a single element. This produces a clean, declarative syntax that eliminates messy conditionals and manual DOM manipulation.

tag.input(
  trait.value(name.$val), // Input value binding
  trait.event('input', handler), // Event handler
  trait.style('color', 'red', isAlert.$test(true)), // conditional style
  trait.style('color', 'blue', isAlert.$test(false)), // conditional style
);

Trait Availability and Customization

Traits are your framework: you build and manage your own library of traits. Ready-made traits live in src/traits/. Simply copy what you need into your project, and customize or extend them as you like.

Ready-Made Traits

| Trait | Description | | :-------------------- | :----------------------------------------------- | | useAttributeTrait | Apply HTML attributes (disabled, type, etc.) | | useStyleTrait | Apply CSS styles | | useEventTrait | Attach event listeners | | useInputValueTrait | Bind input values to state | | useInnerHTMLTrait | Set innerHTML reactively (useful for lists) | | useClassNameTrait | Manage CSS classes | | useFocusTrait | Control element focus | | useTextContentTrait | Set text content reactively |

Creating Custom Traits

A trait is simply a function whose first argument is the element it modifies, and it returns a cleanup function. All behavior—including "reactivity"—is handled inside the trait itself. Here’s the basic anatomy of a reactive trait:

function useMyCustomTrait(
  el: HTMLElement,
  aCustomProperty: string,
  anotherCustomProperty: number,
  ...rest: (StateType<any> | Condition)[]
) {
  // Separate State objects from static conditions
  const isStateObj = (i: any) => Object.keys(i).includes('sub');
  const states = [val ?? '', ...rest].filter(isStateObj) as StateType<any>[];
  const conditions = rest.filter((item) => !isStateObj(item));

  // 1. Define the logic that applies the behavior
  const apply = () => {
    // YOUR CODE GOES HERE: Apply text, change style, etc.
  };

  // 2. Initial application
  apply();

  // 3. Subscribe to all passed State objects
  const states = rest.filter(/* ... logic to find state objects ... */);
  const unsubs = states.map((state) => state.sub(apply));

  // 4. Return cleanup function (crucial for memory management)
  return () => unsubs.forEach((unsub) => unsub());
}

🌐 Browser Support

Requires ES6+ support:

  • Chrome 49+
  • Firefox 18+
  • Safari 10+
  • Edge 12+

📄 License

MIT License

©Copyright 2024. All rights reserved. Made in the USA 🇺🇸 by Lint Trap Media.