Framer
Add a contact form to a Framer site with an embed or code component that posts to SilentForms.
Framer has its own form builder, but to send a form to SilentForms you add the form as code: either HTML in Framer's Embed component, or a React code component. Both run in the visitor's browser and post straight to the submit endpoint.
Get an access key on the access key page first. It only routes submissions to your inbox, so it is safe in published code.
Why not Framer's native form#
Framer's built-in forms can send submissions to a webhook URL. That request comes from Framer, not from the visitor's browser, and it is meant for receivers that understand Framer's own webhook format (Framer signs it and retries failures). SilentForms is built to receive a form post or a fetch call from the visitor's browser: that is what its per-visitor rate limits, domain restriction, honeypot and CAPTCHA checks are designed around. Use one of the two routes below; they are the supported setup.
Option 1: HTML in an Embed component#
This is the quickest route and needs no code editor.
- Open the Insert menu, search for Embed and drag it onto the canvas.
- In the Embed's settings, paste the form below into the HTML section (not the URL input).
- Give the Embed enough height on the canvas for the form and its status message, for example 460 px.
<form id="sf-form" style="display: grid; gap: 8px; font-family: inherit;">
<input type="hidden" name="accessKey" value="YOUR_ACCESS_KEY_HERE">
<label for="sf-name">Name</label>
<input id="sf-name" type="text" name="name" required>
<label for="sf-email">Email</label>
<input id="sf-email" type="email" name="email" required>
<label for="sf-message">Message</label>
<textarea id="sf-message" name="message" rows="5" required></textarea>
<input type="checkbox" name="botcheck" style="display: none;" tabindex="-1" autocomplete="off">
<button type="submit">Send message</button>
<p id="sf-status" role="status"></p>
</form>
<script>
const form = document.getElementById('sf-form');
const status = document.getElementById('sf-status');
form.addEventListener('submit', async (event) => {
event.preventDefault();
const button = form.querySelector('button');
button.disabled = true;
status.textContent = 'Sending…';
try {
const response = await fetch('https://silentforms.com/api/submit', {
method: 'POST',
headers: { Accept: 'application/json' },
body: new FormData(form),
});
const result = await response.json();
status.textContent = result.ok ? 'Thanks! Your message has been sent.' : result.message;
if (result.ok) form.reset();
} catch {
status.textContent = 'Something went wrong. Please try again.';
} finally {
button.disabled = false;
}
});
</script>The form submits with fetch and shows the result in place instead of navigating away. That is deliberate: depending on how Framer renders the embed, a normal form post could load the result page inside the embed's box rather than the whole window. With Accept: application/json the endpoint answers with JSON, and its error messages, such as "Email address is invalid.", are written for visitors.
Scripts in an embed may not run on the canvas, so test on the preview or the published site.
Option 2: a code component#
A code component is a React component you write inside Framer. It renders in the page itself, can use your site's fonts and can expose settings, such as the access key, in the properties panel.
- Open the Assets panel, select Code and click Create Code File.
- Name it
ContactFormand replace its contents with:
import { useState } from 'react';
import { addPropertyControls, ControlType } from 'framer';
export default function ContactForm({ accessKey = 'YOUR_ACCESS_KEY_HERE', buttonLabel = 'Send message' }) {
const [status, setStatus] = useState('idle'); // idle | sending | sent | error
const [message, setMessage] = useState('');
async function handleSubmit(event) {
event.preventDefault();
const form = event.currentTarget;
setStatus('sending');
try {
const response = await fetch('https://silentforms.com/api/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({ ...Object.fromEntries(new FormData(form)), accessKey }),
});
const result = await response.json();
if (!response.ok) throw new Error(result.message);
setStatus('sent');
setMessage('Thanks! Your message has been sent.');
form.reset();
} catch (error) {
setStatus('error');
setMessage(error.message || 'Something went wrong. Please try again.');
}
}
const field = { padding: '10px 12px', border: '1px solid #ccc', borderRadius: 8, font: 'inherit' };
return (
<form onSubmit={handleSubmit} style={{ display: 'grid', gap: 8, width: '100%' }}>
<label htmlFor="cf-name">Name</label>
<input id="cf-name" name="name" type="text" required style={field} />
<label htmlFor="cf-email">Email</label>
<input id="cf-email" name="email" type="email" required style={field} />
<label htmlFor="cf-message">Message</label>
<textarea id="cf-message" name="message" rows={5} required style={field} />
<input type="checkbox" name="botcheck" style={{ display: 'none' }} tabIndex={-1} autoComplete="off" />
<button type="submit" disabled={status === 'sending'} style={{ ...field, cursor: 'pointer' }}>
{status === 'sending' ? 'Sending…' : buttonLabel}
</button>
{message && <p role={status === 'error' ? 'alert' : 'status'}>{message}</p>}
</form>
);
}
addPropertyControls(ContactForm, {
accessKey: { type: ControlType.String, title: 'Access key', defaultValue: 'YOUR_ACCESS_KEY_HERE' },
buttonLabel: { type: ControlType.String, title: 'Button', defaultValue: 'Send message' },
});- Save, then drag ContactForm from the Assets panel onto the page. Paste your key into the Access key property if you did not put it in the code.
Framer requires code components to be React 18 compatible; the component above uses only useState.
Fields and options#
Every input with a name is emailed to you. Keep the visitor's address in a field called email so you can answer with Reply. Add hidden fields for extras, such as subject to set the email subject or from_name for the sender name; see the field reference.
Test it#
Publish the site or open the preview, submit the form and check your inbox. If nothing arrives, look at the message under the form: "Access key is required." means the key is empty, and "Invalid or inactive access key." means it is mistyped. Every message is listed in Troubleshooting.
Next steps#
- Add a CAPTCHA if the form attracts spam: hCaptcha or Cloudflare Turnstile.
- Lock the key to your Framer domain: Restrict to your domain.
- More on submitting with JavaScript: AJAX form with JavaScript.