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

@snappjs/core

v3.0.0

Published

A lightweight JSX-like framework for vanilla JavaScript with full TypeScript support

Readme

Snapp Framework

A lightweight JSX/TSX framework for building fast, reactive web applications with direct DOM manipulation - no virtual DOM overhead.

License: MIT npm version


📋 Table of Contents


What is Snapp?

Snapp is a lightweight JavaScript framework that compiles JSX/TSX directly to native DOM operations. It's designed for developers who want:

  • JSX/TSX syntax you already know
  • Direct DOM control without abstraction layers
  • Reactive state that updates elements individually
  • Zero virtual DOM - just compiled JavaScript
  • TypeScript support out of the box
  • Automatic memory management with built-in cleanup

Why Snapp?

| Feature | Snapp | Virtual DOM Frameworks | | -------------- | ----------------- | ------------------------- | | Learning Curve | Native DOM skills | New abstractions | | Performance | Direct DOM | Reconciliation overhead | | Debugging | Browser DevTools | Framework DevTools needed | | Memory | Efficient cleanup | GC dependent |


Core Concepts

1. JSX Compiles to DOM Operations

// What you write:
<button onClick={() => alert("Hi")}>Click me</button>;

// Gets compiled to:
snapp.create("button", { onClick: () => alert("Hi") }, "Click me");

2. Dynamic State with Arrow Functions

The key difference in Snapp is how you handle reactive values:

const count = snapp.dynamic(0);

// ❌ WRONG - accesses value once at render time
<p>{count.value}</p>

// ✅ CORRECT - wrapped in arrow function for reactivity
<p>{() => count.value}</p>

// When count updates, ONLY this text updates:
count.update(5);

Why arrow functions? Snapp tracks dynamic value access inside the function. When you call count.update(), it re-executes that function and updates just that specific text node, attribute, or style.

3. Regular Variables Stay Static

const staticText = "Hello";
const dynamicText = snapp.dynamic("World");

<div>
  {staticText} {/* Static - never changes */}
  {() => dynamicText.value} {/* Dynamic - updates when dynamicText changes */}
</div>;

Installation

npm install @snappjs/core

Basic Setup

<!DOCTYPE html>
<html>
  <head>
    <title>My Snapp App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="src/index.js"></script>
  </body>
</html>
// src/index.jsx
import snapp from "@snappjs/core";

const App = () => {
  return <h1>Hello Snapp!</h1>;
};

const app = document.getElementById("app");
snapp.render(app, App());

Quick Start

Example 1: Simple Counter

import snapp from "@snappjs/core";

const Counter = () => {
  const count = snapp.dynamic(0);

  return (
    <>
      <h2>Count: {() => count.value}</h2>
      <button onClick={() => count.update(count.value + 1)}>Increment</button>
    </>
  );
};

snapp.render(document.body, Counter());

Example 2: Todo List

import snapp from "@snappjs/core";

const TodoApp = () => {
  const todos = snapp.dynamic([]);
  const input = snapp.dynamic("");

  const addTodo = () => {
    const newTodos = [...todos.value, input.value];
    todos.update(newTodos);
    input.update("");
  };

  return (
    <div>
      <h1>Todos</h1>
      <input
        value={() => input.value}
        onInput={(e) => input.update(e.target.value)}
        placeholder="Add a todo..."
      />
      <button onClick={addTodo}>Add</button>
      <ul>{() => todos.value.map((todo) => <li>{todo}</li>)}</ul>
    </div>
  );
};

snapp.render(document.body, TodoApp());

API Reference

snapp.create()

Creates a DOM element, component, or fragment.

create(
    element: string | Component | "<>",
    props?: SnappProps,
    ...children: SnappChild[]
): Element | DocumentFragment

Usage:

// HTML element
snapp.create("div", { id: "main" }, "Hello");

// Component
const MyComponent = (props) => <div>{props.children}</div>;
snapp.create(MyComponent, {}, "Hello");

// Fragment
snapp.create("<>", null, <div>A</div>, <div>B</div>);

