Formspree is a form backend: you write the form in HTML or JSX and post it to a Formspree endpoint that stores and routes submissions. Fillo renders the fields from one schema and applies the same rules to server validation, uploads, and responses.
At a glance
| Formspree | Fillo | |
|---|---|---|
| Where it renders | Your own HTML or JSX, posting to a Formspree endpoint. @formspree/react adds a useForm hook (React library) | Native fields rendered from a Fillo schema by @usefillo/react or @usefillo/dom, or a Fillo-hosted page |
| Who writes the fields | You write every input | Fillo renders them from the schema. You can swap any field kind for your own component |
| Form definition | Your markup | Fillo editor, or a typed defineForm() schema in your repo |
| Validation | Client checks you write, plus server-side errors returned by Formspree | One schema drives client and server validation |
| File uploads | Paid plans only, 25 MB per file, stored in Formspree against the plan's storage (file uploads) | 500 MB per file by default, configurable up to 5,000 MB, sent from the browser to your Google Drive, Box, S3, or R2 |
| Responses | Submissions dashboard with email notifications | Response grid with answers, app context, files, versions, and delivery state |
| Pricing | Monthly submissions per plan, 50 on Free (plans) | Free includes 200 completed responses a month. Everything starts at €35 or $39 a month for 5,000, with 25,000 and 100,000 tiers. Going over the allowance never stops a form or adds a charge |
| Integrations | Workflow plugins such as webhooks, Slack, Airtable, and Zapier (plugins) | Signed webhooks, a Management API, native Slack, Notion, Google Sheets, HubSpot, and Discord destinations, plus Zapier and n8n apps |
When Fillo fits
- The form still needs to be built, and you want the fields rendered from one schema.
- The same field IDs and rules should drive the UI, server checks, responses, and exports.
- A teammate should be able to edit the form after launch without a deploy.
- Respondents send files over 25 MB, or the files belong in your own storage.
When Formspree is enough
- The markup is finished and the only missing piece is a submission endpoint.
- Form state, layout, and client behavior must stay entirely in your own code.
Moving from Formspree
Fillo isn't a drop-in endpoint for existing markup, so a move means expressing the form as a Fillo schema. With Formspree you write the inputs and useForm() handles submit:
tsx
import { useForm } from "@formspree/react";
export function ContactForm() {
const [state, handleSubmit] = useForm("your-form-id");
return (
<form onSubmit={handleSubmit}>
<label htmlFor="email">Work email</label>
<input id="email" name="email" type="email" required />
<button disabled={state.submitting}>Send</button>
</form>
);
}With Fillo the fields come from the schema:
tsx
import { Fillo, createClient } from "@usefillo/react";
import "@usefillo/react/styles.css";
const fillo = createClient({ key: import.meta.env.VITE_FILLO_KEY });
export function ContactForm() {
return (
<Fillo.Form client={fillo} id="contact" title="Contact us">
<Fillo.Email id="email" label="Work email" required />
</Fillo.Form>
);
}- Export the Formspree submissions and files you need to keep.
- Recreate the fields as a schema, reusing your
nameattributes as field IDs. - Render it on the same route with the default, styled, or headless renderer.
- Map custom validation and conditional UI to Fillo rules and test every path.
- Reconnect notifications and integrations, then send a test response.
- Switch traffic and keep the old form ready to restore until responses look right.
Related
- Embed a Fillo form: Render the form with React, the DOM package, or a hosted page.
- Author forms in code: Keep a typed Fillo schema in your repo.
- Form builder API: How a full form contract differs from a submission endpoint.
- Plans and limits: Response, upload, and branding limits.