Skip to main content

Add a form to Vue, Svelte, Astro, or HTML

Mount a Fillo form with @usefillo/dom in Vue, Svelte, Astro, or a plain HTML page, and destroy it cleanly when the component unmounts.

Build this with your agent. Copy a prompt that hands this guide to Claude Code, Codex, Cursor, or any coding agent.

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:

bash
npm install @usefillo/dom

renderForm(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 methodUse
formIdA published form ID or slug. No key needed.
form and clientA form from defineForm() that saves responses through your pk_ key
onSubmittedRuns after Fillo records the response
initialDataPrefill answers by field ID
themeDesign 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

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

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.

astro
---
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:

html
<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:

DirectiveOrigin
script-src and style-srchttps://unpkg.com
connect-srchttps://fillo.so, or your baseUrl
frame-srchttps://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

  1. Load the form, then load an unpublished form ID and read the error.
  2. Submit empty and confirm focus moves to the first error.
  3. Navigate away mid-upload, come back, and check for leftover forms.
  4. Submit with the network off, then retry without retyping.
  5. Find the saved response in Fillo.

This page for agents: /guides/javascript-form.md · index at /llms.txt

Updated

Was this page helpful?