// In JSX, you use the angle brackets directly:
<div id="main">Hello</div>
<MyComponent>Hello</MyComponent>
<>
    <div>A</div>
    <div>B</div>
</>

snapp.render()

Renders a component or element to the DOM.

render(
    target: Element,
    component: Element | DocumentFragment | string | number,
    type?: "replace" | "append" | "prepend" | "before" | "after",
    callback?: (success: boolean) => void
): void

Parameters:

| Parameter | Type | Default | Description | | ----------- | ----------------------------------------------- | ----------- | ----------------------------------------- | | target | Element | required | The DOM element to render to | | component | Element | DocumentFragment | string | number | required | What to render | | type | string | "replace" | Where/how to render | | callback | Function | optional | Called with true/false on success/failure |

Render Types:

const content = <div>New content</div>;
const target = document.getElementById("app");

// Replace target's children
snapp.render(target, content, "replace"); // Default

// Add as first child
snapp.render(target, content, "prepend");

// Add as last child
snapp.render(target, content, "append");

// Insert before target
snapp.render(target, content, "before");

// Insert after target
snapp.render(target, content, "after");

// With callback
snapp.render(target, content, "replace", (success) => {
  if (success) console.log("Rendered!");
});

snapp.dynamic()

Creates a reactive state value.

dynamic<T = any>(initialValue?: T): DynamicValue<T>

Properties:

interface DynamicValue<T> {
  readonly value: T; // Get current value
  update: (newValue: T) => void; // Set new value
}

Usage:

// Create dynamic state
const count = snapp.dynamic(0);
const user = snapp.dynamic({ name: "John", age: 30 });
const isVisible = snapp.dynamic(true);

// Use in JSX with arrow function
<div>
  Count: {() => count.value}
  Name: {() => user.value.name}
  Visible: {() => (isVisible.value ? "Yes" : "No")}
</div>;

// Update state
count.update(5);
user.update({ name: "Jane", age: 25 });
isVisible.update(false);

// Update based on previous value
count.update(count.value + 1);

Dynamic State in Different Contexts:

// Text Content
const message = snapp.dynamic("Hello");
<p>{() => message.value}</p>;

// Attributes
const id = snapp.dynamic("item-1");
<div id={() => id.value}></div>;

// Styles
const color = snapp.dynamic("blue");
<p style={{ color: () => color.value }}>Colored text</p>;

// Event Handlers (regular functions)
const handleClick = () => alert("clicked");
<button onClick={handleClick}>Click</button>;

// Conditional Rendering
const showHeader = snapp.dynamic(true);
<>{() => (showHeader.value ? <header>Title</header> : null)}</>;

snapp.on()

Listens for DOM ready events.

on(event: string, callback: () => void): void

Usage:

// Wait for DOM to be ready before accessing elements
snapp.on("DOM", () => {
  const element = snapp.select("#myElement");
  console.log("Element is in DOM:", element);
});

// snapp.on("DOM") is called after snapp.render() completes
const App = () => {
  return <h2 id="myElement">Title</h2>;
};

snapp.render(document.body, App(), "replace", () => {
  // At this point, the DOM is ready
  snapp.on("DOM", () => {
    console.log("Now we can access #myElement");
  });
});

snapp.select() / snapp.selectAll()

Query DOM elements using CSS selectors.

select(selector: string | string[]): Element | Element[] | null
selectAll(selector: string | string[]): NodeListOf<Element> | NodeListOf<Element>[] | null

Usage:

// Single selector
const element = snapp.select("#myId");
const element = snapp.select(".myClass");

// Multiple selectors (returns array)
const elements = snapp.select(["#id1", "#id2"]);

// Select all matching
const items = snapp.selectAll(".item");
const items = snapp.selectAll(".item, .product");

// Multiple selectors for selectAll
const results = snapp.selectAll([".class1", ".class2"]);
// Returns array of NodeLists

// Returns null if not found
const missing = snapp.select("#doesNotExist"); // null

snapp.applystyle() / snapp.removestyle()

Manage element styles programmatically.

