Creating a custom form element
This tutorial shows you how to create a custom form element for the TYPO3 Form Framework. We'll create a "Gender Select" element as an example.
Table of Contents
Prerequisites
Before you start, make sure you have:
- Basic knowledge of YAML configuration
- A sitepackage where you can add configuration files
Step 1: Create the configuration files
Create a form set directory in your extension. The sub-directory name is
arbitrary — we use CustomElement here.
File location
Create the following structure in your extension:
EXT:my_extension/
Configuration/
Form/
CustomElement/
config.yaml
Configuration structure
Here's the complete configuration for our Gender Select element:
prototypes:
standard:
formElementsDefinition:
GenderSelect:
implementationClassName: TYPO3\CMS\Form\Domain\Model\FormElements\GenericFormElement
renderingOptions:
templateName: 'RadioButton'
properties:
options:
f: 'Female'
m: 'Male'
d: 'Diverse'
formEditor:
label: 'Gender Select'
group: select
groupSorting: 9000
iconIdentifier: form-single-select
editors:
100:
identifier: header
templateName: Inspector-FormElementHeaderEditor
200:
identifier: label
templateName: Inspector-TextEditor
# Labels are retrieved from the default language file "EXT:form/Resources/Private/Language/Database.xlf"
# The most keys follow the pattern: formEditor.elements.FormElement.editor.[identifier].[key]
# In this example: "formEditor.elements.FormElement.editor.label.label"
label: formEditor.elements.FormElement.editor.label.label
propertyPath: label
230:
identifier: elementDescription
templateName: Inspector-TextEditor
label: formEditor.elements.FormElement.editor.elementDescription.label
propertyPath: properties.elementDescription
700:
identifier: gridColumnViewPortConfiguration
templateName: Inspector-GridColumnViewPortConfigurationEditor
label: formEditor.elements.FormElement.editor.gridColumnViewPortConfiguration.label
configurationOptions:
viewPorts:
10:
viewPortIdentifier: xs
label: formEditor.elements.FormElement.editor.gridColumnViewPortConfiguration.xs.label
20:
viewPortIdentifier: sm
label: formEditor.elements.FormElement.editor.gridColumnViewPortConfiguration.sm.label
30:
viewPortIdentifier: md
label: formEditor.elements.FormElement.editor.gridColumnViewPortConfiguration.md.label
40:
viewPortIdentifier: lg
label: formEditor.elements.FormElement.editor.gridColumnViewPortConfiguration.lg.label
50:
viewPortIdentifier: xl
label: formEditor.elements.FormElement.editor.gridColumnViewPortConfiguration.xl.label
60:
viewPortIdentifier: xxl
label: formEditor.elements.FormElement.editor.gridColumnViewPortConfiguration.xxl.label
numbersOfColumnsToUse:
label: formEditor.elements.FormElement.editor.gridColumnViewPortConfiguration.numbersOfColumnsToUse.label
propertyPath: 'properties.gridColumnClassAutoConfiguration.viewPorts.{@viewPortIdentifier}.numbersOfColumnsToUse'
description: formEditor.elements.FormElement.editor.gridColumnViewPortConfiguration.numbersOfColumnsToUse.description
800:
identifier: requiredValidator
templateName: Inspector-RequiredValidatorEditor
label: formEditor.elements.FormElement.editor.requiredValidator.label
validatorIdentifier: NotEmpty
propertyPath: properties.fluidAdditionalAttributes.required
propertyValue: required
configurationOptions:
validationErrorMessage:
label: formEditor.elements.FormElement.editor.requiredValidator.validationErrorMessage.label
propertyPath: properties.validationErrorMessages
description: formEditor.elements.FormElement.editor.requiredValidator.validationErrorMessage.description
errorCodes:
10: 1221560910
20: 1221560718
30: 1347992400
40: 1347992453
9999:
identifier: removeButton
templateName: Inspector-RemoveElementEditor
Common inspector editors
Here are some commonly used inspector editors (Inspector) you can add to your form elements:
- Inspector-FormElementHeaderEditor (100)
- Shows the element header in the inspector panel
- Inspector-TextEditor (200-300)
- A simple text input field for properties like label and description
- Inspector-PropertyGridEditor (400)
- A grid editor for managing key-value pairs (like options)
- Inspector-GridColumnViewPortConfigurationEditor (700)
- Controls responsive behavior and column widths for different screen sizes
- Inspector-RequiredValidatorEditor (800)
- Adds a checkbox to make the field required
- Inspector-ValidationErrorMessageEditor (900)
- Allows customizing validation error messages
- Inspector-RemoveElementEditor (9999)
- Shows a button to remove the element from the form
Step 2: Register the configuration
No PHP or TypoScript registration is needed. TYPO3 discovers YAML files automatically from any extension that follows the directory convention.
Create a form set with a single config.:
EXT:my_extension/
Configuration/
Form/
CustomElement/
config.yaml ← set metadata + all configuration
Set the priority in config. to a value greater than 10
(the EXT:form core base set) so your configuration is merged on top:
name: my-vendor/custom-element
label: 'My Custom Form Element'
priority: 200
# Form element configuration goes here:
prototypes:
standard:
...
Step 3: Clear Caches
After adding the configuration, you must clear all TYPO3 caches.
Step 4: Using your custom element
Now you can use your custom element in the form editor:
- Open the Form Editor user interface (Forms > [Your Form] > Edit)
- Look for "Gender Select" in the form element browser.
- Add the element to the form.
- Configure the element using the inspector panel on the right.
- Save your form.
- Add a form content element to a page and select the form you just edited.
- Preview the page in the frontend.
The element will now be available in your forms and will render using the RadioButton template in the frontend.
Step 5: Customizing frontend output (optional)
If you want to use a custom template instead of reusing an existing one, follow these steps:
Create custom template
Create your own Fluid template:
EXT:
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
xmlns:formvh="http://typo3.org/ns/TYPO3/CMS/Form/ViewHelpers"
data-namespace-typo3-fluid="true">
<f:comment>
Custom form element template for a gender selection field.
</f:comment>
<formvh:renderRenderable renderable="{element}">
<f:comment>Render the field wrapper with optional fieldset and legend</f:comment>
<f:render partial="Field/Field"
arguments="{
element: element,
renderFieldset: '{true}',
doNotShowLabel: '{true}'
}"
contentAs="elementContent">
<f:form.validationResults for="{element.rootForm.identifier}.{element.identifier}">
<f:comment>Apply error class if validation errors exist</f:comment>
<f:variable name="errorClass">
{f:if(
condition: '{validationResults.errors}',
then: 'is-invalid'
)}
</f:variable>
<f:comment>Radio button group container</f:comment>
<div id="{element.uniqueIdentifier}"
class="gender-select {errorClass}"
role="radiogroup"
aria-label="{element.label}">
<f:comment>Loop through all available options (e.g., male, female, diverse)</f:comment>
<f:for each="{element.properties.options}" as="label" key="value" iteration="idIterator">
<f:comment>Set ARIA attributes for accessibility</f:comment>
<f:if condition="{element.properties.elementDescription}">
<f:variable name="aria" value="{describedby: '{element.uniqueIdentifier}-desc'}" />
</f:if>
<f:comment>Add error indication to the first radio button if validation fails</f:comment>
<f:if condition="{validationResults.errors} && {idIterator.isFirst}">
<f:variable name="aria" value="{
invalid: 'true',
describedby: '{element.uniqueIdentifier}-errors'
}" />
</f:if>
<f:comment>Individual radio button container</f:comment>
<div class="form-check mb-2">
<label class="form-check-wrapping-label"
for="{element.uniqueIdentifier}-{idIterator.index}">
<f:form.radio
property="{element.identifier}"
id="{element.uniqueIdentifier}-{idIterator.index}"
class="form-check-input"
value="{value}"
errorClass="is-invalid"
additionalAttributes="{formvh:translateElementProperty(
element: element,
property: 'fluidAdditionalAttributes'
)}"
aria="{aria}"
/>
<span class="{element.properties.labelTextClassAttribute}">
{formvh:translateElementProperty(
element: element,
property: '{0: \'options\', 1: value}'
)}
</span>
</label>
</div>
</f:for>
</div>
<f:comment>Display validation errors</f:comment>
<f:if condition="{validationResults.flattenedErrors}">
<span id="{element.uniqueIdentifier}-errors"
role="alert">
<f:for each="{validationResults.errors}" as="error">
<f:format.htmlspecialchars>
{formvh:translateElementError(element: element, error: error)}
</f:format.htmlspecialchars>
<br/>
</f:for>
</span>
</f:if>
</f:form.validationResults>
</f:render>
</formvh:renderRenderable>
</html>
Update configuration
Update your YAML configuration to use the custom partial template:
prototypes:
standard:
formElementsDefinition:
Form:
renderingOptions:
partialRootPaths:
1732785721: 'EXT:my_extension/Resources/Private/Partials/Form/'
GenderSelect:
renderingOptions:
templateName: 'GenderSelect'