Questions? Ask them all. π
Question Sent β¨
We've received your questions and will provide feedback soon. Thanks for your patienceπ.
Complete guide to creating, customizing, and deploying forms to inbox in minutes
Get your first form running in under 2 minutes. Here's the fastest way to add a working contact form to any website:
Add one script tag to your page
Drop in the genuine-form component
Set your API key and subject
Copy and paste this complete working example:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Contact Form</title>
</head>
<body>
<!-- Load Genuine Forms -->
<script type="module"
src="https://cryptng.github.io/genuine-forms-vanillajs/genuine-form.js">
</script>
<h1>Contact Us</h1>
<genuine-form
name="contact-form"
subject="New Contact Request"
api-key="YOUR_API_KEY">
<label>Name:</label>
<input name="name" required /><br/>
<label>Email:</label>
<input name="email" type="email" required /><br/>
<label>Message:</label>
<textarea name="message" rows="5" required></textarea><br/>
<input name="terms" type="checkbox" required />
<label>I agree to the terms</label><br/>
<genuine-captcha>
<button type="submit">Send Message</button>
</genuine-captcha>
</genuine-form>
</body>
</html>
That's it! Replace YOUR_API_KEY with your actual API key from the dashboard, and you have a fully working form with captcha protection.
Genuine Forms can be installed in two ways depending on your project setup:
Simply add this script tag anywhere in your HTML file:
<script type="module"
src="https://cryptng.github.io/genuine-forms-vanillajs/genuine-form.js">
</script>
Best for: Static sites, WordPress, simple projects, rapid prototyping
If you're using a build system (Webpack, Vite, etc.), install via npm:
npm install @genuine-forms/web-components
Then import it in your app's entry point:
// app.js or main.js
import '@genuine-forms/web-components';
Best for: React, Vue, Svelte, Angular, Next.js, Nuxt, or any framework with a build step
To verify the component is loaded, open your browser console and type:
console.log(customElements.get('genuine-form'));
If you see a function definition, you're all set!
Every Genuine Form has three essential parts:
<genuine-form>
<!-- 1. Your input fields -->
<input name="email" type="email" required />
<!-- 2. Captcha wrapper -->
<genuine-captcha>
<!-- 3. Submit button -->
<button type="submit">Send</button>
</genuine-captcha>
</genuine-form>
Genuine Forms automatically collects data from these standard HTML elements:
<input name="username" type="text" required />
<input name="email" type="email" required />
<input name="phone" type="tel" />
<input name="website" type="url" />
<textarea name="message" rows="5" required></textarea>
<input name="newsletter" type="checkbox" />
<input name="terms" type="checkbox" required />
Collected as: true or false
<input name="plan" type="radio" value="basic" />
<input name="plan" type="radio" value="pro" />
<input name="plan" type="radio" value="enterprise" required />
Collected as: the selected value (e.g., "pro")
<!-- Single select -->
<select name="country" required>
<option value="">Select country</option>
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
</select>
<!-- Multi-select -->
<select name="interests" multiple>
<option value="design">Design</option>
<option value="dev">Development</option>
<option value="marketing">Marketing</option>
</select>
Important: All form elements must have a name attribute to be collected. Elements without names are ignored.
Genuine Forms respects standard HTML5 validation attributes:
<input name="age" type="number" min="18" max="120" required />
<input name="zipcode" pattern="[0-9]{5}" required />
Configure your form's behavior with these attributes on the <genuine-form> element:
Attribute | Type | Required | Description |
---|---|---|---|
api-key | string | Yes* | Your Genuine Forms API key from the dashboard |
subject | string | No | Email subject line (default: "Generic Subject") |
name | string | No** | Unique form identifier (default: "genuine-form") |
api-url | string | No | Custom API endpoint (advanced usage) |
receiver | string | No | XOR-encrypted email for Developer Preview mode |
* Required in production. Use receiver for Developer Preview.
** Required when using multiple forms on the same page.
<genuine-form
api-key="gf_1234567890abcdef"
subject="Contact Form Submission"
name="contact-form">
<!-- form fields -->
</genuine-form>
<genuine-form
receiver="16,65,20,7,21,12,28,24,0,35,21,10,27,20..."
subject="Test Submission"
name="test-form">
<!-- form fields -->
</genuine-form>
Getting your API key: Log into your Genuine Forms dashboard and navigate to Settings β API Keys. Your key starts with gf_
If you're using your own backend instead of Genuine Forms' hosted service:
<genuine-form
api-url="https://your-api.com/send-email"
api-key="your-custom-key"
subject="Form Submission">
<!-- form fields -->
</genuine-form>
Custom Backend Requirements: Your endpoint must accept GET requests with query parameters: captchaSolution, captchaSecret, apiKey, subject, and body. It should return JSON with a body property on success.
Genuine Forms provides a powerful event system to respond to form actions. Access events through the global window.genuineForms object.
Event | Parameters | When Triggered |
---|---|---|
send-response | { ok, body?, error? } | After submission completes (success or failure) |
validation-failed | none | When form validation fails on submit |
started-sending | none | Immediately before sending the request |
finished-sending | none | After request completes (regardless of result) |
<script>
// Wait for the form to be ready
window.addEventListener('DOMContentLoaded', () => {
// Register event handlers
window.genuineForms['contact-form'].on('send-response', (response) => {
if (response.ok) {
alert('Thank you! Your message was sent successfully.');
console.log('Server response:', response.body);
} else {
alert('Sorry, there was an error sending your message.');
console.error('Error:', response.error);
}
});
window.genuineForms['contact-form'].on('validation-failed', () => {
alert('Please fill out all required fields correctly.');
});
});
</script>
<genuine-form name="contact-form" api-key="YOUR_KEY">
<input name="email" type="email" required />
<textarea name="message" required></textarea>
<genuine-captcha>
<button id="submit-btn" type="submit">Send Message</button>
</genuine-captcha>
<div id="status"></div>
</genuine-form>
<script>
const submitBtn = document.getElementById('submit-btn');
const status = document.getElementById('status');
window.genuineForms['contact-form'].on('started-sending', () => {
submitBtn.disabled = true;
submitBtn.textContent = 'Sending...';
status.textContent = 'Submitting your message...';
status.style.color = '#667eea';
});
window.genuineForms['contact-form'].on('send-response', (response) => {
submitBtn.disabled = false;
submitBtn.textContent = 'Send Message';
if (response.ok) {
status.textContent = 'β Message sent successfully!';
status.style.color = '#10b981';
// Clear the form
document.querySelector('genuine-form').reset();
} else {
status.textContent = 'β Failed to send message. Please try again.';
status.style.color = '#dc2626';
}
});
window.genuineForms['contact-form'].on('validation-failed', () => {
status.textContent = 'β Please fill out all required fields.';
status.style.color = '#f59e0b';
});
</script>
// Remove a specific event handler
window.genuineForms['contact-form'].off('send-response');
// Remove all handlers for an event
window.genuineForms['contact-form'].off('validation-failed');
Tip: In single-page applications, remove event handlers when components unmount to prevent memory leaks.
Override default behavior by defining global functions before your form initializes. The component waits up to 15 seconds for these functions.
Replace the built-in validation with your own rules:
<script>
window.genuineFormHandleValidate = (formName, formElement) => {
// Get form fields
const email = formElement.querySelector('[name="email"]');
const age = formElement.querySelector('[name="age"]');
const terms = formElement.querySelector('[name="terms"]');
// Custom validation rules
if (!email.value.endsWith('@company.com')) {
alert('Please use your company email address');
return false;
}
if (parseInt(age.value) < 18) {
alert('You must be 18 or older');
return false;
}
if (!terms.checked) {
alert('You must accept the terms and conditions');
return false;
}
return true; // All checks passed
};
</script>
Control exactly what gets sent in the email:
<script>
window.genuineFormGenerateSubjectAndBody = (formName, formElement, defaultSubject) => {
// Collect form data
const inputs = formElement.querySelectorAll('[name]');
const data = {};
inputs.forEach(input => {
if (input.type === 'checkbox') {
data[input.name] = input.checked;
} else if (input.type === 'radio') {
if (input.checked) data[input.name] = input.value;
} else if (input.tagName === 'SELECT' && input.multiple) {
data[input.name] = Array.from(input.selectedOptions).map(opt => opt.value);
} else {
data[input.name] = input.value;
}
});
// Create custom subject with user's name
const customSubject = `${defaultSubject} - ${data.name}`;
// Create formatted body
const customBody = `
New Form Submission
==================
Name: ${data.name}
Email: ${data.email}
Phone: ${data.phone || 'Not provided'}
Message:
${data.message}
Submitted: ${new Date().toLocaleString()}
Form: ${formName}
`.trim();
return {
subject: customSubject,
body: customBody
};
};
</script>
Run code when the form is ready:
<script>
window.genuineFormHandleInitialized = (formName, formElement) => {
console.log(`Form "${formName}" is now ready!`);
// Pre-fill form fields from URL parameters
const urlParams = new URLSearchParams(window.location.search);
const email = formElement.querySelector('[name="email"]');
if (email && urlParams.has('email')) {
email.value = urlParams.get('email');
}
// Add custom analytics tracking
formElement.addEventListener('submit', () => {
// Your analytics code here
console.log('Form submission tracked');
});
};
</script>
Important: Define these global functions before the <genuine-form> element appears in your HTML, or place them in a separate script tag in the <head>.
<!DOCTYPE html>
<html>
<head>
<script type="module"
src="https://cryptng.github.io/genuine-forms-vanillajs/genuine-form.js">
</script>
<script>
// Custom validation
window.genuineFormHandleValidate = (formName, formElement) => {
const email = formElement.querySelector('[name="email"]').value;
return email.includes('@'); // Simple check
};
// Custom email format
window.genuineFormGenerateSubjectAndBody = (name, el, subject) => {
const formData = {};
el.querySelectorAll('[name]').forEach(input => {
formData[input.name] = input.value;
});
return {
subject: `[${name}] ${subject}`,
body: JSON.stringify(formData, null, 2)
};
};
// Initialization
window.genuineFormHandleInitialized = (formName, formElement) => {
console.log('Form ready:', formName);
};
</script>
</head>
<body>
<genuine-form name="custom-form" api-key="YOUR_KEY">
<!-- form fields -->
</genuine-form>
</body>
</html>
Genuine Forms uses Shadow DOM with CSS custom properties, giving you full styling control while maintaining encapsulation.
Override these variables to customize the form container:
<style>
genuine-form {
/* Layout */
--form-display: flex;
--form-flex-direction: column;
--form-gap: 1rem;
--form-padding: 1rem;
/* Appearance */
--form-border-radius: 0.5rem;
--form-border: 1px solid #ccc;
--form-background-color: #fff;
--form-box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
</style>
Your form fields are slotted content, so style them normally:
<style>
/* Style inputs inside genuine-form */
genuine-form input,
genuine-form textarea,
genuine-form select {
width: 100%;
padding: 0.75rem;
border: 2px solid #e5e7eb;
border-radius: 0.5rem;
font-size: 1rem;
transition: border-color 0.2s;
}
genuine-form input:focus,
genuine-form textarea:focus,
genuine-form select:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
/* Style labels */
genuine-form label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
color: #374151;
}
/* Style submit button */
genuine-form button[type="submit"] {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 0.875rem 2rem;
border: none;
border-radius: 0.5rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
genuine-form button[type="submit"]:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}
genuine-form button[type="submit"]:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
</style>
<!DOCTYPE html>
<html>
<head>
<script type="module"
src="https://cryptng.github.io/genuine-forms-vanillajs/genuine-form.js">
</script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
max-width: 600px;
margin: 2rem auto;
padding: 0 1rem;
}
genuine-form {
--form-gap: 1.5rem;
--form-padding: 2rem;
--form-border-radius: 1rem;
--form-border: none;
--form-background-color: #f9fafb;
--form-box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
genuine-form label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
color: #374151;
}
genuine-form input,
genuine-form textarea {
width: 100%;
padding: 0.875rem;
border: 2px solid #e5e7eb;
border-radius: 0.5rem;
font-size: 1rem;
font-family: inherit;
transition: all 0.2s;
}
genuine-form input:focus,
genuine-form textarea:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
genuine-form input[type="checkbox"] {
width: auto;
margin-right: 0.5rem;
}
genuine-form button[type="submit"] {
width: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 1rem;
border: none;
border-radius: 0.5rem;
font-size: 1.125rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
genuine-form button[type="submit"]:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4);
}
</style>
</head>
<body>
<h1>Contact Us</h1>
<genuine-form name="styled-form" api-key="YOUR_KEY" subject="Contact">
<div>
<label for="name">Name</label>
<input id="name" name="name" required />
</div>
<div>
<label for="email">Email</label>
<input id="email" name="email" type="email" required />
</div>
<div>
<label for="message">Message</label>
<textarea id="message" name="message" rows="5" required></textarea>
</div>
<div>
<input id="terms" name="terms" type="checkbox" required />
<label for="terms" style="display: inline;">I agree to the terms</label>
</div>
<genuine-captcha>
<button type="submit">Send Message</button>
</genuine-captcha>
</genuine-form>
</body>
</html>
Pro Tip: Use CSS Grid or Flexbox on wrapper divs inside the form for more complex layouts like multi-column forms.
When using multiple forms on the same page, give each form a unique name attribute:
<!-- Contact Form -->
<genuine-form
name="contact-form"
subject="Contact Request"
api-key="YOUR_KEY">
<input name="email" type="email" required />
<textarea name="message" required></textarea>
<genuine-captcha>
<button type="submit">Contact Us</button>
</genuine-captcha>
</genuine-form>
<!-- Newsletter Form -->
<genuine-form
name="newsletter-form"
subject="Newsletter Signup"
api-key="YOUR_KEY">
<input name="email" type="email" required />
<input name="name" required />
<genuine-captcha>
<button type="submit">Subscribe</button>
</genuine-captcha>
</genuine-form>
<script>
// Contact form events
window.genuineForms['contact-form'].on('send-response', (response) => {
if (response.ok) {
document.getElementById('contact-status').textContent =
'Thank you for contacting us!';
}
});
// Newsletter form events
window.genuineForms['newsletter-form'].on('send-response', (response) => {
if (response.ok) {
document.getElementById('newsletter-status').textContent =
'Successfully subscribed!';
}
});
</script>
The custom hook functions receive the form name, so you can apply different logic:
<script>
window.genuineFormHandleValidate = (formName, formElement) => {
if (formName === 'contact-form') {
// Custom validation for contact form
const message = formElement.querySelector('[name="message"]');
if (message.value.length < 10) {
alert('Message must be at least 10 characters');
return false;
}
}
if (formName === 'newsletter-form') {
// Custom validation for newsletter
const email = formElement.querySelector('[name="email"]');
if (!email.value.includes('@')) {
alert('Please enter a valid email');
return false;
}
}
return true;
};
window.genuineFormGenerateSubjectAndBody = (formName, formElement, subject) => {
const inputs = formElement.querySelectorAll('[name]');
const data = {};
inputs.forEach(input => {
data[input.name] = input.value;
});
// Different formatting for each form
let body;
if (formName === 'contact-form') {
body = `Contact Request\n\nEmail: ${data.email}\nMessage: ${data.message}`;
} else if (formName === 'newsletter-form') {
body = `Newsletter Signup\n\nName: ${data.name}\nEmail: ${data.email}`;
}
return {
subject: `[${formName}] ${subject}`,
body: body
};
};
</script>
Warning: If you have multiple forms without unique names, you'll see a console warning, and only the first submit button will work properly.
Possible causes:
Solution:
// Check browser console for errors
// Verify structure:
<genuine-form api-key="YOUR_KEY">
<input name="test" required />
<genuine-captcha>
<button type="submit">Send</button> <!-- Must be here -->
</genuine-captcha>
</genuine-form>
Possible causes:
Solution:
<!-- Check that captcha script loads -->
<!-- It's automatically imported, but you can verify in Network tab -->
<!-- If using CSP, allow: -->
<meta http-equiv="Content-Security-Policy"
content="script-src 'self' https://cryptng.github.io;
style-src 'self' 'unsafe-inline';">
Possible causes:
Solution:
<!-- β Wrong - no name attribute -->
<input type="email" required />
<!-- β Correct - has name -->
<input name="email" type="email" required />
Possible causes:
Solution:
<script>
// β Wrong - might run too early
window.genuineForms['my-form'].on('send-response', handler);
// β Correct - wait for DOM
window.addEventListener('DOMContentLoaded', () => {
window.genuineForms['my-form'].on('send-response', handler);
});
// Or check if it exists
if (window.genuineForms && window.genuineForms['my-form']) {
window.genuineForms['my-form'].on('send-response', handler);
}
</script>
Possible causes:
Solution:
<!-- β Define before component -->
<head>
<script>
window.genuineFormHandleValidate = (name, el) => {
// Your logic
return true;
};
</script>
<script type="module" src="genuine-form.js"></script>
</head>
<body>
<genuine-form>...</genuine-form>
</body>
Possible causes:
Solution:
// If using custom backend, ensure CORS headers:
Access-Control-Allow-Origin: *
// Or specific domain:
Access-Control-Allow-Origin: https://yourdomain.com
// For local testing, use a local server:
// Python: python -m http.server 8000
// Node: npx serve
// PHP: php -S localhost:8000
Possible causes:
Solution:
<!-- β Each form needs unique name -->
<genuine-form name="form-1" api-key="KEY">...</genuine-form>
<genuine-form name="form-2" api-key="KEY">...</genuine-form>
Still Having Issues?
Debugging Checklist:
Need more help? Contact us at below with your browser console output, code snippet or any question you have.
We've received your questions and will provide feedback soon. Thanks for your patienceπ.
Start free with 1,000 credits. No credit card required.
Get Started Free