@metigan/forms

Embed Metigan forms anywhere: a framework-free core, a DOM renderer, React and Vue components.
299
Install
npm install @metigan/forms
Latest Version:1.0.0
Vue:3
License:MIT
Last Updated:Oct 18, 2026
Links: npm
Maintainer: frantchessico

@metigan/forms

Put a Metigan form on any site: a one-line script, or React and Vue components, or a headless core that drives your own markup. Zero runtime dependencies.

  • Light DOM (no Shadow DOM): your CSS reaches every element.
  • Theme CSS lives in @layer metigan and every selector sits inside :where(), so any rule you write wins without !important. Or turn it off completely with theme="none".
  • Validation matches the Metigan api, multi-step forms, captcha, honeypot, prefill, UTM capture, pt / en / es.

Script (any site, WordPress, Webflow, Angular, …)

<div data-metigan-form="form-6710f3a2c9e77b1a2d4e5f60"></div>
<script src="https://f.metigan.io/embed/v1.js" async></script>

The script mounts every [data-metigan-form] on the page, including ones added later (it watches the DOM, so it works in SPAs). Mounting is idempotent per element.

Pin a version with Subresource Integrity: use https://f.metigan.io/embed/1.0.0.js with the hash from https://f.metigan.io/embed/1.0.0.js.sri.txt:

<script src="https://f.metigan.io/embed/1.0.0.js" integrity="sha384-…" crossorigin="anonymous" async></script>

Attributes

