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 file
First, create a YAML configuration file for your custom form element. This file defines how the element behaves in both form editor and frontend.
File location
Create the following file in your extension (also create the directories if they do not yet exist):
EXT:
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
The YAML configuration must be registered in two places to work in both the form editor (backend) and the frontend.
Backend registration (Form Editor)
Register your YAML configuration file in your extension's
ext_:
<?php
declare(strict_types=1);
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
defined('TYPO3') or die();
ExtensionManagementUtility::addTypoScriptSetup('
module.tx_form {
settings {
yamlConfigurations {
1732785702 = EXT:your_extension/Configuration/Form/CustomFormSetup.yaml
}
}
}
');
Important
The numeric key (1732785702 in this example) should be unique across all
configurations. You can use a timestamp to ensure uniqueness. This number
determines the loading order - higher numbers are loaded later and can override
earlier settings.
Frontend registration (TypoScript)
To render the custom form element in the frontend, you must also register the
YAML configuration via TypoScript. Add the following to your site's TypoScript
setup (e.g., in EXT:):
plugin.tx_form {
settings {
yamlConfigurations {
1732785702 = EXT:my_extension/Configuration/Form/CustomFormSetup.yaml
}
}
}
Note
Please make sure your TypoScript template is included in the site configuration.
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'