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

tacit-dom

v0.6.2

Published

A React-like library with reactive signals and computed values for building dynamic web applications—without the need for JSX

Readme

Tacit-DOM Banner

A React-like library with reactive signals and computed values for building dynamic web applications—without the need for JSX.

🤔 Why the Name "Tacit-DOM"?

Tacit knowledge—the kind of understanding you possess intuitively, without needing explicit instructions or formal training. Tacit DOM is designed to be so intuitive you should be able to build apps with it in minutes.

The name is inspired by the Northrop Grumman stealth demonstrator aircraft Tacit Blue, developed for the U.S. Air Force’s research programme. This experimental platform pioneered advances, which directly influenced the next generation of stealth aircraft, including the Nighthawk and B-2 Spirit.”

🚫 Why Not Just Continue Using React?

React Has Peaked: It Solved the DOM Problem — But Not Today’s Problems.

FaxJS was an internal Facebook prototype by Jordan Walke in 2011 that introduced the virtual DOM concept, serving as the foundation for what became ReactJS.

React’s virtual DOM was revolutionary when browsers were slow and UI updates were costly. But now, with modern DOM APIs and frameworks like Solid, Svelte, and signals-based architectures, the VDOM is an expensive abstraction. React solved yesterday’s bottlenecks, not today’s.

Community Innovation Is Moving Elsewhere

The fastest-moving ideas (signals, resumability, fine-grained reactivity) are not coming from React anymore. Solid, Qwik, Vue 3’s composition API, and Svelte are demonstrating more elegant and performant models. React is becoming the “legacy default”, not the future driver.

Hooks and Providers Add Cognitive Complexity

React initially promised simplicity — components as functions. Over time, hooks, contexts, and providers have piled up layers of implicit state, dependency rules, and fragile lifecycles. It’s now harder to reason about state than in simpler reactive models. That’s a sign of maturity tipping into decline.

Why Providers Are Terrible

Deep Nesting Hell: Providers lead to “provider pyramids” — readability nightmare where the app tree is wrapped in multiple contexts just to pass config/state around.

Over-Re-rendering: Context updates cause every consumer to re-render, even if only a small slice of the state changed. That’s wasteful.

Global State Masquerading as Local: Context is meant for “infrequent global config” but is now misused for business logic and state sharing, making reasoning about boundaries harder.

Opaque Performance: Developers often don’t realise performance pitfalls until too late. Debugging unnecessary renders in provider-based systems is painful.

Why Hooks Are Error-Prone

Rules of Hooks Are a Runtime Tax: Needing linters and mental discipline just to ensure hooks aren’t called in the wrong order shows the API is fragile.

  • Hidden Dependencies: useEffect dependencies are error-prone by design. Either you forget dependencies (leading to bugs), or you add everything (causing infinite loops).
  • Boilerplate Instead of Clarity: Complex hook composition often obscures intent. Instead of simple, declarative state, you end up juggling effect cleanup, stale closures, and race conditions.

Concurrency Makes It Worse: React 18’s concurrent features amplify these issues — async rendering often exposes subtle bugs in hooks logic.

Why Redux and Similar State Managers Are Hacks

  • Boilerplate Explosion: Reducers, actions, dispatchers — a ceremony to model state that should just be reactive data.
  • Single Store Centralisation: One giant object pretending to solve state management leads to brittle dependencies and unnecessary coupling.

Extra Layer of Indirection: You don’t manipulate state directly — you must describe it, dispatch it, reduce it, then subscribe to it. This indirection adds complexity but doesn’t remove the fundamental re-rendering inefficiency.

Ecosystem Gravity: Redux exists largely to patch React’s weaknesses around state propagation. The fact it was even necessary shows React’s core model was incomplete.

The Positive Case for Building Something Better

  • Fine-Grained Reactivity: State changes should propagate only where needed, without full component re-renders. (Think Svelte or Solid.)
  • Explicit, Declarative State Models: Move away from implicit lifecycles and fragile hooks to clear reactive primitives.
  • Tree-Shakeable, Lightweight Runtime: Build frameworks that compile away boilerplate rather than ship a heavy runtime.
  • Resumability & Edge-Readiness: Future apps need frameworks optimised for streaming, islands, and instant hydration — things React is bolting on, but not designed for.