attribute
data-metigan-form="ID" form id (form-<24 hex>)
data-theme="metigan|minimal|none" default metigan (the builder's appearance)
data-lang="pt|en|es" default <html lang>, then the browser, then en
data-redirect="https://…" where to go after a successful submission
data-prefill-<name>="value" initial answer for the field named <name> (?<name>= in the page URL also works)
data-analytics="off" no view/start events, no localStorage
data-api="https://…" api override (tests)

Two modes

Render mode — <div data-metigan-form>: the script draws the whole form.

Enhance mode — <form data-metigan-form>: your own HTML. Controls are matched to the form's fields by name; the script validates, submits JSON, shows errors and success, and adds the honeypot and the captcha. Error messages go to [data-mtg-error="<name>"], else to a .mtg-error inside the control's .mtg-field, else to a <p class="mtg-error"> it creates after the control. Without JavaScript the same form still works if its action is https://api.metigan.io/f/<ID>/submit (method post).

<form data-metigan-form="form-…" action="https://api.metigan.io/f/form-…/submit" method="post">
  <input name="name">
  <input name="email" type="email">
  <label><input type="checkbox" name="topics" value="Sales"> Sales</label>
  <input type="hidden" name="_mtg_redirect" value="https://example.com/thanks">
  <button type="submit">Send</button>
</form>

JavaScript API

const form = Metigan.forms.mount("#contact", {
  formId: "form-…", theme: "minimal", lang: "en",
  classNames: { submit: "btn btn-primary" },
  prefill: { email: "ana@example.com" },
  onSuccess: ({ message }) => console.log(message),
})
form.reset()
form.unmount()
Metigan.forms.scan()      // mount new [data-metigan-form] now
Metigan.forms.version     // "1.0.0"

Options: formId, theme, lang, classNames, prefill, redirect, analytics, apiBase, onReady, onSubmit, onSuccess, onError (onSubmit may return false to cancel).

Events

Dispatched on the mount element; they bubble.

event detail
metigan:ready { form } (the public definition)
metigan:submit { data } (answers by field name)
metigan:success { message, redirect? }
metigan:error { message, status, code? } (status 0 = network)
document.addEventListener("metigan:success", (e) => gtag("event", "lead", { form: e.target.dataset.metiganForm }))

Iframe mode

<iframe data-metigan-frame src="https://f.metigan.io/f/<slug>?embed=1"> plus the script: the iframe resizes to its content.

npm

npm install @metigan/forms

React / Next.js

import { MetiganForm } from "@metigan/forms/react"

export default function Contact() {
  return <MetiganForm formId="form-…" theme="metigan" lang="pt" classNames={{ submit: "btn" }} onSuccess={({ message }) => console.log(message)} />
}

The module is marked "use client" and is SSR-safe: nothing touches window at import or during render; the form mounts in an effect. Works in the App Router and Pages Router.

Headless:

import { useMetiganForm } from "@metigan/forms/react"

function MyForm() {
  const f = useMetiganForm("form-…")
  if (f.status === "loading") return null
  if (f.status === "success") return <p>{f.message}</p>
  return (
    <form onSubmit={f.submit} noValidate>
      <input {...f.register("email")} type="email" />
      {f.errors[f.fields.find((x) => x.name === "email")!.id]}
      <label><input type="checkbox" {...f.register("topics", "Sales")} /> Sales</label>
      <div ref={f.captchaRef} />
      <button disabled={f.status === "submitting"}>Send</button>
      {f.status === "error" && <p role="alert">{f.message}</p>}
    </form>
  )
}

useMetiganForm(formId, opts) returns { form, fields, status, values, errors, message, register(name, option?), setValue, submit, reset, steps, step, next, back, captchaRef }. status is loading | ready | submitting | success | error; errors and values are keyed by field id.

Vue / Nuxt

<script setup>
import { MetiganForm } from "@metigan/forms/vue"
</script>

<template>
  <MetiganForm form-id="form-…" theme="minimal" :class-names="{ submit: 'btn' }" @success="({ message }) => console.log(message)" />
</template>

Mounts in onMounted (client only), so it is safe in Nuxt SSR. useMetiganForm is the headless composable (same shape as React, as refs; register() returns v-bind props).

Script behaviour from a bundle

import "@metigan/forms/embed" // auto-mounts [data-metigan-form] and sets window.Metigan.forms

Headless core

import { loadForm, submitForm, validate, createFormController } from "@metigan/forms"

const form = await loadForm("form-…")
const { valid, errors } = validate(form.fields, { email: "nope" }) // errors: { field_ab12: "invalid email address" }
const r = await submitForm(form.id, { email: "ana@example.com" }) // { ok, status, message, code?, fieldId?, reason? }

const c = createFormController({ formId: "form-…", lang: "en" })
c.subscribe((state) => render(state)) // { status, form, fields, steps, step, values, errors, formError, message, blocked }
await c.load()
c.setValue("email", "ana@example.com")
await c.submit()

Styling

/* variables, on .mtg-form */
.mtg-form {
  --mtg-font: inherit; --mtg-text: #111; --mtg-muted: #666; --mtg-bg: transparent; --mtg-field-bg: #f6f6f4;
  --mtg-border: rgba(0,0,0,.08); --mtg-primary: #0e6b5c; --mtg-primary-text: #fff; --mtg-error: #c2291d;
  --mtg-radius: 12px; --mtg-gap: 20px; --mtg-input-height: 48px; --mtg-focus: rgba(14,107,92,.2);
}
/* or plain rules: they always win over the theme */
.mtg-submit { text-transform: uppercase; }

Themes: metigan (the builder's appearance: claro / papel / noite / marca with its accent colour), minimal (structure and spacing only, colours inherited from your page), none (no CSS at all). classNames adds your classes next to the mtg-* ones (keys: form, field, label, input, choice, help, error, submit, back, next, success, formError).

Stable DOM (v1):

<form class="mtg-form" data-state="idle|submitting|success|error" data-theme="metigan">
  <div class="mtg-header"><p class="mtg-eyebrow"/><h2 class="mtg-title"/><p class="mtg-description"/></div>
  <div class="mtg-progress"><div class="mtg-progress-bar"/></div>
  <div class="mtg-step" data-step="0">
    <div class="mtg-field" data-type="email" data-name="email" [data-invalid]>
      <label class="mtg-label">E-mail <span class="mtg-required">*</span></label>
      <input class="mtg-input">
      <div class="mtg-choices"><label class="mtg-choice"><input class="mtg-choice-input"><span class="mtg-choice-label"/></label></div>
      <p class="mtg-help"/> <p class="mtg-error" role="alert"/>
    </div>
  </div>
  <div class="mtg-captcha"/> <p class="mtg-form-error" role="alert"/>
  <div class="mtg-actions"><button class="mtg-back"/><button class="mtg-next"/><button class="mtg-submit"/></div>
  <div class="mtg-success" role="status"/>
  <a class="mtg-branding">Made with Metigan</a>
</form>

File fields cannot be uploaded from an embedded form: they show a notice with a link to the hosted form (a required file field disables the submit button).

Content Security Policy

script-src  https://f.metigan.io;            (only for the script; not needed with npm)
connect-src https://api.metigan.io;
frame-src   https://f.metigan.io;            (captcha, iframe mode)
img-src     https: data:;                    (image-choice options)
style-src   'unsafe-inline';                 (or a nonce: see below)

The theme is one <style id="metigan-forms-css">. With a nonce-based style-src, put the nonce on the script tag (<script nonce="…" src=…>: it is copied to the style) or pass nonce to mount() / <MetiganForm nonce>. theme="none" needs no style permission.

Privacy

The script stores an anonymous visitor id in localStorage (mtg_v) to count distinct views; data-analytics="off" (or analytics: false) disables it and the view/start events.

Related Packages

@easy-cms/core

Embedded, code-first headless CMS for Nuxt and Next.js

25,029 2
theme-switch-animation

Theme switch animation library built on the View Transitions API. Works with Rea...

967 7
@drmhse/authos-cli

Pre-1.0 CLI for scaffolding AuthOS integrations

3,866 6
@cui-universal/theme

Cross-framework design system tokens, Tailwind plugin, and variants for CUI

108 2
@docus/twitter

Twitter static embed for Docus.

1,113

Version History

Version Released Vue License
1.0.0 >=3.2 MIT
Pre-releases (1)
Version Released License
0.0.0-stage