Type-safe reactive Web Components — HTML-first, backend-agnostic
Le Truc adds a thin reactive layer to server-rendered HTML. You keep your existing backend (Java, PHP, Python, C#, static site generator — anything that outputs HTML). Le Truc wires fine-grained DOM updates to reactive component properties in the browser, without re-rendering whole subtrees and without requiring JavaScript on the server.
The result is SolidJS-style reactivity packaged as standard Custom Elements: components are reusable across projects, type-safe, and carry no framework lock-in.
Digital agencies building content-rich sites face a recurring choice: imperative JavaScript that becomes unmaintainable as complexity grows, or a SPA framework that takes over rendering and requires a JavaScript backend. Neither is a good fit when the backend is a CMS and the initial HTML is already correct.
Le Truc solves the specific problem of stateful interactivity on server-rendered pages:
- The server renders HTML — Le Truc never re-renders it
- Reactive properties update only the DOM nodes that actually changed
- Components are plain Custom Elements — they work in any host environment
- TypeScript catches integration errors at compile time
npm install @zeix/le-truc
# or
bun add @zeix/le-truc- Start with your server-rendered HTML:
<basic-hello>
<label for="name">Your name</label>
<input id="name" name="name" type="text" autocomplete="given-name" />
<p>Hello, <output for="name">World</output>!</p>
</basic-hello>- Define the component:
import { bindText, defineComponent } from '@zeix/le-truc'
defineComponent(
'basic-hello', // Component name (must contain a hyphen)
({ expose, first, on, watch }) => { // Factory: query DOM, declare props, return effects
const input = first('input', 'Needed to enter the name.')
const output = first('output', 'Needed to display the name.')
const fallback = output.textContent || ''
expose({ name: output.textContent ?? '' }) // declare reactive prop
return [
on(input, 'input', () => ({ name: input.value || fallback })),
watch('name', bindText(output)),
]
},
)- Import the module and watch it work.
defineComponent registers the element via customElements.define(). expose() declares reactive properties, watch() wires up DOM updates that fire only when the tracked signal actually changes, on() binds event listeners.
Full documentation with live examples is at zeixcom.github.io/le-truc:
- 🧱 HTML-first — enhances server-rendered markup; no Virtual DOM, no hydration
- 🚦 Reactive properties — signals with automatic dependency tracking and fine-grained updates
- ⚡️ Pinpoint effects — only the exact DOM nodes that changed are touched
- 🧩 Composable — build behaviour from small, reusable parsers and effect functions
- 🌐 Context support — share state across components without prop drilling
- 🪶 Tiny — ≤10 kB gzipped, tree-shakeable
- 🛡️ Type-safe — full TypeScript inference from selector strings to property types
Le Truc uses Cause & Effect for its reactive primitives.
Contributions, bug reports, and suggestions are welcome — see CONTRIBUTING.md.