A lightweight, embeddable contact form widget for websites. Easily add beautiful, customizable contact forms to any website with just a few lines of code.
- Features
- Getting Started
- Quick Start
- Installation
- Configuration
- Field Types
- Examples
- Development
- Building
- Contributing
- Browser Support
- API Reference
- Security
- License
- Support
- Changelog
- π Easy Integration - Add forms to your site with a single script tag
- π¨ Customizable Themes - Light, dark, and system theme support
- π± Fully Responsive - Works perfectly on all devices
- β Client-side Validation - Real-time form validation
- π International Support - Phone number formatting for 40+ countries
- π File Uploads - Support for file attachments with drag-and-drop
- β Rich Field Types - Text, email, phone, file uploads, ratings, rich text editor, and more
- π Custom Callbacks - Success and error handlers for custom logic
- βΏ Accessible - WCAG compliant with proper ARIA labels
Before you can use the widget, you'll need an EZCONTACTFORM account:
- Create an account at www.ezcontactform.com
- Create a form in your dashboard
- Get your Source ID from the form settings
- Add the widget to your website using the code examples below
The widget is free to use and open source. You only need an EZCONTACTFORM account to manage your forms and receive submissions.
Add this to your HTML:
<div id="ezform-container"></div>
<script src="https://cdn.ezcontactform.com/widget.js"
data-source-id="YOUR_SOURCE_ID"></script>Note: See CDN URLs section for versioned URLs and fallback options.
<div id="ezform-container"></div>
<script src="https://cdn.ezcontactform.com/widget.js"
data-source-id="YOUR_SOURCE_ID"
data-theme="dark"></script><div id="my-custom-form"></div>
<script src="https://cdn.ezcontactform.com/widget.js"
data-source-id="YOUR_SOURCE_ID"
data-container="my-custom-form"></script><div id="ezform-container"></div>
<script>
(function(w,d,s,f) {
w.EZForm=w.EZForm||{};
w.EZForm.forms=w.EZForm.forms||[];
w.EZForm.forms.push({
sourceId: 'YOUR_SOURCE_ID',
container: 'ezform-container',
options: {
theme: 'light',
metadata: { campaign_id: 'abc123' },
onSuccess: function(response) {
console.log('Form submitted:', response);
},
onError: function(error) {
console.error('Error:', error);
}
}
});
if(!w.EZForm.loaded) {
var js=d.createElement(s);
js.src=f;
js.async=true;
d.getElementsByTagName('head')[0].appendChild(js);
w.EZForm.loaded=true;
}
})(window,document,'script','https://cdn.ezcontactform.com/widget.js');
</script>Use the hosted version from our CDN. We provide both versioned URLs (for production stability) and latest URLs (for development).
Primary CDN (Cloudflare):
- JavaScript:
https://cdn.ezcontactform.com/widget.js(latest) - CSS:
https://cdn.ezcontactform.com/widget.css(latest) - Versioned JavaScript:
https://cdn.ezcontactform.com/widget-{version}.js(e.g.,widget-1.4.0.js) - Versioned CSS:
https://cdn.ezcontactform.com/widget-{version}.css(e.g.,widget-1.4.0.css)
Fallback CDN:
- JavaScript:
https://app.ezcontactform.com/widget.js(latest) - CSS:
https://app.ezcontactform.com/widget.css(latest) - Versioned JavaScript:
https://app.ezcontactform.com/widget-{version}.js - Versioned CSS:
https://app.ezcontactform.com/widget-{version}.css
Latest version (recommended for development):
<script src="https://cdn.ezcontactform.com/widget.js"></script>
<link rel="stylesheet" href="https://cdn.ezcontactform.com/widget.css">Pinned version (recommended for production):
<script src="https://cdn.ezcontactform.com/widget-1.4.0.js"></script>
<link rel="stylesheet" href="https://cdn.ezcontactform.com/widget-1.4.0.css">Note: Versioned URLs are immutable and cached forever, while latest URLs are cached for 1 hour. For production, we recommend using versioned URLs to ensure stability.
- Download
widget.jsandwidget.cssfrom this repository - Host them on your server
- Update the script source in your HTML
data-source-id- Required. Your source ID from the admin paneldata-theme- Theme:light,dark, orsystem(default:light)data-version- Pin to a specific version (default:latest)data-container- Custom container element ID (default:ezform-containeror auto-generated)data-disable-styles- Set totrueto use your own CSSdata-enable-logging- Set totrueto enable console loggingdata-container- Custom container ID (default: auto-generated orezform-container)
The container ID is the HTML element ID where the form will be rendered.
Default behavior:
- If you have a
<div id="ezform-container"></div>before the script tag, it will use that - Otherwise, the widget creates a container automatically with ID
ezform-{sourceId}
Custom container:
<div id="my-custom-form-container"></div>
<script src="https://cdn.ezcontactform.com/widget.js"
data-source-id="YOUR_SOURCE_ID"
data-container="my-custom-form-container"></script>Programmatic usage:
EZForm.renderForm({
sourceId: 'YOUR_SOURCE_ID',
container: 'my-custom-form-container', // Container ID
options: {}
})The options object allows fine-grained control over form behavior:
{
// Theme Configuration
theme: 'light', // 'light', 'dark', or 'system' (default: 'light')
// 'system' auto-detects user's OS preference
// Styling
disableStyles: false, // Set to true to use your own CSS
customCSS: '', // Custom CSS string to inject
// Logging
enableLogging: false, // Enable console logging for debugging
// Messages
successMessage: 'Custom message', // Override default success message
// Custom Metadata
metadata: { // Custom metadata passed with submission
campaign_id: 'abc123', // Tracking campaign ID
source: 'homepage', // Source page identifier
user_id: 'user123', // User identifier
// Any custom key-value pairs
},
// Callbacks
onSuccess: function(response) {
// Called when form submits successfully
// response: { id, status, message, ... }
console.log('Form submitted:', response);
},
onError: function(error) {
// Called when form submission fails
// error: { message, ... }
console.error('Submission error:', error);
}
}Custom metadata can be passed with form submissions for tracking and analytics. Your custom metadata is stored in metadata.client on the server and can be used for filtering, reporting, and integrations.
Via Options Object (Recommended):
EZForm.renderForm({
sourceId: 'abc123',
container: 'ezform-container',
options: {
metadata: {
campaign_id: 'summer-2024',
source: 'homepage',
user_segment: 'premium'
}
}
})Via Script Tag (using hidden field):
<div id="ezform-container"></div>
<input type="hidden" name="metadata" value='{"campaign_id":"summer-2024","source":"homepage"}' />
<script src="https://cdn.ezcontactform.com/widget.js"
data-source-id="YOUR_SOURCE_ID"></script>How it works:
- Metadata passed via
options.metadatais automatically wrapped inmetadata.clientbefore sending to the API - The widget sends metadata as a separate field in the submission payload:
{ data: {...}, metadata: { client: {...} } } - Server stores your custom metadata in
metadata.clientfor easy querying
Supported types:
string- Text valuesnumber- Numeric valuesboolean- True/false valuesobject- Nested objectsarray- Arrays of strings, numbers, or objectsnull- Null values
Limitations:
- Maximum payload size: 256KB per field (data and metadata combined)
- Maximum keys: 100 keys per object
- No functions or circular references
Simple metadata:
metadata: {
campaign_id: 'summer-2024',
source: 'homepage',
referrer: 'google',
user_id: 'user123'
}Nested metadata:
metadata: {
tracking: {
campaign: 'summer-2024',
ad_group: 'banner-ads',
keyword: 'contact-form',
creative: 'banner-728x90'
},
user: {
segment: 'premium',
cohort: '2024-Q1',
lifetime_value: 1500
},
page: {
url: '/contact',
title: 'Contact Us',
section: 'footer'
}
}With arrays:
metadata: {
tags: ['marketing', 'lead-gen', 'contact'],
visited_pages: ['/about', '/pricing', '/contact'],
interests: [
{ category: 'technology', score: 0.8 },
{ category: 'business', score: 0.6 }
]
}Complex nested structure:
metadata: {
marketing: {
channel: 'email',
campaign: {
id: 'summer-sale',
variant: 'A',
test_group: 'control'
}
},
analytics: {
page: {
url: '/contact',
section: 'footer',
viewport: 'desktop'
},
user: {
segment: 'premium',
cohort: '2024-Q1'
}
}
}The server receives and stores metadata in this structure:
{
"metadata": {
"client": {
"campaign_id": "summer-2024",
"source": "homepage",
"user_segment": "premium"
},
"timing": {
"start_time": 1234567890,
"end_time": 1234567900
},
"localization": {
"locale": "en-US",
"timezone": "America/New_York"
},
"browser": {
"window_width": 1920,
"window_height": 1080,
"screen_width": 1920,
"screen_height": 1080,
"device_type": "desktop"
},
"source": {
"id": "source-uuid",
"source_key": "abc123",
"version": 1,
"pinned_version": 1
},
"widget": {
"version": "1.4.0"
},
"spam_protection": {
"blocked_by": null,
"rate_limit_remaining": 29
},
"request": {
"ip_address": "192.168.1.1",
"user_agent": "Mozilla/5.0...",
"referer": "https://example.com",
"headers": {}
},
"utm_params": {
"utm_source": "google",
"utm_medium": "cpc",
"utm_campaign": "summer-2024"
},
"referrer": "https://google.com",
"landing_page": "/contact",
"confirmation_acceptances": {
"tos": "2024-12-07T16:00:00Z"
},
"custom": {
"source_level_metadata": "value"
}
}
}The following metadata is always captured automatically:
timing- Form fill duration (start_time and end_time in milliseconds)localization- User locale (e.g., "en-US") and timezone (e.g., "America/New_York")source- Source identification (id, source_key, version)widget- Widget version (e.g., "1.4.0") - identifies which widget version created the submissionrequest- Server-side request info (IP address, user agent, referer)spam_protection- Spam protection results
The following metadata is conditionally captured:
browser- Browser details (only if enabled in source settings)window_width,window_heightscreen_width,screen_heightdevice_type(desktop, mobile, tablet)browser_name,browser_versionos_name,os_version
Once stored, you can query entries by metadata:
// Example: Filter entries by campaign
// In your backend/admin panel, you can filter by:
metadata.client.campaign_id = 'summer-2024'
// Or query nested metadata:
metadata.client.tracking.campaign = 'summer-2024'The API enforces the following limits to prevent abuse:
- Maximum payload size: 256KB per field (data and metadata combined)
- Maximum keys: 100 keys per object
- Validation: Invalid or oversized payloads will return a 400 error
Example error response:
{
"error": "Payload too large",
"success": false
}- Use consistent keys - Use the same metadata keys across forms for easier reporting
- Keep it simple - Avoid deeply nested structures (max 3-4 levels recommended)
- Size limits - Keep individual metadata objects under 10KB (well below the 256KB limit)
- No PII - Don't include personally identifiable information in metadata (use form fields instead)
- Use for tracking - Metadata is ideal for:
- Campaign tracking (UTM parameters, campaign IDs)
- A/B testing (variant IDs, test groups)
- Analytics (page context, user segments)
- Integration data (external system IDs, sync flags)
- Query-friendly - Structure metadata for easy filtering and reporting in your admin panel
Marketing campaign:
metadata: {
campaign: {
id: 'summer-2024',
channel: 'email',
variant: 'A',
test_group: 'control'
},
utm: {
source: 'newsletter',
medium: 'email',
campaign: 'summer-sale',
term: 'contact-form',
content: 'banner-top'
}
}Multi-step form tracking:
metadata: {
form: {
step: 3,
total_steps: 5,
completion_percentage: 60,
started_at: '2024-12-07T16:00:00Z'
},
user: {
session_id: 'session-xyz789',
previous_interactions: 2
}
}Integration tracking:
metadata: {
integration: {
crm_id: 'crm-12345',
sync_status: 'pending',
external_system: 'salesforce'
},
workflow: {
trigger_id: 'workflow-abc',
step: 'form_submission',
pipeline: 'lead-nurture'
}
}The widget supports a wide variety of field types:
- Text - Single-line text input
- Email - Email validation
- Phone - Phone number with international support
- Textarea - Multi-line text input
- Select - Dropdown selection
- Checkbox - Single checkbox
- Radio - Radio button group
- File Upload - File attachments with drag-and-drop
- Rating - Star rating (1-5)
- Rich Text - Markdown editor with preview
- Confirmation - Terms of service/privacy policy acceptance
- Date/Time - Date and time pickers
- Country/State - Location selectors
See widget-example.html for complete examples including:
- Simple embed
- Dark theme
- Callbacks
- Multiple forms on one page
- Custom styling
- Clone this repository
- Serve the files locally:
# Python
python -m http.server 3000
# Node.js
npx http-server -p 3000
# PHP
php -S localhost:3000- Open
widget-example.htmlin your browser
To test the widget locally:
-
Get a source ID:
- Create a free account at www.ezcontactform.com
- Create a form in your dashboard
- Copy your Source ID from the form settings
-
Configure allowed domains:
- In your EZCONTACTFORM dashboard, add
localhostto your allowed domains - This allows the widget to work during local development
- In your EZCONTACTFORM dashboard, add
-
Update the example:
- Open
src/widget-example.html - Replace
YOUR_SOURCE_IDwith your actual Source ID - Serve locally and test
- Open
Note: For production use, make sure to add your actual domain to the allowed domains list in your EZCONTACTFORM dashboard.
- Node.js 14+ and npm
# Install dependencies
npm install
# Build from package.json version
npm run build
# Build from Git tag
npm run build:tag
# Build with specific version
npm run build:version 1.4.0The build process generates versioned distribution files in the dist/ directory:
widget-{version}.js- Versioned JavaScript file (immutable)widget-{version}.css- Versioned CSS file (immutable)widget.js- Latest JavaScript file (points to current version)widget.css- Latest CSS file (points to current version)
The build script automatically:
- Updates version in widget.js header comment
- Updates version constant in the code
- Generates versioned filenames
- Creates latest symlinks/copies
-
Update version:
npm version patch # or minor, major -
Build and test locally:
npm run build # Test dist/widget.js in browser -
Create Git tag and push:
git tag v1.4.0 git push origin v1.4.0
-
Automated release:
- GitHub Actions automatically builds on tag push
- Creates GitHub Release with artifacts
- Deploys to CDN (if configured)
widget/
βββ src/
β βββ widget.js # Source file
β βββ widget.css # Styles
β βββ widget-example.html # Examples
βββ dist/ # Generated files (gitignored)
β βββ widget-1.4.0.js # Versioned
β βββ widget-1.4.0.css
β βββ widget.js # Latest
β βββ widget.css
βββ build.js # Build script
βββ package.json # Version management
βββ README.md
We welcome contributions! Please see our Contributing Guidelines and Code of Conduct for details on how to get involved.
- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
- Mobile browsers (iOS Safari, Chrome Mobile)
The widget exposes a global EZForm object with the following methods:
Initialize and render all forms on the page. Called automatically on page load, but can be called manually if needed.
EZForm.init()Render a form programmatically.
Parameters:
config.sourceId(string, required) - Your source IDconfig.container(string, required) - Container element ID where form will renderconfig.version(string, optional) - Version to use ('latest', specific number, or '2.x')config.options(object, optional) - Options object (see Options Object section)
Example:
EZForm.renderForm({
sourceId: 'abc123',
container: 'my-form-container',
version: 'latest',
options: {
theme: 'dark',
metadata: { campaign: 'summer-2024' },
onSuccess: function(response) {
console.log('Success!', response);
}
}
})Fetch form configuration from the API.
Parameters:
sourceId(string, required) - Your source IDversion(string, optional) - Version to fetch ('latest', specific number, or '2.x')
Returns: Promise resolving to form configuration object
Example:
const config = await EZForm.fetchConfig('abc123', 'latest');
console.log('Form config:', config);Log messages (respects enableLogging setting).
Parameters:
level(string) - Log level: 'log', 'warn', 'error'config(object) - Form configuration...args- Additional arguments to log
Example:
EZForm.log('log', config, 'Form loaded successfully');Escape HTML to prevent XSS attacks.
Parameters:
text(string) - Text to escape
Returns: Escaped HTML string
Example:
const safe = EZForm.escapeHtml('<script>alert("xss")</script>');
// Returns: "<script>alert("xss")</script>"Parse markdown text to HTML.
Parameters:
text(string) - Markdown text
Returns: HTML string
Example:
const html = EZForm.parseMarkdown('**Bold** text');
// Returns: "<strong>Bold</strong> text"The widget dispatches custom events you can listen to:
Dispatched when a form should auto-open (based on auto_open_delay setting).
document.addEventListener('ezform:auto-open', function(event) {
const sourceId = event.detail.sourceId;
// Show modal or scroll to form
console.log('Auto-open form:', sourceId);
});Event detail:
sourceId(string) - The source ID that should auto-open
Called when form submission succeeds.
Parameters:
response(object) - Server response containing:id(string) - Entry IDstatus(string) - Status (usually 'success')message(string) - Success message
Example:
options: {
onSuccess: function(response) {
console.log('Entry ID:', response.id);
// Redirect to thank you page
window.location.href = '/thank-you';
}
}Called when form submission fails.
Parameters:
error(object) - Error object containing:message(string) - Error messagestatus(number, optional) - HTTP status codedetails(object, optional) - Additional error details
Example:
options: {
onError: function(error) {
console.error('Submission failed:', error.message);
// Show custom error message
alert('Sorry, there was an error. Please try again.');
}
}We take security seriously. Please see our Security Policy for reporting vulnerabilities and a list of built-in security features.
This project is licensed under the MIT License - see the LICENSE file for details.
Need help? We're here for you:
- π¬ Help Center - www.ezcontactform.com/support
- π Bug Reports - GitHub Issues
- π Documentation - www.ezcontactform.com/docs
- π§ Email - support@ezcontactform.com
For widget-specific issues or feature requests, please use GitHub Issues. For account-related questions or general support, visit our Help Center.
- Added international phone support
- Added file upload with drag-and-drop
- Added rich text editor
- Added confirmation modal
- Improved accessibility
- Dark theme improvements
- Version pinning support
- Metadata structure updates
- Browser details capture (optional)
- Initial release
- Basic form rendering
- Light/dark themes
- Client-side validation
Made with β€οΈ by the EZCONTACTFORM team