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 the package
Section titled “Add the package”Add @fullhaus/rivet to the sitepackage’s package.json (see
Installation) and import it in the Vite entry
script.
import { createRivet } from "@fullhaus/rivet";
import disclosure from "./components/disclosure";import counter from "./components/counter";
createRivet() .component("disclosure", disclosure) .component("counter", counter) .mount();Markup in the Fluid partial
Section titled “Markup in the Fluid partial”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.
<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>Component
Section titled “Component”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) };});Vite setup
Section titled “Vite setup”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.
Re-mount on loaded content
Section titled “Re-mount on loaded content”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);}Translations
Section titled “Translations”Texts stay in the Fluid template (f:translate), Rivet only drives behaviour:
<button data-rivet-part="button"> {f:translate(key: 'faq.toggle')}</button>