applystyle(
    element: Element | Element[],
    styles: Record<string, string | number>
): void

removestyle(
    element: Element | Element[],
    styles?: Record<string, string | number> | boolean
): void

Usage:

const box = snapp.select("#box");

// Apply styles
snapp.applystyle(box, {
  backgroundColor: "blue",
  padding: "20px",
  "border-radius": "8px", // CSS property names with hyphens work
});

// Remove specific styles
snapp.removestyle(box, {
  backgroundColor: "blue",
  padding: "20px",
});

// Remove all styles
snapp.removestyle(box, true);

// Multiple elements
const boxes = snapp.selectAll(".box");
snapp.applystyle(boxes, { color: "red" });
snapp.removestyle(boxes, { color: "red" });

snapp.remove()

Remove elements from the DOM.

remove(items: Element | Element[]): void

Usage:

const element = snapp.select("#myElement");
snapp.remove(element);

// Remove multiple
const items = snapp.selectAll(".item");
snapp.remove(items);

Type Definitions

SnappChild

type SnappChild =
  | string
  | number
  | Element
  | DocumentFragment
  | SnappComponent
  | SnappChild[]
  | null
  | undefined
  | boolean;

Represents anything that can be rendered as a child element.

SnappProps

type SnappProps = Record<string, any>;

Props object for components. Can contain any key-value pairs.

SnappComponent

type SnappComponent<P extends SnappProps = SnappProps> = (
  props: P & { children?: SnappChild[] }
) => Element | DocumentFragment;

A component function that takes props and returns a DOM element or fragment.

Example:

interface ButtonProps {
  label: string;
  onClick?: (e: Event) => void;
}

const MyButton: SnappComponent<ButtonProps> = (props) => {
  return <button onClick={props.onClick}>{props.label}</button>;
};

DynamicValue

interface DynamicValue<T = any> {
  readonly value: T;
  update: (newValue: T) => void;
}

Reactive state container that notifies subscribers when updated.

RenderType

type RenderType = "before" | "prepend" | "replace" | "append" | "after";

Determines where elements are rendered relative to the target.

EventHandler

type EventHandler = (event: Event) => void;

Function called when an event fires.

HTMLAttributes

Comprehensive attribute interface supporting:

  • Global attributes: id, class, style, title, etc.
  • Data attributes: data-*
  • ARIA attributes: aria-*
  • All event handlers: onClick, onSubmit, onChange, etc.

Core Architecture

How Snapp Works

  1. JSX Compilation → TypeScript/Babel compiles JSX to snapp.create() calls
  2. Element Creationsnapp.create() builds native DOM elements
  3. Dynamic Tracking → When you use () => dynamicValue.value, Snapp tracks dependencies
  4. Subscriptions → Each dynamic value tracks which elements depend on it
  5. Updates → When you call update(), only affected text nodes/attributes/styles change
  6. Cleanup → MutationObserver automatically cleans up when elements are removed

Internal Files

src/core.ts (Main Implementation)

The heart of Snapp framework containing:

Key Functions:

  • create(element, props, ...children) - Creates DOM elements or components
  • render(target, component, type, callback) - Renders to DOM
  • dynamic(initialValue) - Creates reactive state
  • on(event, callback) - Listens for DOM events
  • select(selector) / selectAll(selector) - DOM queries
  • applystyle(element, styles) - Apply CSS styles
  • removestyle(element, styles) - Remove CSS styles
  • remove(items) - Remove elements from DOM

Internal State Management:

// Counter for unique element IDs
let dataId: number = 0;

// Counter for dynamic state IDs
let dynamicId: number = 1;

// Tracks if DOM is ready
let DOMReady: boolean = false;

// Tracks which dynamic values are being accessed
let track_dynamic: Set<string> | null = null;

// Stores all dynamic values and their subscribers
const dynamicData: Record<
  string,
  {
    value: any;
    subscribe: Map<Element, number[]>;
  }
> = {};

// Maps elements to their dependent dynamic values
const dynamicDependencies = new Map<Element, SubscribeData[]>();

