EXT:form 

If EXT:form is enabled in the TYPO3 instance, EXT:headless produces JSON form definitions instead of HTML. Forms designed in the form editor work out of the box; this page documents the headless-specific hooks that help frontend developers.

Configuration (YAML) 

All options live in the form's YAML configuration file.

i18n strings 

Add translated UI strings (button labels, help messages) directly in the form root. They land in the response's i18n section.

i18n:
  identifier: 'i18n'
  properties:
    someButtonLabel: 'Submit or Cancel'
    someHelpMessage: 'You need to fill out this form'
    requiredFields: 'These fields are required'
Copied!

Strings are translated through the standard TYPO3 XLF pipeline. Translation keys resolve against the form's original identifier (kept in renderingOptions._originalIdentifier), not the runtime identifier that headless suffixes with the content element uid — key your XLF entries on the identifier from the form YAML. A forced locale can be passed to FormTranslationService::translateElementValue() as its optional fourth parameter.

On key collision, i18n.properties entries win over the renderingOptions.submitButtonLabel shortcut — both end up in the response's i18n object, YAML properties last.

Form decorator 

Headless ships FormDefinitionDecorator as the default. Override per form via renderingOptions.formDecorator:

renderingOptions:
  formDecorator: Your\Vendor\Form\CustomDefinitionDecorator
Copied!

See Customising the JSON output (decorators) below for writing your own decorator.

Rich-text fields (TYPO3 v14.2+) 

TYPO3 v14.2 allows RTE content in form element labels and StaticText elements. To ship that HTML safely in the JSON definition, opt in per form to the shipped decorator:

renderingOptions:
  formDecorator: FriendsOfTYPO3\Headless\Form\Decorator\RichTextFormDefinitionDecorator
Copied!

HTML-carrying labels and static texts are then run through lib.parseFunc_RTE (resolving t3:// links), falling back to lib.parseFunc_links and finally to the plain HTML sanitizer when neither lib exists, and flagged with labelFormat/textFormat: html so the frontend knows to render them as markup. The decorator also processes api.actionAfterSuccess.message the same way and flags it with messageFormat: html.

Validator error codes 

Per validator, set errorMessage to a TYPO3 XLF error code — the default form translation files resolve it:

validators:
  - identifier: 'NotEmpty'
    errorMessage: 1221560910
Copied!

For RegularExpression validators, PHP and JS regex flavours differ. Provide a JS-flavoured fallback via FERegularExpression:

validators:
  - identifier: 'RegularExpression'
    options:
      regularExpression: '/^[a-z]+$/'
    FERegularExpression:
      expression: '^[a-z]+$'
      flags: 'i'
    errorMessage: 1221565130
Copied!

When the headless decorator sees FERegularExpression, it replaces options.regularExpression with that value as-is in the JSON response — the {expression, flags} pair maps directly to JavaScript's new RegExp(expression, flags).

Custom options (dynamic select/radio/checkbox) 

Implement FriendsOfTYPO3HeadlessFormCustomOptionsInterface and point the field at it:

- type: 'SingleSelectWithCountryList'
  identifier: 'country'
  label: 'Country'
  properties:
    customOptions: 'Your\Vendor\Form\CountryOptions'
Copied!

CountryOptions::get() is called per render and its return value replaces the field's options. Although the interface only declares get(): array, implementations are instantiated with four constructor arguments — ($field, $formFields, $identifier, $formRuntime): the field's definition array, all fields of the current page, the runtime form identifier and the FormRuntime — so options can depend on the surrounding form state.

If your custom form type isn't a standard one (so the frontend wouldn't know what to render), override the type sent to the frontend with FEOverrideType:

type: 'SingleSelectWithCountryList'
renderingOptions:
  FEOverrideType: 'Select'
Copied!

JSON redirect finisher 

The standard RedirectFinisher issues a real HTTP redirect. In headless mode that breaks the SPA flow. Use the JsonRedirect finisher instead — it puts the redirect target into api.actionAfterSuccess and lets the frontend decide what to do.

Since 5.0 the finisher is registered out of the box (form set friendsoftypo3/headless-form, EXT:headless/Configuration/Form/Headless/config.yaml) — use it directly in your form definition:

finishers:
  -
    identifier: JsonRedirect
    options:
      pageUid: '2'
      message: 'Thanks! You will be redirected shortly.'
Copied!

On success it emits { redirectUrl, statusCode, message } (message defaults to null, statusCode to 303 and is also an option). The redirectUrl is made relative only when its host already equals the site's frontendBase host; otherwise the absolute TYPO3-host URL is returned unchanged — it is not rewritten to frontendBase. It does not redirect by itself. Further options: additionalParameters (appended to the target URL) and sameSiteOnly — with it, a pageUid outside the current site (or an unresolvable one) falls back to the finisher's default target (pageUid: 1) instead of being used.

Submitting the form 

POST the form to the content element's link value — headless builds it with tx_form_formframework[action]=perform and tx_form_formframework[controller]=FormFrontend already appended. Nothing inside the form definition itself is a valid submit target.

Every field is submitted under its exact name, tx_form_formframework[<formId>][<identifier>]. Besides the visible fields, the JSON elements contain Hidden elements that must be posted back verbatim:

  • __state — the HMAC-protected form state; round-trip it unchanged.
  • __currentPage — the page index being submitted.
  • __trustedProperties — the extbase property-mapping token, generated for the exact set of listed field names: omitting any field (the honeypot included) fails property mapping.
  • __session — present only once the form is performing (after the first POST); echo it back on subsequent steps.

Honeypot 

When renderingOptions.honeypot.enable is true, an extra field with a session-random identifier appears in elements and its name is baked into __trustedProperties. Render it hidden from humans and submit it empty — filling or omitting it fails the submission. When using a custom honeypot element, its type must match renderingOptions.honeypot.formElementToUse (default Honeypot) for headless to expose it correctly in the JSON definition.

Customising the JSON output (decorators) 

EXT:headless provides three building blocks:

  • FriendsOfTYPO3HeadlessFormDecoratorFormDefinitionDecorator — default implementation.
  • FriendsOfTYPO3HeadlessFormDecoratorAbstractFormDefinitionDecorator — base class with hooks to override per-element or whole-form output.
  • FriendsOfTYPO3HeadlessFormDecoratorDefinitionDecoratorInterface — the contract a custom decorator implements.

Default output (FormDefinitionDecorator) — the decorator returns the bare definition; in the page response it sits under the content element's content.form key:

{
  "id": "ContactForm-1",
  "api": {
    "status": null,
    "errors": null,
    "actionAfterSuccess": null,
    "page": { "current": 0, "nextPage": null, "pages": 1 }
  },
  "i18n": { "submitButtonLabel": "Submit" },
  "elements": []
}
Copied!

elements is abbreviated here — in a real response it lists the current page's fields plus the hidden round-trip fields described in Submitting the form above.

Subclass AbstractFormDefinitionDecorator if you only need to tweak one element type or one form root field; implement DefinitionDecoratorInterface directly if you want full control.

Attach via renderingOptions.formDecorator as shown above.