A lightweight JSX/TSX framework for building fast, reactive web applications with direct DOM manipulation - no virtual DOM overhead.
- What is Snapp?
- Core Concepts
- Installation
- Quick Start
- API Reference
- Type Definitions
- Core Architecture
- Contributing
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
| 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 |
// What you write:
<button onClick={() => alert("Hi")}>Click me</button>;
// Gets compiled to:
snapp.create("button", { onClick: () => alert("Hi") }, "Click me");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.
const staticText = "Hello";
const dynamicText = snapp.dynamic("World");
<div>
{staticText} {/* Static - never changes */}
{() => dynamicText.value} {/* Dynamic - updates when dynamicText changes */}
</div>;npm install @snappjs/core<!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());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());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());Creates a DOM element, component, or fragment.
create(
element: string | Component | "<>",
props?: SnappProps,
...children: SnappChild[]
): Element | DocumentFragmentUsage:
// 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>
</>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
): voidParameters:
| 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!");
});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)}</>;Listens for DOM ready events.
on(event: string, callback: () => void): voidUsage:
// 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");
});
});Query DOM elements using CSS selectors.
select(selector: string | string[]): Element | Element[] | null
selectAll(selector: string | string[]): NodeListOf<Element> | NodeListOf<Element>[] | nullUsage:
// 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"); // nullManage element styles programmatically.
applystyle(
element: Element | Element[],
styles: Record<string, string | number>
): void
removestyle(
element: Element | Element[],
styles?: Record<string, string | number> | boolean
): voidUsage:
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" });Remove elements from the DOM.
remove(items: Element | Element[]): voidUsage:
const element = snapp.select("#myElement");
snapp.remove(element);
// Remove multiple
const items = snapp.selectAll(".item");
snapp.remove(items);type SnappChild =
| string
| number
| Element
| DocumentFragment
| SnappComponent
| SnappChild[]
| null
| undefined
| boolean;Represents anything that can be rendered as a child element.
type SnappProps = Record<string, any>;Props object for components. Can contain any key-value pairs.
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>;
};interface DynamicValue<T = any> {
readonly value: T;
update: (newValue: T) => void;
}Reactive state container that notifies subscribers when updated.
type RenderType = "before" | "prepend" | "replace" | "append" | "after";Determines where elements are rendered relative to the target.
type EventHandler = (event: Event) => void;Function called when an event fires.
Comprehensive attribute interface supporting:
- Global attributes:
id,class,style,title, etc. - Data attributes:
data-* - ARIA attributes:
aria-* - All event handlers:
onClick,onSubmit,onChange, etc.
- JSX Compilation → TypeScript/Babel compiles JSX to
snapp.create()calls - Element Creation →
snapp.create()builds native DOM elements - Dynamic Tracking → When you use
() => dynamicValue.value, Snapp tracks dependencies - Subscriptions → Each dynamic value tracks which elements depend on it
- Updates → When you call
update(), only affected text nodes/attributes/styles change - Cleanup → MutationObserver automatically cleans up when elements are removed
The heart of Snapp framework containing:
Key Functions:
create(element, props, ...children)- Creates DOM elements or componentsrender(target, component, type, callback)- Renders to DOMdynamic(initialValue)- Creates reactive stateon(event, callback)- Listens for DOM eventsselect(selector)/selectAll(selector)- DOM queriesapplystyle(element, styles)- Apply CSS stylesremovestyle(element, styles)- Remove CSS stylesremove(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);
}
});
});
});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.
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.
import snapp from "./core";
export default snapp;
export type { DynamicValue, SnappProps, SnappComponent, SnappChild };Single entry point that re-exports the framework and types.
When you write:
const count = snapp.dynamic(0);
<p>{() => count.value}</p>;Snapp does the following:
- Start tracking: Set
track_dynamic = new Set() - Execute function: Call
() => count.value, which accesses thevaluegetter - Track access: Inside
valuegetter, add ID totrack_dynamic - Create subscription: Store the function and which dynamic values it depends on
- 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.
We welcome contributions! Here's how to get involved:
# Install dependencies
npm install
# Start development watch mode
npm run dev
# Build for production
npm run build- Use TypeScript for type safety
- Follow existing code patterns in
src/core.ts - Add JSDoc comments for public APIs
- Test with JSX examples
- Fork the repository
- Create a feature branch
- Make your changes with clear commit messages
- Ensure code compiles:
npm run build - Submit PR with description
MIT - See LICENSE file for details