Customization
The embed widget is opinionated by default and customizable when you need it. Three escape hatches cover most real cases: theming via CSS variables, callbacks for custom behavior, and full headless mode when you want your own markup.
Theming with CSS variables
The widget exposes its visual surface as CSS custom properties on the host element. Override them in your stylesheet to match your brand.
[data-form-id] {
/* Colors */
--fs-color-primary: #6366f1;
--fs-color-primary-hover: #4f46e5;
--fs-color-text: #111827;
--fs-color-muted: #6b7280;
--fs-color-bg: #ffffff;
--fs-color-border: #e5e7eb;
--fs-color-error: #dc2626;
--fs-color-success: #059669;
/* Layout */
--fs-radius: 8px;
--fs-spacing: 16px;
--fs-input-padding: 10px 12px;
/* Typography */
--fs-font-family: inherit;
--fs-font-size: 14px;
--fs-line-height: 1.5;
}
The defaults work on light and dark backgrounds. The widget ships a data-theme="dark" token set as well, applied automatically when data-theme="dark" or data-theme="auto" resolves to dark.
Class hooks for finer-grained styling:
.fs-form { /* the <form> wrapper */ }
.fs-field { /* each field block */ }
.fs-label { /* field labels */ }
.fs-input,
.fs-textarea,
.fs-select { /* form controls */ }
.fs-button { /* submit button */ }
.fs-error { /* validation messages */ }
.fs-success { /* success state */ }
Class names are stable within a major version.
Custom CSS via the dashboard
If you don't have direct control over the host page's CSS (e.g. embedding into a CMS that strips style tags), paste custom CSS into the form's edit page → Embed → Custom CSS. It's loaded inside the widget's render scope, so it can't break the surrounding page.
There's a 16KB limit on saved custom CSS. For larger overrides, host the file yourself and reference it with data-custom-css="https://...".
Mounting in a specific element
By default the widget renders into the [data-form-id] element it found. To render into a different element, pass mountTo:
<aside id="newsletter-spot"></aside>
<script>
window.formspringConfig = {
formId: 'frm_01H...',
mountTo: '#newsletter-spot'
};
</script>
<script src="https://formspring.io/embed.js" defer></script>
mountTo accepts a CSS selector or a DOM node. If the selector matches multiple elements, the widget renders into the first one.
Lifecycle callbacks
Hook into submission lifecycle events:
window.formspringConfig = {
formId: 'frm_01H...',
// Called once the widget has rendered
onReady: (api) => {
console.log('form rendered', api);
},
// Called after the user clicks submit but before validation runs.
// Return false to cancel.
onBeforeSubmit: (data) => {
if (!data.email.endsWith('@example.com')) {
alert('Use your work email.');
return false;
}
},
// Called when the submission was accepted by Formspring.
onSuccess: (submission) => {
window.gtag?.('event', 'form_submit', { form_id: 'contact' });
},
// Called when the submission was rejected.
onError: (error) => {
console.warn('submission failed', error);
},
// Called every time a field's value changes (debounced 200ms)
onChange: (data) => {}
};
api (passed to onReady) exposes:
| Method | Notes |
|---|---|
api.reset() |
Clears the form |
api.submit() |
Submits programmatically |
api.setField(name, value) |
Sets a field value |
api.getValues() |
Returns the current values as an object |
api.destroy() |
Tears the widget down |
Programmatic field overrides
Override field labels, placeholders, help text, and required-ness without touching the dashboard:
window.formspringConfig = {
formId: 'frm_01H...',
fields: {
email: { label: 'Work email', placeholder: 'you@company.com' },
message: { placeholder: 'How can we help?', rows: 6 },
company: { hidden: true } // remove from rendering
}
};
Fields you don't list are rendered with their dashboard defaults. You cannot add fields here — only fields that exist on the form can be customized or hidden.
Headless mode
When you want your own markup but Formspring's validation, file-upload, captcha, and spam-protection logic, use headless mode:
<form id="contact" novalidate>
<input name="email" type="email" required>
<textarea name="message" required></textarea>
<button type="submit">Send</button>
</form>
<script src="https://formspring.io/embed.js" defer></script>
<script>
document.getElementById('contact').addEventListener('submit', async (e) => {
e.preventDefault();
const data = Object.fromEntries(new FormData(e.target));
try {
const result = await Formspring.submit({
formId: 'frm_01H...',
data
});
// Your success UI
console.log('ok', result.submissionId);
} catch (err) {
// Your error UI
console.warn(err.code, err.message);
}
});
</script>
The Formspring.submit({ formId, data }) helper handles the POST, applies the form's spam protection (honeypot, rate limit, captcha if configured), and returns a { submissionId, accepted } payload on success or throws an error with code, message, and fields on failure.
In headless mode you're responsible for:
- Form HTML and styling
- Loading captcha scripts and rendering challenges (the helper expects the token in
data['h-captcha-response']ordata['g-recaptcha-response']) - Rendering success and error states
You get the benefit of Formspring's spam-rejection logic and submission storage without giving up control of the markup.