Example Vite + React app that renders Solspace Freeform forms using the published @solspace/freeform-* packages from npm.
Official packages:
| Package | Role |
|---|---|
@solspace/freeform-core |
Manifest, state, submit |
@solspace/freeform-react |
<Freeform /> and useFreeform() |
@solspace/freeform-extensions |
Captchas, calculation, datetime, file drag & drop, table, signature |
@solspace/freeform-theme-default |
Default light / dark theme |
@solspace/freeform-theme-tailwind |
Official Tailwind starter (class maps, no CSS) |
@solspace/freeform-theme-bootstrap |
Official Bootstrap 5 starter (class maps, no CSS) |
Before running this app:
- A Craft CMS site with Freeform installed (version that includes the headless REST API)
- A Freeform form you want to display (you’ll use its handle, e.g.
contact) - Node.js 20+
This step is done in your Craft CMS project, not in this React demo repo.
Headless must be turned on in Freeform before this app can load a form.
-
Open your Craft site project (the CMS install — e.g. a DDEV project, not this Vite app).
-
Edit (or create) the Freeform config file at:
your-craft-project/config/freeform.phpTypical locations:
Setup Path Standard Craft config/freeform.phpat the Craft project rootDDEV / local same — e.g. ~/Sites/my-site/config/freeform.php -
Merge a
headlesssection into that file (keep any other Freeform settings you already have). See the Freeform Headless docs for the full options:
<?php
// your-craft-project/config/freeform.php
return [
'headless' => [
'enabled' => true,
'forms' => [
'contact' => [ // ← your Freeform form handle
'exposeManifest' => true,
'allowSubmit' => true,
],
],
],
];Tips:
- Replace
contactwith the form handle from the Freeform control panel (Forms → your form). - For public forms, enable a captcha in Freeform.
- Keep Craft running (e.g. DDEV at
https://site.ddev.site) — you will pointCRAFT_PROXY_TARGETat that URL in step 2.
git clone https://github.com/solspace/freeform-headless-react-demo.git
cd freeform-headless-react-demo
cp .env.example .envEdit .env:
# Craft site URL (Vite proxies /freeform and /actions here)
CRAFT_PROXY_TARGET=https://your-site.example
# Form handle from Freeform (same as in step 1)
VITE_FREEFORM_HANDLE=contact
# Optional — GraphQL tab
VITE_GRAPHQL_PATH=/actions/graphql/api
VITE_GRAPHQL_TOKEN=your-craft-graphql-token| Variable | Purpose |
|---|---|
CRAFT_PROXY_TARGET |
Your Craft / Freeform site URL |
VITE_FREEFORM_HANDLE |
Default form handle shown when the app starts |
VITE_GRAPHQL_PATH |
Craft GraphQL endpoint (default /actions/graphql/api) |
VITE_GRAPHQL_TOKEN |
Craft GraphQL schema token (needed for the GraphQL tab) |
pnpm install
pnpm devOpen http://localhost:3000.
How it connects: the React app calls /freeform/... on localhost. Vite proxies those requests to CRAFT_PROXY_TARGET, so CSRF cookies work on the same origin.
Use npm or yarn if you prefer. This demo’s package.json pins @solspace/freeform-* from the registry.
- Make sure the form is headless-enabled (step 1).
- In the app Form settings, enter the form handle → Load form.
Or setVITE_FREEFORM_HANDLEin.envand restartpnpm dev.
1. Pick an API (top tabs)
| Tab | What it uses |
|---|---|
| REST | /freeform headless endpoints (recommended default) |
| GraphQL | Craft GraphQL adapters via fetch={graphqlFetch} |
2. Pick a view (second row)
| Tab | What it shows |
|---|---|
<Freeform /> |
Full form with the default theme |
useFreeform() |
Headless hook — you own the markup |
| Manifest JSON | Raw manifest (REST or GraphQL depending on the API tab) |
GraphQL: create a Craft GraphQL schema with Freeform form read + submission create, enable your site, and paste a token into VITE_GRAPHQL_TOKEN. Pass the demo’s graphqlFetch helper as fetch:
import { Freeform } from "@solspace/freeform-react";
import { graphqlFetch } from "./graphqlFetch"; // see this repo
<Freeform
handle="contact"
baseUrl={window.location.origin}
fetch={graphqlFetch}
extensions={recommendedExtensions}
/>JSON submits use GraphQL; multipart file uploads still go through REST.
Save & Continue: if the form has Save enabled, click Save — the URL gets ?session-token=…&key=…. Refresh or share that link to resume. See React docs → Save & Continue Later.
Advanced fields: recommendedExtensions covers captchas, datetime, file drag & drop, calculation, table, and signature.
import { Freeform } from "@solspace/freeform-react";
import { recommendedExtensions } from "@solspace/freeform-extensions";
import "@solspace/freeform-theme-default/styles.css";
export function ContactForm() {
return (
<Freeform
handle="contact"
baseUrl={window.location.origin}
extensions={recommendedExtensions}
/>
);
}When /freeform is proxied (or served) on the same host as your frontend, keep baseUrl as your app’s origin.
| Command | Description |
|---|---|
pnpm dev |
Dev server |
pnpm build |
Production build |
pnpm preview |
Preview the production build |
Custom port: PORT=3001 pnpm dev
The same packages work in Next.js. Use a Client Component and rewrite /freeform to your Craft site (same idea as this Vite proxy). See Solspace Freeform → Headless docs.
- Solspace Freeform documentation → Headless
- Packages on npm
| Issue | Fix |
|---|---|
| Form won’t load / 404 | Wrong handle, or headless not enabled for that form (step 1). |
| CSRF / session errors | Keep using the Vite proxy; don’t call Craft from another origin without CORS + credentials. |
| CORS errors | Prefer the proxy, or add http://localhost:3000 to headless.allowedOrigins. |
| Captcha / file upload missing | Enable those integrations in Freeform; this demo already loads recommendedExtensions. |
| Save & Continue | Enable Save on the form’s Button Layout. After Save, this demo puts session-token + key in the URL — refresh to resume. |
This demo is MIT. The Freeform Craft plugin is licensed separately — see Solspace Freeform.