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.

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:my_extension/Configuration/Form/CustomFormSetup.yaml

Configuration Structure 

Here's the complete configuration for our Gender Select element:

EXT:my_extension/Configuration/Form/CustomFormSetup.yaml
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
Copied!

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_localconf.php:

EXT:my_extension/ext_localconf.php
<?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
           }
       }
    }
');
Copied!

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:my_extension/Configuration/TypoScript/setup.typoscript):

EXT:my_extension/Configuration/TypoScript/setup.typoscript
plugin.tx_form {
    settings {
        yamlConfigurations {
            1732785702 = EXT:my_extension/Configuration/Form/CustomFormSetup.yaml
        }
    }
}
Copied!

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:

  1. Open the Form Editor user interface (Forms > [Your Form] > Edit)
  2. Look for "Gender Select" in the form element browser.
  3. Add the element to the form.
  4. Configure the element using the inspector panel on the right.
  5. Save your form.
  6. Add a form content element to a page and select the form you just edited.
  7. 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:my_extension/Resources/Private/Partials/Form/GenderSelect.fluid.html

EXT:my_extension/Resources/Private/Partials/Form/GenderSelect.fluid.html
<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>
Copied!

Update configuration 

Update your YAML configuration to use the custom partial template:

EXT:my_extension/Configuration/Form/CustomFormSetup.yaml
prototypes:
  standard:
    formElementsDefinition:
      Form:
         renderingOptions:
            partialRootPaths:
               1732785721: 'EXT:my_extension/Resources/Private/Partials/Form/'
      GenderSelect:
        renderingOptions:
          templateName: 'GenderSelect'
Copied!

Further reading