Getting Started (Headless)LiteProImproved in 5.16+
Freeform's recommended headless path for React, Next.js, and Vue is:
- Use a Freeform plugin build that includes the headless REST API
- Enable headless in Freeform config
- Install the official npm packages
- Render forms with
<Freeform />or build your own UI withuseFreeform()
GraphQL and older AJAX demos remain available, but new projects should start here.
Clone the Freeform Headless React Demo, Vue Demo, or Nuxt Demo to try the packages against your Craft site (REST, GraphQL tab, Save & Continue, table, signature, calculation, payments, and more).
Requirements
| Piece | Version / notes |
|---|---|
| Craft CMS | 4.17+ or 5.9+ |
| Freeform plugin | 5.16+ |
| npm packages | Official @solspace/freeform-* packages (npm org) |
| React | 18 or 19 (for @solspace/freeform-react) |
| Vue | 3.4+ (for @solspace/freeform-vue) |
| Node | Modern LTS with npm / pnpm / yarn |
Quick setup
Enable headless in Freeform (Craft project)
Headless is off by default. Configure it in your Craft CMS project (the site where Freeform is installed) — not in your React / Vue / Next.js app repo.
Edit or create:
your-craft-project/config/freeform.php
That file lives next to Craft's other config files (general.php, routes.php, etc.), for example:
~/Sites/my-craft-site/config/freeform.php
Merge a headless section into that file (keep any existing Freeform settings):
<?php
return [
'headless' => [
// Master switch — endpoints return 404 when false
'enabled' => true,
// CORS origins for browser apps that call Craft directly
'allowedOrigins' => [
'http://localhost:3000',
'https://app.example.com',
],
// Per-form exposure (keyed by form handle from the Freeform CP)
'forms' => [
'contact' => [
'exposeManifest' => true,
'allowSubmit' => true,
],
],
],
];
If your frontend proxies /freeform/* to Craft (recommended for Next.js and SPAs), CSRF cookies stay same-origin and you can keep allowedOrigins tight.
See the full REST API reference for profiles, CSRF, and submit details.
Install the npm packages
- React / Next.js
- Vue 3
npm install @solspace/freeform-core \
@solspace/freeform-react \
@solspace/freeform-extensions \
@solspace/freeform-theme-default
npm install @solspace/freeform-core \
@solspace/freeform-vue \
@solspace/freeform-extensions \
@solspace/freeform-theme-default
| Package | Purpose |
|---|---|
@solspace/freeform-core | Manifest client, form state, conditionals, submit |
@solspace/freeform-react | React <Freeform /> and useFreeform() hook |
@solspace/freeform-vue | Vue <Freeform /> and useFreeform() composable |
@solspace/freeform-extensions | Captchas, datetime, file drag & drop, calculation, table, signature, Stripe, Square, PayPal, and Mollie payments |
@solspace/freeform-theme-default | Default light/dark theme CSS (React & Vue) |
@solspace/freeform-theme-tailwind | Official Tailwind starter theme (class maps) |
@solspace/freeform-theme-bootstrap | Official Bootstrap 5 starter theme (class maps) |
Render a form
- React
- Vue
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="https://cms.example.com"
extensions={recommendedExtensions}
onSuccess={(response) => {
console.log('Submitted', response);
}}
/>
);
}
<script setup lang="ts">
import { Freeform } from '@solspace/freeform-vue';
import { recommendedExtensions } from '@solspace/freeform-extensions';
import '@solspace/freeform-theme-default/styles.css';
const baseUrl = 'https://cms.example.com';
function onSuccess(response: unknown) {
console.log('Submitted', response);
}
</script>
<template>
<Freeform
handle="contact"
:base-url="baseUrl"
:extensions="recommendedExtensions"
:on-success="onSuccess"
/>
</template>
handle— Freeform form handle (must be enabled underheadless.forms)baseUrl— Craft site origin, or""/ same origin when you proxy/freeformextensions— register captchas and advanced fields when the form needs them
Choose your guide
| Guide | When to use it |
|---|---|
| React JS | Vite, CRA, Remix, or any React SPA |
| Next.js | App Router / Pages Router + proxy |
| Vue.js | Vite or any Vue 3 app |
| Nuxt | Nuxt 3 / 4 + client-only Freeform + proxy |
| REST API | Endpoints, CSRF, CORS, profiles |
| GraphQL | Headless adapters (freeformHeadlessManifest / freeformHeadlessSubmit) or legacy form GraphQL |
| React demo | Cloneable Vite + React app |
| Vue demo | Cloneable Vite + Vue app |
| Nuxt demo | Cloneable Nuxt 3 app |
What's included
- Manifest load + CSRF + submit (JSON and multipart)
- Multi-page forms and conditionals (client UX)
- Captchas via
@solspace/freeform-extensions - File upload and File Drag & Drop
- Save & Continue Later (draft token + key; see React JS / Vue.js)
- Calculation fields
- Table fields (row limits, required columns, file cells)
- Signature fields (canvas pad + clear)
- Stripe payments (Payment Element; see React JS → Stripe / Vue.js → Payments)
- Square payments (Web Payments SDK; see React JS → Square / Vue.js → Payments)
- PayPal payments (Buttons; see React JS → PayPal / Vue.js → Payments)
- Mollie payments (hosted checkout redirect; see React JS → Mollie / Vue.js → Payments)
- Default theme with light / dark / system color schemes
- Official Tailwind starter theme (
@solspace/freeform-theme-tailwind) - Official Bootstrap 5 starter theme (
@solspace/freeform-theme-bootstrap) - Headless GraphQL adapters (
freeformHeadlessManifest/freeformHeadlessSubmit)
Security checklist
- Keep headless disabled until you intentionally enable forms.
- Set explicit
headless.allowedOriginsfor cross-origin apps. - Enable a captcha on public forms (do not rely on honeypot alone for API callers).
- Leave
allowRawHtmloff unless HTML / rich-text field content is trusted CMS content. - Do not treat client-side conditional hiding as a server access-control boundary yet.