tacit-dom
Version: 
A React-like library with reactive signals and computed values for building dynamic web applications—without the need for JSX
1,015 lines (767 loc) • 35.6 kB
Markdown
<div align="center">

> A React-like library with reactive signals and computed values for building dynamic web applications—without the need for JSX.
</div>
## 🤔 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.
</div>
[](https://www.npmjs.com/package/tacit-dom)
[](https://github.com/mjbeswick/tacit-dom/blob/main/LICENSE)
[](https://www.typescriptlang.org/)
[](https://github.com/mjbeswick/tacit-dom/actions)
## 📋 Table of Contents
- [Project Status](#-project-status)
- [Features](#-features)
- [Why Tacit-DOM?](#-why-tacit-dom)
- [Installation](#-installation)
- [Quick Start](#-quick-start)
- [Components](#-components)
- [Conditional and List Rendering](#-conditional-and-list-rendering)
- [Examples](#-examples)
- [Documentation](#-documentation)
- [API Reference](#-api-reference)
- [Development](#-development)
- [License](#-license)
## ⚠️ 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:**
```typescript
// 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:**
```typescript
// 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:
```typescript
// 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:
```typescript
// Global signals - accessible everywhere
const theme = signal('light');
const user = signal({ name: 'John', email: 'john@example.com' });
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:
```typescript
// 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:**
```typescript
// 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:**
```typescript
// 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()`**
```typescript
const count = signal(0);
// Reading values
const currentValue = count.value;
// Writing values
count.set(10);
```
**Modern API: `value` Property**
```typescript
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:**
```typescript
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
```bash
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:
```bash
# 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
```typescript
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
```typescript
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
```typescript
const userProfile = component(({}, { signal }) => {
  const user = signal({ name: 'John', email: 'john@example.com' });
  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`
```typescript
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`
```typescript
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')),
  );
});
```
```typescript
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: 'john@example.com' });
// 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`
```typescript
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`
```typescript
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`
```typescript
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](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
- **[📖 API Reference](docs/API.md)**: Complete API documentation with examples
- **[🔄 Signals Guide](docs/SIGNALS.md)**: Learn about reactive signals, the foundation of Tacit-DOM
- **[💡 Signals Usage Guide](docs/SIGNAL_USAGE_GUIDE.md)**: Practical examples and common patterns
### 🎨 DOM & Components
- **[🌐 DOM Internals](docs/DOM_INTERNALS.md)**: Deep dive into DOM manipulation and reactive updates
- **[🎨 ClassName Utility](docs/CLASSNAMES.md)**: Dynamic CSS class management (recommended)
### 🔧 Advanced Features
- **[🌐 Router Guide](docs/ROUTER.md)**: 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
- **[⚙️ Development Guide](docs/DEVELOPMENT.md)**: Setup, building, testing, and contributing
- **[🔍 Signal Internals](docs/SIGNAL_INTERNALS.md)**: Technical implementation details
### 📚 Component Naming Convention
Tacit-DOM uses a clean, intuitive naming convention:
| Function           | Type           | Description                |
| ------------------ | -------------- | -------------------------- |
| **`component<P>`** | `Component<P>` | Create reactive components |
```typescript
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](docs/API.md).
### Core Functions
#### `signal<T>(initialValue: T): Signal<T>`
Creates a reactive signal with an initial value.
```typescript
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.
```typescript
const doubleCount = computed(() => count.value * 2);
```
#### `render(element: HTMLElement, container: HTMLElement): void`
Renders a reactive element into a DOM container.
```typescript
render(counter(), document.getElementById('app'));
```
#### `cleanup(element: HTMLElement): void`
Removes an element from the DOM and cleans up any associated resources.
```typescript
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.
```typescript
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:
```typescript
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:
```typescript
// 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