| title | DOM Renderer |
|---|---|
| description | Guide to using the rEFui DOM renderer for building reactive web applications. |
| weight | 31 |
This document provides a guide on how to set up and use the rEFui DOM renderer for building reactive web applications.
Note: For detailed information about rEFui's reactive system and signals, see the Signals documentation.
First, you need to create a renderer instance. We provide a default setup for browsers for convenience.
import { createDOMRenderer } from 'refui/dom';
import { defaults } from 'refui/browser';
// Create the renderer with browser defaults
const renderer = createDOMRenderer(defaults);If your platform exposes DOM-style APIs (via undom-ng, DOMiNATIVE(DOM for NativeScript), or a custom document), pass it to createDOMRenderer:
import { createDOMRenderer } from 'refui/dom'
import { document } from 'dominative' // or your own doc
const renderer = createDOMRenderer({
doc: document,
rendererID: 'MyTarget',
// optional: namespaces, tagNamespaceMap, tagAliases, propAliases, onDirective, macros
})This reuses all DOM renderer behaviors: signal-aware text/props, event normalization, namespaces, and macros.
For instructions on how to set up JSX, please see the JSX Setup Guide. It covers both the preferred Classic Transform and the Automatic Runtime.
Here is a complete example of rendering a component to the DOM using the preferred classic transform. You will first need to configure your build tool.
index.html:
<!DOCTYPE html>
<html>
<body>
<div id="app"></div>
<script src="index.js"></script>
</body>
</html>index.js:
import { createDOMRenderer } from 'refui/dom';
import { defaults } from 'refui/browser';
// 1. Create renderer
const renderer = createDOMRenderer(defaults);
// 2. Define a component that accepts a renderer (R)
const App = () => <h1>Hello, World!</h1>;
// 3. Render the component to a DOM element
renderer.render(document.getElementById('app'), App);rEFui's power comes from its reactive signal system. Here's an example with interactive state:
Note on Expressions: Because rEFui is a retained-mode renderer, you only need to wrap expressions that dereference
.valueor perform inline computations in a computed signal ($()) to keep them reactive.
<div>Count: {count}</div>- Correct: Passing the signal itself keeps it reactive.{<div>{$(() => \Count is ${count.value}`)}}</code> - **Correct**: Wrap computed strings or.value` access.<div>Count is {count.value}</div>- Incorrect: Dereferencing.valueinline is evaluated once and will not update.
If you know a value will never change after the initial render, you can pass a plain literal—or even signal.value—so the renderer keeps it static and skips wiring a subscription.
import { createDOMRenderer } from 'refui/dom';
import { defaults } from 'refui/browser';
import { signal } from 'refui';
const renderer = createDOMRenderer(defaults);
const Counter = () => {
// Create a reactive signal
const count = signal(0);
return (
<div>
<h1>Count: {count}</h1>
<button on:click={() => count.value++}>
Increment
</button>
<button on:click={() => count.value--}>
Decrement
</button>
</div>
);
};
renderer.render(document.getElementById('app'), Counter);
Components can receive props that can be signals or regular values:
import { signal, read } from 'refui';
const Greeting = ({ name, count }) => {
return (
<div>
<h1>Hello, {name}!</h1>
<p>You have {count} messages</p>
</div>
);
};
const App = () => {
const userName = signal('John');
const messageCount = signal(5);
return (
<div>
<Greeting name={userName} count={messageCount} />
<button on:click={() => userName.value = 'Jane'}>
Change Name
</button>
<button on:click={() => messageCount.value++}>
Add Message
</button>
</div>
);
};
The DOM renderer automatically handles the difference between DOM properties and HTML attributes:
- Props are set as DOM object properties by default
- Attributes should use the
attr: prefix
- Props containing
- are treated as attributes automatically
- You can force a prop by using the
prop: prefix
- Boolean values toggle the property/attribute on and off
- Signal values are automatically unwrapped and kept reactive
Usage: attr:attribute-to-be-set="value"
Example:
import { signal } from 'refui';
const MyComponent = () => {
const isChecked = signal(false);
const tooltipText = signal('Hello');
const customValue = signal('test');
return (
<>
{/* Reactive attribute */}
<input type="checkbox" attr:checked={isChecked}/>
{/* Data attributes (automatic) */}
<div data-tooltip={tooltipText}/>
{/* Force as property */}
<div prop:myWeirdProp={customValue}/>
{/* Static values */}
<input type="text" placeholder="Enter text"/>
</>
);
};
When using the browser preset, you can use the class: directive to conditionally apply CSS classes. This is much more direct than complex conditional expressions.
import { signal } from 'refui';
const NavigationItem = ({ href, children, currentPath }) => {
const isActive = currentPath.eq(href);
const isHovered = signal(false);
return (
<a
href={href}
class:active={isActive} // Apply 'active' class conditionally
class:hover={isHovered} // Apply 'hover' class conditionally
on:mouseenter={() => isHovered.value = true}
on:mouseleave={() => isHovered.value = false}
>
{children}
</a>
);
};
// You can combine multiple conditional classes
const StatusCard = ({ status, isLoading }) => {
const isError = status.eq('error');
const isSuccess = status.eq('success');
return (
<div
class="card" // Static class
class:loading={isLoading} // Conditional: loading state
class:error={isError} // Conditional: error state
class:success={isSuccess} // Conditional: success state
>
Status: {status}
</div>
);
};
Note: The class: directive is available when using the browser preset (refui/browser). For more information about presets and directives, see the Presets documentation.
Tailwind CSS tip: Tailwind’s scanner only picks up class names that appear inside literal class="..." attributes. Keep your full utility list in the static class attribute, then layer class: toggles for conditional pieces. For example, class="card text-sm error" plus class:error={isError} ensures Tailwind sees card, text-sm, and error while still letting you flip the class on and off at runtime.
React-style props on SVG can hit read-only DOM properties. Use attr: to set attributes for SVG elements to ensure they are applied correctly.
// ❌ Incorrect
<svg width="24" viewBox="0 0 24 24" />
// ✅ Correct
<svg attr:width="24" attr:viewBox="0 0 24 24" />
Macro directives let you attach reusable DOM behaviors to elements by prefixing a prop with m:. When the renderer sees m:macroName, it looks up a handler registered on the renderer and calls it with the element and the value you passed.
You can provide macros when creating the renderer, or register them later with renderer.useMacro. Handlers are responsible for managing reactive values themselves.
import { createDOMRenderer } from 'refui/dom'
import { defaults } from 'refui/browser'
import { bind, nextTick } from 'refui/signal'
const renderer = createDOMRenderer({
...defaults,
macros: {
tooltip(node, value) {
bind(function(text) {
node.setAttribute('aria-label', text ?? '')
}, value)
}
}
})
renderer.useMacro({
name: 'autofocus',
handler(node, value) {
if (!value) return
nextTick(function() {
node.focus()
})
}
})
After the renderer knows about a macro, you can opt-in from JSX with the m: directive. JSX boolean syntax (m:autofocus) passes true; you can also pass signals or other values that your handler understands.
const Field = ({ helperText }) => (
<div class="field" m:tooltip={helperText}>
<input type="text" m:autofocus />
</div>
)
Because the macro handler receives the raw value, use bind or isSignal inside the handler when you need reactive updates.
Keep macros for behaviors that genuinely repeat across components. When a DOM tweak only appears in one place, inline it inside the component instead—doing so keeps reviews and refactors straightforward.
The DOM renderer provides a flexible event system that works seamlessly with signals.
Usage: on[-option-moreOptions]:eventName={handler}
Examples:
- Simple click with signal updates
import { signal } from 'refui';
const Counter = () => {
const count = signal(0);
return (
<button on:click={() => count.value++}>
Clicked {count} times
</button>
);
};
- Click once
<button on-once:click={() => alert('Clicked!')}>Click me!</button>
- Passive events for performance
<div on-passive:scroll={() => {/* do some time consuming operations */}}>{longContent}</div>
- Multiple options
<div on-capture-passive:click={() => alert('Clicked!')}><button>Click me!</button></div>
- Working with event objects and signals
const SearchInput = () => {
const query = signal('');
const results = signal([]);
return (
<input
type="text"
value={query}
on:input={(event) => {
query.value = event.target.value;
// Trigger search logic here
}}
placeholder="Search..."
/>
);
};
We provide presets for conveinence.
- Check here