@metigan/forms
| Install | |
|---|---|
npm install @metigan/forms |
|
| Latest Version: | 1.0.0 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Oct 18, 2026 |
| Links: | npm |
@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 metiganand every selector sits inside:where(), so any rule you write wins without!important. Or turn it off completely withtheme="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
Theme switch animation library built on the View Transitions API. Works with Rea...
Cross-framework design system tokens, Tailwind plugin, and variants for CUI
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 1.0.0 | >=3.2 | MIT |
Pre-releases (1)
| Version | Released | License |
|---|---|---|
| 0.0.0-stage |