Developer Experience First: Simpler mental models. No “rules of hooks”. No endless provider pyramids. Just state and UI, directly connected.

NPM Version License TypeScript Build Status

📋 Table of Contents

⚠️ Project Status

Current Status: Experimental Proof of Concept

  • 🚧 Experimental: APIs are evolving and may change without notice
  • 🧪 Proof of Concept: Designed to explore reactive programming patterns
  • ⚠️ Not Production Ready: Limited test coverage and may break with non-trivial use cases
  • 🔄 Work in Progress: Subject to significant changes and improvements

Note: If you need a stable, production-ready solution, consider established alternatives like React, Vue, or Svelte.

✨ Features

🚀 Core Reactivity

  • ⚡ Reactive Signals: Create reactive state that automatically updates when dependencies change
  • 🧮 Computed Values: Derive values from signals with automatic dependency tracking
  • 🌍 Global State Management: Create global state anywhere without providers, context, or complex setup
  • 🧹 Automatic Cleanup: Prevents memory leaks with smart cleanup

🎨 DOM & Components

  • 🚫 No Virtual DOM: Direct DOM updates without the overhead of virtual DOM reconciliation
  • 🧩 Component Pattern: Build components using a familiar JSX-like syntax
  • 🎭 Conditional Rendering: Built-in when function for reactive conditional content
  • 📋 List Rendering: Powerful map function with optional filtering for dynamic lists
  • 🧩 Conditional Rendering: when function for reactive conditional content without wrappers
  • 🎯 Event Handling: Comprehensive DOM event support including mouse, keyboard, touch, pointer, clipboard, selection, composition, animation, transition, media, and drag & drop events
  • 🎨 Style Support: React-like style props with reactive updates

🛠️ Developer Experience

  • 🔒 TypeScript Support: Full TypeScript support with type safety
  • 📦 Zero Dependencies: Lightweight with no external dependencies
  • ⚡ Optimized Bundles: Multiple formats (ESM, UMD, CJS) with Rollup
  • 🎯 Tree-shaking: Individual modules for optimal bundling

🚀 Why Tacit-DOM?

React has transformed web development, but state management complexity remains a significant pain point. Tacit-DOM offers a simpler, signal-first approach that addresses these fundamental issues:

🎯 Key Advantages

| Feature | React | Tacit-DOM | | --------------------- | --------------------------------------------------- | ---------------------------------------- | | State Management | useState, useContext, useReducer, Redux, etc. | Simple signals, anywhere | | Re-renders | Component-level re-renders | Granular DOM updates only | | Dependencies | Manual dependency arrays (useEffect, useMemo) | Automatic dependency tracking | | Virtual DOM | Yes (reconciliation overhead) | No (direct DOM updates) | | Bundle Size | ~42KB (React 18 + ReactDOM) | ~15KB (zero dependencies) | | Learning Curve | Complex (hooks rules, render cycles, patterns) | Simple (just signals) | | Global State | Context providers, prop drilling, or external libs | Create signals anywhere, use anywhere | | Async State | Complex patterns with useEffect + loading flags | Built-in loading states in signals | | Computed Values | useMemo with manual dependencies | Automatic dependency tracking | | Side Effects | useEffect with cleanup and dependency arrays | Simple effect() with automatic cleanup | | Component Updates | Entire component re-executes on state change | Only affected DOM nodes update |

🧠 Simpler Mental Model

React Hooks Complexity:

// React - Complex async state management
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [posts, setPosts] = useState([]);
  const [postsLoading, setPostsLoading] = useState(false);

  // Multiple useEffect hooks with dependency arrays
  useEffect(() => {
    setLoading(true);
    fetchUser(userId)
      .then(setUser)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [userId]); // Don't forget dependencies!

  useEffect(() => {
    if (user) {
      setPostsLoading(true);
      fetchUserPosts(user.id)
        .then(setPosts)
        .finally(() => setPostsLoading(false));
    }
  }, [user]); // Another dependency array

  const displayName = useMemo(() => {
    return user ? `${user.firstName} ${user.lastName}` : '';
  }, [user]); // More manual dependencies

  if (loading) return <div>Loading user...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!user) return <div>User not found</div>;

  return (
    <div>
      <h1>{displayName}</h1>
      <p>Email: {user.email}</p>
      {postsLoading ? (
        <div>Loading posts...</div>
      ) : (
        <ul>
          {posts.map(post => <li key={post.id}>{post.title}</li>)}
        </ul>
      )}
    </div>
  );
}

