Render a Fillo form as native elements in a Vue, Svelte, or Astro app, or on a plain HTML page. Every framework uses the same @usefillo/dom pattern. Mount the form with renderForm(), and call destroy() when the component unmounts.
The shared pattern
Install the package in a bundled app:
npm install @usefillo/domrenderForm(target, options) mounts the form into one element and returns a handle. The renderer owns that element's children, so don't let your framework render inside it.
| Option or method | Use |
|---|---|
formId | A published form ID or slug. No key needed. |
form and client | A form from defineForm() that saves responses through your pk_ key |
onSubmitted | Runs after Fillo records the response |
initialData | Prefill answers by field ID |
theme | Design tokens for the default renderer |
destroy() | Removes the form and cancels in-flight uploads. Call it on unmount. |
setRespondent() | Adds signed-in account context after your session loads |
When the form ID changes, destroy the current form before you mount the next one.
Vue
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from "vue";
import { renderForm, type FilloDomForm } from "@usefillo/dom";
import "@usefillo/dom/styles.css";
const root = ref<HTMLElement | null>(null);
let form: FilloDomForm | undefined;
onMounted(() => {
if (!root.value) return;
form = renderForm(root.value, {
formId: "your-published-form-id",
onSubmitted: (responseId) => console.info("Fillo response", responseId),
});
});
onBeforeUnmount(() => form?.destroy());
</script>
<template>
<section aria-labelledby="feedback-title">
<h2 id="feedback-title">Product feedback</h2>
<div ref="root" />
</section>
</template>If a route reuses the component for another form, destroy and remount in a watch on the form ID.
Svelte
<script lang="ts">
import { onMount } from "svelte";
import { renderForm } from "@usefillo/dom";
import "@usefillo/dom/styles.css";
let root: HTMLDivElement;
onMount(() => {
const form = renderForm(root, {
formId: "your-published-form-id",
onSubmitted: (responseId) => console.info("Fillo response", responseId),
});
return () => form.destroy();
});
</script>
<section aria-labelledby="feedback-title">
<h2 id="feedback-title">Product feedback</h2>
<div bind:this={root}></div>
</section>onMount runs only in the browser, so server rendering never touches the form. For fully Svelte-owned markup, use createFormController() from the same package and see custom UI.
Astro
The page stays server-rendered. A client script mounts the form into one element.
---
import "@usefillo/dom/styles.css";
const { formId = "your-published-form-id" } = Astro.props;
---
<section aria-labelledby="feedback-title">
<h2 id="feedback-title">Product feedback</h2>
<div id="fillo-feedback" data-form-id={formId}></div>
</section>
<script>
import { renderForm } from "@usefillo/dom";
const root = document.querySelector<HTMLElement>("#fillo-feedback");
if (root) {
renderForm(root, {
formId: root.dataset.formId!,
onSubmitted: (responseId) => console.info("Fillo response", responseId),
});
}
</script>Give each instance on a page its own element ID. With view transitions, mount on astro:page-load and call destroy() on astro:before-swap.
HTML
No package manager or build step. The standalone bundle exposes a global Fillo object:
<link rel="stylesheet" href="https://unpkg.com/@usefillo/dom@0.23/dist/styles.css" />
<section aria-labelledby="contact-title">
<h2 id="contact-title">Contact us</h2>
<div id="fillo-contact"></div>
</section>
<script src="https://unpkg.com/@usefillo/dom@0.23/dist/standalone.global.js"></script>
<script>
Fillo.renderForm("#fillo-contact", {
formId: "your-published-form-id",
onSubmitted: function (responseId) {
console.info("Fillo response", responseId);
},
});
</script>Use the version pinned in the embed quickstart, and test upgrades before you change it. The bundle also registers a <fillo-form form-id="…"> element.
If your site sets a Content Security Policy, allow these origins:
| Directive | Origin |
|---|---|
script-src and style-src | https://unpkg.com |
connect-src | https://fillo.so, or your baseUrl |
frame-src | https://fillo.so, when the form uses the human check |
Serve the page over HTTPS. When you can run a build, prefer the npm package for version locking and types.
Keep secrets out of the page
A published form ID is safe in page HTML. Ship a pk_ publishable key only when a code-defined form needs it. Never put an fsk_ or fcli_ token, an identity secret, a webhook secret, or storage credentials in client code.
Check it before you ship
- Load the form, then load an unpublished form ID and read the error.
- Submit empty and confirm focus moves to the first error.
- Navigate away mid-upload, come back, and check for leftover forms.
- Submit with the network off, then retry without retyping.
- Find the saved response in Fillo.
Related
- Contact example: See the validation, loading, and success states in the browser.
- Embed quickstart: Compare the DOM renderer, the web component, and the standalone bundle.
- Styling: Override tokens, slots, and state attributes.
- Blocked by CORS policy: Configure API and storage origins.