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
Maintainers
Readme
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.
📋 Table of Contents
- Project Status
- Features
- Why Tacit-DOM?
- Installation
- Quick Start
- Components
- Conditional and List Rendering
- Examples
- Documentation
- API Reference
- Development
- 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
whenfunction for reactive conditional content - 📋 List Rendering: Powerful
mapfunction with optional filtering for dynamic lists - 🧩 Conditional Rendering:
whenfunction 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 dependenciesBest 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 = 10vscount.set(10) - Property-like Access:
count.valuefor both reading and writing - Full Compatibility: Works alongside
get()andset()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-domRequirements
- 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 colorsMultiple 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
- 📖 API Reference: Complete API documentation with examples
- 🔄 Signals Guide: Learn about reactive signals, the foundation of Tacit-DOM
- 💡 Signals Usage Guide: Practical examples and common patterns
🎨 DOM & Components
🌐 DOM Internals: Deep dive into DOM manipulation and reactive updates
🎨 ClassName Utility: Dynamic CSS class management (recommended)
🔧 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
:?paramsyntax - Enhanced Component Props: Direct access to
path,params,search, anddata - Nested Route Patterns: Natural hierarchical route organization
- Link Component:
link()function for creating navigation links with automatic routing
- Object Map Routes: Cleaner syntax with
🛠️ Development & Internals
- ⚙️ Development Guide: Setup, building, testing, and contributing
- 🔍 Signal Internals: Technical implementation details
📚 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 valuecomputed<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
backgroundColorautomatically convert tobackground-color - Automatic units: Numeric values for properties like
fontSizeautomatically getpxunits - Mixed types: Support for both string and numeric values
- Reactive updates: Styles automatically update when signals change