Tacit-DOM Simplicity:

// Tacit-DOM - Simple reactive state
const userProfile = component(({}, { signal, computed, effect }) => {
  const user = signal(null);
  const posts = signal([]);

  // Computed values automatically track dependencies
  const displayName = computed(() => {
    const u = user.value;
    return u ? `${u.firstName} ${u.lastName}` : '';
  });

  // Effects automatically track dependencies and clean up
  effect(() => {
    user.setLoading(true);
    fetchUser(userId).then((fetchedUser) => (user.value = fetchedUser));
  });

  effect(() => {
    const u = user.value;
    if (u) {
      posts.setLoading(true);
      fetchUserPosts(u.id).then((fetchedPosts) => (posts.value = fetchedPosts));
    }
  });

  return div(
    user.loading ? div('Loading user...') : null,
    user.error ? div('Error: ', user.error.message) : null,
    user.value
      ? div(
          h1(displayName),
          p('Email: ', user.value.email),
          posts.loading ? div('Loading posts...') : ul(...posts.value.map((post) => li(post.title))),
        )
      : div('User not found'),
  );
});

🎭 No More Re-render Roulette

In React, changing any state re-renders the entire component, including expensive child components. With Tacit-DOM, only DOM elements that depend on a specific signal will update:

// React: Changing count re-renders EVERYTHING
const dashboard = component(({}, { signal, computed }) => {
  const [count, setCount] = useState(0);
  const [user, setUser] = useState({ name: 'John' });
  const [expensiveData, setExpensiveData] = useState([]);

  // This expensive computation runs on EVERY render
  const processedData = useMemo(() => {
    return expensiveData.map(item => processExpensiveItem(item));
  }, [expensiveData]);

  return (
    <div>
      <div>Count: {count}</div> {/* Changing count re-renders entire component */}
      <div>User: {user.name}</div>
      <ExpensiveChart data={processedData} /> {/* Re-renders even when count changes */}
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
});

// Tacit-DOM: Surgical DOM updates
const dashboard = component(({}, { signal, computed }) => {
  const count = signal(0);
  const user = signal({ name: 'John' });
  const expensiveData = signal([]);

  // Computed value only recalculates when expensiveData changes
  const processedData = computed(() =>
    expensiveData.value.map(item => processExpensiveItem(item))
  );

  return div(
    div('Count: ', count), // Only this element updates when count changes
    div('User: ', user.value.name), // Only updates when user changes
    ExpensiveChart({ data: processedData }), // Only updates when processedData changes
    button({ onclick: () => count.value = count.value + 1 }, 'Increment')
  );
});

🌍 Global vs Local Signals

Tacit-DOM provides two types of signals: global signals and local signals scoped to components.

Global Signals - Shared State

Global signals can be created anywhere and accessed from any component:

// Global signals - accessible everywhere
const theme = signal('light');
const user = signal({ name: 'John', email: '[email protected]' });
const shoppingCart = signal([]);

// Any component can use these signals
const Header = component(({}, { signal }) => {
  return header(
    { className: theme },
    span('Welcome ', user.value.name),
    span('Cart items: ', shoppingCart.value.length),
  );
});

const Settings = component(({}, { signal }) => {
  return div(button({ onclick: () => (theme.value = theme.value === 'light' ? 'dark' : 'light') }, 'Toggle Theme'));
});

Local Signals - Component-Scoped State

Use the component() function to create signals that are scoped to a specific component instance:

// Component with local signals - must use camelCase name
const counter = component(({}, { signal, computed }) => {
  // These signals are local to this counter instance
  // Use the signal function from component utilities
  const count = signal(0);
  const isEven = computed(() => count.value % 2 === 0);

  return div(
    span('Count: ', count),
    span(isEven.value ? ' (even)' : ' (odd)'),
    button({ onclick: () => (count.value = count.value + 1) }, 'Increment'),
  );
});

// Each counter instance has its own local state
function App() {
  return div(
    h1('Multiple Counters'),
    counter(), // Counter #1 with its own count signal
    counter(), // Counter #2 with its own count signal
    counter(), // Counter #3 with its own count signal
  );
}

Key Differences

| Aspect | Global Signals | Local Signals | | ------------- | ------------------------------ | ----------------------------------------------------------------------- | | Scope | Application-wide | Component instance | | Creation | const signal = signal(value) | Inside component(({}, { signal }) => { const count = signal(value) }) | | Sharing | Accessible everywhere | Only within component | | Lifecycle | Persist until manually cleaned | Cleaned up when component unmounts | | Use Cases | Theme, user data, app state | Form state, local counters, component-specific state |

React Comparison

React - Complex state management:

// Global state requires Context/Redux
const ThemeContext = createContext();
const UserContext = createContext();

function App() {
  return (
    <ThemeProvider>
      <UserProvider>
        <Header />
        <MainContent />
      </UserProvider>
    </ThemeProvider>
  );
}

// Local state requires hooks with rules
function Counter() {
  const [count, setCount] = useState(0);
  const [isEven, setIsEven] = useState(true);

  useEffect(() => {
    setIsEven(count % 2 === 0);
  }, [count]); // Manual dependency

  return <div>...</div>;
}

Tacit-DOM - Simple and intuitive:

// Global state - just create signals
const theme = signal('light');
const user = signal(null);

// Local state - use component function
const counter = component(({}, { signal, computed }) => {
  const count = signal(0);
  const isEven = computed(() => count.value % 2 === 0); // Automatic dependency

  return div(/* ... */);
});

// No providers, no hooks rules, no manual dependencies

Best Practices

Use Global Signals for:

  • Application theme/settings
  • User authentication state
  • Shopping cart contents
  • API data that needs sharing
  • Route parameters

Use Local Signals for:

  • Form input values
  • Component-specific toggles
  • Local counters/timers
  • Modal open/closed state
  • Temporary UI state

This approach gives you the best of both worlds: simple global state management without the complexity of Context providers, and encapsulated local state without hooks rules.

🆕 Alternative Value API

Tacit-DOM provides two ways to read and write signal values:

Traditional API: get() and set()

const count = signal(0);

// Reading values
const currentValue = count.value;

// Writing values
count.set(10);

Modern API: value Property

const count = signal(0);

// Reading values (getter)
const currentValue = count.value;

// Writing values (setter)
count.value = 10;

Benefits of the value property:

  • Cleaner Syntax: count.value = 10 vs count.set(10)
  • Property-like Access: count.value for both reading and writing
  • Full Compatibility: Works alongside get() and set() methods
  • Reactive by Default: Automatically tracks dependencies when accessed

Both APIs work together seamlessly:

const count = signal(0);

// Mix and match - both work identically
count.set(5);
console.log(count.value); // 5

count.value = 10;
console.log(count.value); // 10

// Both trigger the same reactive updates
count.set(15);
count.value = 20;

📦 Installation

npm install tacit-dom

Requirements

  • Node.js: 16.0.0 or higher
  • TypeScript: 4.5.0 or higher (recommended)
  • Modern Browsers: ES2020+ support

Bundle Options

Tacit-DOM provides multiple bundle options to optimize for your use case:

# Full library (default)
import { signal, component, div } from 'tacit-dom';

# Signals only (lightweight - ~1.1KB)
import { signal, computed, effect } from 'tacit-dom/signals';

# DOM system only (~15KB)
import { component, div, render } from 'tacit-dom/dom';

Bundle Sizes:

  • Full Library: ~19KB (ESM/UMD/CJS)
  • Signals Only: ~1.1KB (94% smaller than full bundle)
  • DOM Only: ~15KB (21% smaller than full bundle)

Use Cases:

  • Signals Only: When you only need reactive state management
  • DOM Only: When you need DOM manipulation without signals
  • Full Bundle: When you need the complete feature set

🚀 Quick Start

Ready to build reactive apps without the React complexity? Dive in! 🏊‍♂️

No virtual DOM, no reconciliation, no provider hell - just pure, simple reactivity!

🧩 Components

Tacit-DOM provides a simple component system using the component function. The key benefit is that signals created inside a component are local to that component instance, providing automatic state encapsulation.

Local Signals with Component Function

import { component, div, h1, p, button, render } from 'tacit-dom';

// Component function creates local signal scope - must use camelCase name
const counter = component(({}, { signal }) => {
  // This signal is LOCAL to this counter instance
  // Use signal from component utilities parameter
  const count = signal(0);

  return div(
    { className: 'counter' },
    h1('Counter'),
    p('Count: ', count), // Signal used directly
    button({ onclick: () => (count.value = count.value + 1) }, 'Increment'),
  );
});

// Each render creates a separate instance with its own signals
render(counter, document.getElementById('app1')); // Counter #1
render(counter, document.getElementById('app2')); // Counter #2 (independent)

Why use component()?

  • Automatic Cleanup: Local signals are cleaned up when component unmounts
  • State Encapsulation: Each instance has its own state
  • No State Leakage: Signals don't interfere between instances
  • Memory Management: Prevents memory leaks from orphaned signals

Component with Props

const greeting = component<{ name: string; greeting?: string }>((props) => {
  return div({ className: 'greeting' }, h1(`${props?.greeting || 'Hello'}, ${props?.name || 'World'}!`));
});

// Usage
render(greeting({ name: 'Alice', greeting: 'Welcome' }), document.getElementById('greeting'));

Component with Local State

const userProfile = component(({}, { signal }) => {
  const user = signal({ name: 'John', email: '[email protected]' });
  const isEditing = signal(false);

  const toggleEdit = () => (isEditing.value = !isEditing.value);

  return div(
    { className: 'profile' },
    h1('User Profile'),
    isEditing.value
      ? div(
          input({
            value: user.value.name,
            oninput: (e) => (user.value = { ...user.value, name: e.target.value }),
          }),
          input({
            value: user.value.email,
            oninput: (e) => (user.value = { ...user.value, email: e.target.value }),
          }),
          button({ onclick: toggleEdit }, 'Save'),
        )
      : div(p(`Name: ${user.value.name}`), p(`Email: ${user.value.email}`), button({ onclick: toggleEdit }, 'Edit')),
  );
});

Component with Conditional Rendering using when

const conditionalCounter = component(({}, { signal, computed }) => {
  const count = signal(0);
  const isPositive = computed(() => count.value > 0);
  const isEven = computed(() => count.value % 2 === 0);

  return div(
    { className: 'conditional-counter' },
    h1('Conditional Counter'),
    p(`Count: ${count.value}`),

    // Use when() for conditional rendering
    when(isPositive, () => div({ className: 'positive-message' }, '✅ Count is positive!')),

    when(isEven, () => div({ className: 'even-message' }, '🔢 Count is even!')),

    when(
      computed(() => count.value === 0),
      () => div({ className: 'zero-message' }, '🎯 Count is zero!'),
    ),

    button({ onclick: () => (count.value = count.value + 1) }, 'Increment'),
    button({ onclick: () => (count.value = count.value - 1) }, 'Decrement'),
  );
});

Complex Component with when

const dashboard = component(({}, { signal, computed }) => {
  const user = signal({ name: 'Alice', role: 'admin', isOnline: true });
  const notifications = signal([]);
  const isLoading = signal(false);

  const isAdmin = computed(() => user.value.role === 'admin');
  const hasNotifications = computed(() => notifications.value.length > 0);

  return div(
    // Header section
    header(
      { className: 'dashboard-header' },
      h1(`Welcome, ${user.value.name}`),
      when(isOnline, () => span({ className: 'status online' }, '🟢 Online')),
      when(!isOnline, () => span({ className: 'status offline' }, '🔴 Offline')),
    ),

    // Main content
    main(
      { className: 'dashboard-content' },
      // Admin panel - only visible to admins
      when(isAdmin, () =>
        section(
          { className: 'admin-panel' },
          h2('Admin Panel'),
          button({ onclick: () => console.log('Admin action') }, 'Admin Action'),
        ),
      ),

      // Notifications - only visible when there are notifications
      when(hasNotifications, () =>
        section(
          { className: 'notifications' },
          h2('Notifications'),
          ...notifications.value.map((notification) => div({ className: 'notification' }, notification.message)),
        ),
      ),

      // Loading state
      when(isLoading, () => div({ className: 'loading' }, 'Loading...')),
    ),

    // Footer
    footer({ className: 'dashboard-footer' }, p('Dashboard v1.0')),
  );
});
import { signal, computed, component, div, h1, p, button, render } from 'tacit-dom';

// Create global reactive signals - accessible anywhere in your app
const count = signal(0);
const user = signal({ name: 'John', email: '[email protected]' });

// Create a reactive component without props
const counter = component(({}, { computed }) => {
  // Create a local computed value
  const doubleCount = computed(() => count.value * 2);

  // Create a reactive element
  return div(
    { className: 'counter' },
    h1('Counter Example'),
    p('Count: ', count),
    p('Double Count: ', doubleCount),
    p('User: ', user.value.name),
    button(
      {
        onclick: () => (count.value = count.value + 1),
      },
      'Increment',
    ),
  );
});

// Create a component with typed props
const greeting = component<{ name: string; greeting?: string }>((props) => {
  return div(
    { className: 'greeting' },
    h1(`${props?.greeting || 'Hello'}, ${props?.name || 'World'}!`),
    p('User: ', user.value.name),
    p('Email: ', user.value.email),
  );
});

// Another component can access the same global state
const userProfile = component(() => {
  return div(
    { className: 'profile' },
    h1('User Profile'),
    p('Name: ', user.value.name),
    p('Email: ', user.value.email),
  );
});

// Render components to DOM
render(counter, document.getElementById('app'));
render(greeting({ name: 'Alice', greeting: 'Welcome' }), document.getElementById('greeting'));
render(userProfile, document.getElementById('profile'));

🎭 Conditional and List Rendering

Tacit-DOM provides powerful utilities for conditional rendering and list management that automatically update when signals change.

Conditional Rendering with when

import { when, signal, div, h1, computed, component } from 'tacit-dom';

// Basic conditional rendering
const isVisible = signal(true);
const element = when(isVisible, div('This is visible'));

// With computed values
const count = signal(0);
const isPositive = computed(() => count.value > 0);
const element = when(isPositive, div(`Count is positive: ${count.value}`));

// Inside components - perfect for conditional UI elements
const statusIndicator = component(({}, { signal }) => {
  const status = signal('loading');

  return div(
    when(status === 'loading', div('⏳ Loading...')),
    when(status === 'success', div('✅ Success!')),
    when(status === 'error', div('❌ Error occurred')),
  );
});

// Complex conditions with computed values
const userCard = component(({}, { signal, computed }) => {
  const user = signal({ name: 'John', age: 25, isVerified: true });
  const isAdult = computed(() => user.value.age >= 18);
  const showVerification = computed(() => user.value.isVerified && isAdult.value);

  return div(
    h1(user.value.name),
    when(isAdult, p('Adult user')),
    when(showVerification, div({ className: 'verified-badge' }, '✓ Verified')),
  );
});

List Rendering with map and mapArray

import { map, mapArray, signal, div, li } from 'tacit-dom';

// Basic array mapping with map (returns array of elements)
const items = signal(['a', 'b', 'c']);
const listElements = map(items, (item) => div(item));
// listElements is an array: [div('a'), div('b'), div('c')]

// With filtering
const numbers = signal([1, 2, 3, 4, 5]);
const evenNumbers = map(
  numbers,
  (num) => div(num),
  (num) => num % 2 === 0,
);
// evenNumbers is an array: [div(2), div(4)]

// Using mapArray for reactive DOM updates
const fruits = signal(['apple', 'banana', 'cherry']);
const fruitList = mapArray(fruits, (fruit, index) => li({ className: `fruit-${index}` }, fruit));
// fruitList is a container element that updates when fruits changes

// mapArray with filtering
const colors = signal(['red', 'blue', 'green', 'yellow']);
const warmColors = mapArray(
  colors,
  (color) => div({ className: 'warm-color' }, color),
  (color) => ['red', 'yellow'].includes(color),
);
// warmColors is a container element showing only warm colors

Multiple Elements with when

import { div, h1, p, signal, when, component } from 'tacit-dom';

// Return multiple elements without a wrapper using when
const myComponent = component(({}, { signal }) => {
  const showHeader = signal(true);
  const showFooter = signal(true);

  return div(when(showHeader, h1('Header')), div('Main content'), when(showFooter, div('Footer')));
});

// Using when for conditional navigation elements
const navigation = component(({}, { signal }) => {
  const isLoggedIn = signal(false);

  return div(
    nav(
      { className: 'main-nav' },
      a({ href: '/' }, 'Home'),
      a({ href: '/about' }, 'About'),
      when(isLoggedIn, a({ href: '/profile' }, 'Profile')),
      when(!isLoggedIn, a({ href: '/login' }, 'Login')),
    ),
    // Conditional user info
    when(isLoggedIn, div({ className: 'user-info' }, 'Welcome back!')),
  );
});

🛠️ Development

For development setup, building, testing, and project structure, see DEVELOPMENT.md.

📚 Documentation

Tacit-DOM provides comprehensive documentation covering all aspects of the library. The documentation is organized into logical sections to help you find what you need quickly.

🚀 Getting Started

🎨 DOM & Components

🔧 Advanced Features

  • 🌐 Router Guide: Advanced client-side routing with object map routes, nested paths, optional parameters, and error handling
    • Object Map Routes: Cleaner syntax with { '/path': { component } } structure
    • Optional Parameters: Flexible routing with :?param syntax
    • Enhanced Component Props: Direct access to path, params, search, and data
    • Nested Route Patterns: Natural hierarchical route organization
    • Link Component: link() function for creating navigation links with automatic routing

🛠️ Development & Internals

📚 Component Naming Convention

Tacit-DOM uses a clean, intuitive naming convention:

| Function | Type | Description | | ------------------ | -------------- | -------------------------- | | component<P> | Component<P> | Create reactive components |

import { component, Component, div } from 'tacit-dom';

// Component without props
const simpleCounter = component(() => {
  return div('Hello World');
});

// Component with typed props
const greeting = component<{ name: string }>((props) => {
  return div(`Hello, ${props?.name || 'World'}!`);
});

🛠️ API Reference

For detailed API documentation, see API.md.

Core Functions

signal<T>(initialValue: T): Signal<T>

Creates a reactive signal with an initial value.

const count = signal(0);
count.value = 5; // Update value
console.log(count.value); // Get current value

computed<T>(fn: () => T): Computed<T>

Creates a computed value that automatically updates when dependencies change.

const doubleCount = computed(() => count.value * 2);

render(element: HTMLElement, container: HTMLElement): void

Renders a reactive element into a DOM container.

render(counter(), document.getElementById('app'));

cleanup(element: HTMLElement): void

Removes an element from the DOM and cleans up any associated resources.

const element = div('Hello World');
render(element, container);

// Later, clean up the element
cleanup(element);

link(props: { to: string; className?: string; children: any; [key: string]: any }): HTMLElement

Creates a navigation link that integrates with the router system.

import { link } from 'tacit-dom';

const navigation = nav(
  link({ to: '/', className: 'nav-link' }, 'Home'),
  link({ to: '/about', className: 'nav-link' }, 'About'),
  link({ to: '/contact', className: 'nav-link' }, 'Contact'),
);

DOM Elements

All HTML elements are available as factory functions:

import { div, h1, h2, h3, p, button, input, label, span, a } from 'tacit-dom';

const element = div(
  { className: 'container' },
  h1('Hello World'),
  h2('Subtitle'),
  h3('Section'),
  p('This is a paragraph'),
  button({ onclick: handleClick }, 'Click me'),
  input({ type: 'text', placeholder: 'Enter text' }),
  label({ for: 'input-id' }, 'Input Label'),
);

Styling

Tacit-DOM supports React-like style props with both static and reactive styles:

// String-based styles
div({ style: 'background-color: red; color: white;' }, 'Content');

// Object-based styles (React-like)
div(
  {
    style: {
      backgroundColor: 'red',
      color: 'white',
      fontSize: 16,
      padding: 15,
    },
  },
  'Content',
);

// Reactive styles
const colorSignal = signal('red');
div({ style: { backgroundColor: colorSignal } }, 'Content');

// Computed styles
const dynamicStyle = computed(() => ({
  backgroundColor: colorSignal.value,
  fontSize: sizeSignal.value,
}));
div({ style: dynamicStyle }, 'Content');

Style Features:

  • CamelCase to kebab-case: Properties like backgroundColor automatically convert to background-color
  • Automatic units: Numeric values for properties like fontSize automatically get px units
  • Mixed types: Support for both string and numeric values
  • Reactive updates: Styles automatically update when signals change