@formvue/embed-react
v0.3.0
Published
React components for embedding FormVue lead capture forms — Inline, Modal, Contact, Content widgets with Shadow DOM isolation.
Readme
@formvue/embed-react
React components for embedding FormVue lead-capture widgets in any modern React application — Next.js, Vite, TanStack Start, Remix, Astro, Gatsby.
Each component renders a Preact widget inside a Shadow DOM so styles, scripts, and DOM events stay fully isolated from the host page. The widget collects a visitor's phone number and triggers a personalized SMS link to a FormVue video form.
Table of contents
- Install
- Quick start
- Components
- Props reference
- Callbacks
- TypeScript
- Framework guides
- Vanilla HTML / CDN bundles
- Content Security Policy
- SSR safety
- Bundle size
- Browser support
- License
Install
pnpm add @formvue/embed-react
# or
npm install @formvue/embed-react
# or
yarn add @formvue/embed-reactPeer dependencies:
react@^18 || ^19react-dom@^18 || ^19
The Preact runtime and all internal helpers (signals, formisch, valibot, openapi-fetch) are bundled inside the package — you don't need to install them.
Quick start
'use client';
import { FormVueModal } from '@formvue/embed-react';
export function Header() {
return (
<FormVueModal
formId="your-form-id"
id="your-button-id"
/>
);
}That's it. The component mounts a Preact-powered trigger button. Clicking it opens a modal containing the FormVue form with phone-number capture, validation, and submission baked in.
Components
The package exposes four components — one per widget mode. They share the same prop shape (extending the corresponding FormVueXxxOptions interface) and lifecycle pattern (useEffect mount → Preact render → cleanup on unmount).
<FormVueInline />
Animated inline button that expands into a phone-number input.
[ Start Your Free Estimate ▶ ] → [ +1 (___) ___-____ ▶ ]Behavior:
- State machine:
button→input→submitting→success/error - CSS transitions on flex, opacity, and transform
- Mobile fallback renders a simple
<a>link instead of the interactive widget (screen size + pointer/hover capability detection)
'use client';
import { FormVueInline } from '@formvue/embed-react';
export function CTA() {
return (
<FormVueInline
formId="your-form-id"
variant="primary"
size="lg"
text="Get a free quote"
label="Trusted by 10,000+ homeowners:"
onSuccess={(data) => console.log('Submitted!', data)}
/>
);
}<FormVueModal />
Trigger button that opens a Shadow-DOM-isolated modal overlay containing the full embed form.
Behavior:
- Trigger button uses the same variants/styling as
<FormVueInline /> - Modal is mounted on
document.bodyin its own Shadow DOM (avoids z-index stacking issues with the host page) - Form fields are built dynamically from your FormVue embed config (text, email, phone, number, textarea, select, radio, checkbox)
- Mobile: opens the form in the same tab via deep link (configurable)
'use client';
import { FormVueModal } from '@formvue/embed-react';
export function NavbarCTA() {
return (
<FormVueModal
formId="your-form-id"
id="your-embed-id"
text="Open Video Form"
label="Complete from your phone:"
/>
);
}<FormVueContact />
Embedded form rendered directly without a trigger button — ideal for contact pages and dedicated form sections.
Behavior:
- Same dynamic field rendering as
<FormVueModal /> - No trigger / no modal — form is always visible
- Skeleton state during config fetch
'use client';
import { FormVueContact } from '@formvue/embed-react';
export function ContactPage() {
return (
<section>
<h1>Get in touch</h1>
<FormVueContact
formId="your-form-id"
id="your-embed-id"
onSuccess={(data) => console.log('Submitted!', data)}
/>
</section>
);
}<FormVueContent />
Embedded form with a coupon screen shown after submission and before the SMS is sent. Use when you want to incentivize completion with a discount code, promo offer, or printable voucher.
Behavior:
- Contact form → coupon screen → SMS submission
- Coupon copy (
headline,offer,ctaLabel,subtext) configured per embed in FormVue dashboard - Same dynamic fields as Contact
'use client';
import { FormVueContent } from '@formvue/embed-react';
export function PromoSection() {
return (
<FormVueContent
formId="your-form-id"
id="your-embed-id"
/>
);
}Props reference
Common to all components
| Prop | Type | Default | Description |
|---|---|---|---|
| formId | string | — | Required. FormVue form UUID. |
| id | string | — | Embed config ID (required for Modal / Contact / Content). |
| locale | string | 'en-US' | Locale for phone formatting and validation. |
| attribution | Attribution | — | UTM parameters and campaign attribution. |
| mobileNewTab | boolean | mode-dependent | Whether the mobile form link opens in a new tab. |
| className | string | — | Class applied to the host <div> (not the Shadow DOM content). |
| style | React.CSSProperties | — | Inline styles applied to the host <div>. |
| onSubmit | (phone: string) => void | — | Fires when the user submits a phone number. |
| onSuccess | (data: FormLinkResponse) => void | — | Fires after the API returns a successful form-link creation. |
| onError | (err: Error) => void | — | Fires on any submission error (network, validation, API). |
Button styling (Inline + Modal)
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | 'default' \| 'primary' \| 'outline' | 'default' | Button layout. default has a body + logo overlay on the right; primary puts the logo on the left; outline is primary with a light border. |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Preset dimensions (see size presets). |
| text | string | 'Start Your Free Estimate' | Button label. |
| label | string | 'Get Faster Quotes With Video:' | Caption shown above the button. |
| width | 'auto' \| 'full' | 'auto' | Whether the button fills its container width. |
| height | number | from size preset | Override button height in pixels. |
| fontSize | number | from size preset | Button text size in pixels. |
| borderRadius | number | from size preset | Corner radius in pixels (set to height / 2 for a pill). |
| glass | boolean | false | Adds a translucent backdrop-blur background — pair with colors.label = '#fff' over dark backgrounds. |
| buttonBorder | string | 'none' | CSS border shorthand applied to the button (e.g. '1px solid #ccc'). |
| colors | Colors | see color defaults | Per-element colors. |
| inputBorder | string (Inline only) | 'none' | CSS border on the expanded phone input. |
Size presets
| Preset | height | fontSize | borderRadius | logoSize |
|---|---|---|---|---|
| sm | 36 | 13 | 8 | 24 |
| md | 44 | 16 | 10 | 32 |
| lg | 52 | 18 | 12 | 36 |
Color defaults
| Key | Default | Used for |
|---|---|---|
| button | #007145 | Button background |
| buttonText | #FFFFFF | Button text |
| logo | #5A5A5A | Logo background |
| logoIcon | #FFFFFF | Logo icon |
| label | #5A5A5A | Caption above the button |
<FormVueInline
formId="..."
colors={{
button: '#2563eb',
buttonText: '#ffffff',
logo: '#1e40af',
logoIcon: '#ffffff',
label: '#374151',
}}
/>Callbacks
All four components accept onSubmit, onSuccess, and onError. They're stable across re-renders — the wrapper uses useStableCallbacks so updates don't tear down the Preact tree.
<FormVueModal
formId="..."
id="..."
onSubmit={(phone) => {
// Fires before the API request
console.log('Submitting:', phone);
}}
onSuccess={(data) => {
// Fires after the form-link is created
// data.id is the FormLink UUID
analytics.track('lead_captured', { formLinkId: data.id });
}}
onError={(err) => {
// Fires on network failure, validation error, or API error
Sentry.captureException(err);
}}
/>TypeScript
The package ships full TypeScript declarations. All props are typed; auto-complete shows every option in your editor.
import type {
FormVueInlineOptions,
FormVueModalOptions,
FormVueContactOptions,
FormVueContentOptions,
} from '@formvue/embed-react';
// Build helper functions that wrap our types
function makeButtonProps(formId: string): FormVueInlineOptions {
return {
formId,
variant: 'primary',
size: 'lg',
};
}Framework guides
Next.js (App Router)
The package preserves the "use client" directive on its ESM chunk, so Next.js treats the import boundary as a Client Component automatically — no transpilePackages needed.
// app/contact/page.tsx
import { FormVueModal } from '@formvue/embed-react';
export default function ContactPage() {
return (
<main>
<h1>Get a quote</h1>
<FormVueModal formId="..." id="..." />
</main>
);
}If you want the form to render conditionally inside a Server Component:
// app/page.tsx (Server Component)
import dynamic from 'next/dynamic';
const FormVueModal = dynamic(
() => import('@formvue/embed-react').then((m) => m.FormVueModal),
{ ssr: false }
);
export default function Page() {
return <FormVueModal formId="..." id="..." />;
}Next.js (Pages Router)
Works the same way. If you're on Next < 13.1, add the package to transpilePackages in next.config.js:
module.exports = {
transpilePackages: ['@formvue/embed-react'],
};Vite + React
No setup required — just import and render.
// src/App.tsx
import { FormVueInline } from '@formvue/embed-react';
export default function App() {
return <FormVueInline formId="..." />;
}TanStack Start
Treat the components as client-side; mount inside a route component.
// app/routes/contact.tsx
import { createFileRoute } from '@tanstack/react-router';
import { FormVueContact } from '@formvue/embed-react';
export const Route = createFileRoute('/contact')({
component: ContactPage,
});
function ContactPage() {
return <FormVueContact formId="..." id="..." />;
}Remix
// app/routes/contact.tsx
import { ClientOnly } from 'remix-utils/client-only';
import { FormVueModal } from '@formvue/embed-react';
export default function Contact() {
return (
<ClientOnly fallback={<div style={{ height: 44 }} />}>
{() => <FormVueModal formId="..." id="..." />}
</ClientOnly>
);
}ClientOnly is optional — the component is SSR-safe — but using it skips the server-rendered empty <div> flash.
Vanilla HTML / CDN bundles
For non-React integrations (WordPress, Webflow, plain HTML, static sites), use the CDN bundles instead. They're fully bundled IIFE scripts that expose mode-specific globals.
<div id="formvue-btn"></div>
<script src="https://cdn.formvue.com/v1/formvue-modal.js"></script>
<script>
FormVueModal.button('#formvue-btn', {
formId: 'your-form-id',
id: 'your-embed-id',
});
</script>Available bundles:
| URL | Global | Method |
|---|---|---|
| cdn.formvue.com/v1/formvue-inline.js | window.FormVueInline | .button(target, options) |
| cdn.formvue.com/v1/formvue-modal.js | window.FormVueModal | .button(target, options) |
| cdn.formvue.com/v1/formvue-contact.js | window.FormVueContact | .render(target, options) |
| cdn.formvue.com/v1/formvue-content.js | window.FormVueContent | .render(target, options) |
All globals also expose .destroy(target) and .update(target, options) for programmatic teardown / re-render.
Content Security Policy
The widget injects a <style> tag inside its Shadow DOM at mount time (Tailwind CSS). Sites with strict CSP need to allow style-src 'unsafe-inline' on the host page for the styles to apply.
Content-Security-Policy: style-src 'self' 'unsafe-inline'; script-src 'self' https://cdn.formvue.com;A nonce-based mechanism is on the roadmap.
SSR safety
All components are safe to import from Server Components and SSR runtimes:
- The wrapper renders a plain
<div ref>on the server — no DOM access. - All widget mounting happens inside
useEffect, which runs only on the client. - The internal SDK guards
windowaccess withtypeof window !== 'undefined'checks. - Strict Mode double-mount is handled —
useEffectcleanup tears down the Shadow DOM and re-mounts cleanly.
Bundle size
| Build | Size | Gzipped |
|---|---|---|
| dist/index.js (npm ESM) | ~220 KB | ~55 KB |
| dist/cdn/formvue-inline.js (CDN IIFE) | ~62 KB | ~19 KB |
| dist/cdn/formvue-modal.js (CDN IIFE) | ~102 KB | ~30 KB |
| dist/cdn/formvue-contact.js (CDN IIFE) | ~94 KB | ~28 KB |
| dist/cdn/formvue-content.js (CDN IIFE) | ~96 KB | ~29 KB |
The npm bundle ships all four components in one ESM module so tree-shaking is automatic — importing only FormVueInline won't drag in Modal / Contact / Content widget code (most of it; some shared chunks like config resolution are always included).
Browser support
- Chrome / Edge ≥ 90
- Safari ≥ 14
- Firefox ≥ 88
- Mobile Safari ≥ 14
- Chrome for Android ≥ 90
Requires native Shadow DOM (no polyfill). All modern browsers from 2021 onward.
License
MIT © FormVue
