Skip to content

TYPO3 Integration

Rivet is built for exactly this case: the markup comes from Fluid templates, Rivet only attaches the behaviour. No renderer, no hydration mismatch.

Add @fullhaus/rivet to the sitepackage’s package.json (see Installation) and import it in the Vite entry script.

Resources/Private/Assets/Scripts/main.ts
import { createRivet } from "@fullhaus/rivet";
import disclosure from "./components/disclosure";
import counter from "./components/counter";
createRivet()
.component("disclosure", disclosure)
.component("counter", counter)
.mount();

The root and its parts are marked directly in the Fluid template. IDs can be generated from Fluid variables to get a unique ID per element.

Resources/Private/Partials/Faq/Item.html
<f:section name="Main">
<div data-rivet="disclosure" data-rivet-id="faq-{f:variable(name: 'item.uid')}">
<button data-rivet-part="button" aria-expanded="false">
{item.question}
</button>
<div data-rivet-part="panel" hidden>
<f:format.html>{item.answer}</f:format.html>
</div>
</div>
</f:section>
Resources/Private/Assets/Scripts/components/disclosure.ts
import { defineComponent } from "@fullhaus/rivet";
export default defineComponent(({ part, signal, on, effect }) => {
const button = part("button");
const panel = part("panel");
const open = signal(false);
on(button, "click", () => open.update((v) => !v));
effect(() => {
panel.hidden = !open();
button.setAttribute("aria-expanded", String(open()));
});
return { show: () => open.set(true), hide: () => open.set(false) };
});

Rivet is pure ESM with type declarations — it works without special config in the sitepackage’s Vite build. Just make sure your entry script (main.ts above) is included as a module, e.g. via the f:asset.script ViewHelper or your setup’s Vite manifest.

If your frontend loads content via AJAX (e.g. filters, pagination, tabs), call mount() again after inserting — new roots are picked up, existing ones skipped.

const app = createRivet().component("disclosure", disclosure).mount();
async function loadPage(container: HTMLElement, url: string) {
container.innerHTML = await fetch(url).then((r) => r.text());
app.mount(container);
}

Texts stay in the Fluid template (f:translate), Rivet only drives behaviour:

<button data-rivet-part="button">
{f:translate(key: 'faq.toggle')}
</button>