// Event delegation system
const eventListener: Record<string, EventHandler> = {};
const elementEvent: Record<string, Record<number, EventHandler>> = {};

SVG Support:

Snapp automatically detects SVG elements and uses createElementNS() instead of createElement():

const SVG_ELEMENTS = new Set([
    "svg", "circle", "ellipse", "line", "path", "polygon", "polyline",
    "rect", "text", "g", "defs", "filter", "image", "use", "mask", "pattern",
    "linearGradient", "radialGradient", "stop", "animate", "animateTransform", ...
]);

Event Delegation:

Rather than adding listeners to every element, Snapp uses event delegation:

// Single listener per event type
document.addEventListener("click", eventTemplate);

// Template checks if target has snapp-e-click attribute
const elWithAttr = target.closest("[snapp-e-click]");
if (elWithAttr) {
  // Get element's ID and call its handler
  const elementDataId = elWithAttr.getAttribute("snapp-data");
  elementEvent["click"][elementDataId](event);
}

This is more efficient than listeners on every element.

Memory Management:

MutationObserver watches for removed elements:

const observer = new MutationObserver((mutations) => {
  mutations.forEach((element) => {
    element.removedNodes.forEach((node) => {
      // Clean up event listeners
      if (node.getAttribute("snapp-e-click")) {
        delete eventEvent["click"][elementDataId];
      }
      // Clean up dynamic dependencies
      if (node.getAttribute("snapp-dynamic")) {
        dynamicDependencies.delete(node);
      }
    });
  });
});

src/types.ts (Type Definitions)

Comprehensive TypeScript types for:

  • Component types: SnappComponent, SnappChild, SnappProps
  • State types: DynamicValue, SubscribeData
  • Attribute types: HTMLAttributes, IntrinsicElements
  • Handler types: EventHandler, RenderType

SubscribeData Interface:

interface SubscribeData {
  type: "node" | "attr" | "style"; // What's being updated
  temp: Function; // Function to re-execute
  subscribe: string[]; // Dynamic IDs this depends on
  node?: Text; // Text node being updated
  attr?: string; // Attribute name being updated
  prop?: string; // Style property being updated
}

This tracks what type of update happens when a dynamic value changes.

src/jsx.d.ts (JSX Support)

Ambient type declarations for JSX:

declare global {
  namespace JSX {
    interface IntrinsicElements {
      // All HTML/SVG elements with their attributes
      div: HTMLAttributes<HTMLDivElement>;
      button: HTMLAttributes<HTMLButtonElement>;
      svg: HTMLAttributes<SVGSVGElement>;
      // ... etc
    }
    interface ElementAttributesProperty {
      props: any;
    }
    interface ElementChildrenAttribute {
      children: any;
    }
  }
}

This enables TypeScript to recognize JSX syntax and provide autocomplete for elements and attributes.

src/index.ts (Entry Point)

import snapp from "./core";
export default snapp;
export type { DynamicValue, SnappProps, SnappComponent, SnappChild };

Single entry point that re-exports the framework and types.

Dependency Tracking Algorithm

When you write:

const count = snapp.dynamic(0);
<p>{() => count.value}</p>;

Snapp does the following:

  1. Start tracking: Set track_dynamic = new Set()
  2. Execute function: Call () => count.value, which accesses the value getter
  3. Track access: Inside value getter, add ID to track_dynamic
  4. Create subscription: Store the function and which dynamic values it depends on
  5. On update: When count.update(5) is called, re-execute the function and update the text node

This is why arrow functions are essential - they defer execution so Snapp can track what's accessed.


Contributing

We welcome contributions! Here's how to get involved:

Development Setup

# Install dependencies
npm install

# Start development watch mode
npm run dev

# Build for production
npm run build

Code Style

  • Use TypeScript for type safety
  • Follow existing code patterns in src/core.ts
  • Add JSDoc comments for public APIs
  • Test with JSX examples

Pull Request Process

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes with clear commit messages
  4. Ensure code compiles: npm run build
  5. Submit PR with description

License

MIT - See LICENSE file for details


Support