---
title: "Building and rendering forms"
manual: "Form"
version: "main"
permalink: "https://docs.typo3.org/permalink/typo3/cms-form:apireference-frontendrendering@main"
source: "D/FrontendRendering/Index.rst"
rendered: "2026-09-19T16:47:20+00:00"
---

# Building and rendering forms {#apireference-frontendrendering}

This chapter explains how EXT:form renders forms in the frontend and how
developers can build forms programmatically or customize the rendering
pipeline.

For the complete PHP API of every class mentioned here, see
[EXT:form API on api.typo3.org](https://api.typo3.org/main/namespaces/typo3-cms-form.html).

-   [Template resolution (FluidFormRenderer)](https://docs.typo3.org/permalink/typo3/cms-form:template-resolution-fluidformrenderer@main)
-   [The render ViewHelper](https://docs.typo3.org/permalink/typo3/cms-form:the-render-viewhelper@main)
-   [Building forms programmatically](https://docs.typo3.org/permalink/typo3/cms-form:building-forms-programmatically@main)
-   [Working with finishers](https://docs.typo3.org/permalink/typo3/cms-form:working-with-finishers@main)
-   [Runtime manipulation](https://docs.typo3.org/permalink/typo3/cms-form:runtime-manipulation@main)

## Template resolution (FluidFormRenderer) {#apireference-frontendrendering-fluidformrenderer}

The `FluidFormRenderer` resolves
Fluid templates, layouts and partials through rendering options defined in the
prototype configuration. All options are read from
`\TYPO3\CMS\Form\Domain\Model\FormDefinition::getRenderingOptions()`.

### templateRootPaths {#apireference-frontendrendering-fluidformrenderer-options-templaterootpaths}

Defines one or more paths to Fluid **templates**.
Paths are searched in reverse order (bottom to top); the first match wins.

Only the root form element (type `Form`) must be a **template** file.
All child elements are resolved as **partials**.

**EXT:my_sitepackage/Configuration/Form/CustomPrototype.yaml**

```yaml
prototypes:
  standard:
    formElementsDefinition:
      Form:
        renderingOptions:
          templateRootPaths:
            10: 'EXT:form/Resources/Private/Frontend/Templates/'
            20: 'EXT:my_sitepackage/Resources/Private/Forms/Frontend/Templates/'

```

With the default type `Form` the renderer expects a file named
`Form.html` inside the first matching path.

### layoutRootPaths {#apireference-frontendrendering-fluidformrenderer-options-layoutrootpaths}

Defines one or more paths to Fluid **layouts**, searched in reverse order.

**EXT:my_sitepackage/Configuration/Form/CustomPrototype.yaml**

```yaml
prototypes:
  standard:
    formElementsDefinition:
      Form:
        renderingOptions:
          layoutRootPaths:
            10: 'EXT:form/Resources/Private/Frontend/Layouts/'
            20: 'EXT:my_sitepackage/Resources/Private/Forms/Frontend/Layouts/'

```

### partialRootPaths {#apireference-frontendrendering-fluidformrenderer-options-partialrootpaths}

Defines one or more paths to Fluid **partials**, searched in reverse order.

Within these paths the renderer looks for a file named after the
form element type (e.g. `Text.html` for a `Text` element).
Use [templateName](https://docs.typo3.org/permalink/typo3/cms-form:apireference-frontendrendering-fluidformrenderer-options-templatename@main)
to override this convention.

**EXT:my_sitepackage/Configuration/Form/CustomPrototype.yaml**

```yaml
prototypes:
  standard:
    formElementsDefinition:
      Form:
        renderingOptions:
          partialRootPaths:
            10: 'EXT:form/Resources/Private/Frontend/Partials/'
            20: 'EXT:my_sitepackage/Resources/Private/Forms/Frontend/Partials/'

```

### templateName {#apireference-frontendrendering-fluidformrenderer-options-templatename}

By default the element type is used as the partial file name
(e.g. type `Text` → `Text.html`).
Set `templateName` to use a different file instead:

**EXT:my_sitepackage/Configuration/Form/CustomPrototype.yaml**

```yaml
prototypes:
  standard:
    formElementsDefinition:
      Foo:
        renderingOptions:
          templateName: 'Text'

```

The element of type `Foo` now renders using `Text.html`.

## The render ViewHelper {#apireference-frontendrendering-renderviewhelper}

Use `<formvh:render>` in a Fluid template to render a form.
The ViewHelper accepts the following arguments:

### persistenceIdentifier {#apireference-frontendrendering-renderviewhelper-persistenceidentifier}

Path to a YAML form definition. This is the most common way to render a
form:

**EXT:my_sitepackage/Resources/Private/Templates/ContactPage.html**

```html
<formvh:render
    persistenceIdentifier="EXT:my_sitepackage/Resources/Private/Forms/Contact.yaml"
/>

```

### overrideConfiguration {#apireference-frontendrendering-renderviewhelper-overrideconfiguration}

A configuration array that is merged **on top** of the loaded form
definition (or passed directly to the factory when no
`persistenceIdentifier` is given).
This allows adjusting a form per usage without duplicating the YAML file.

### factoryClass {#apireference-frontendrendering-renderviewhelper-factoryclass}

A fully qualified class name implementing
`FormFactoryInterface`.
Defaults to `ArrayFormFactory`.
Set a custom factory to [build forms programmatically](https://docs.typo3.org/permalink/typo3/cms-form:apireference-frontendrendering-programmatically@main).

**EXT:my_sitepackage/Resources/Private/Templates/ContactPage.html**

```html
<formvh:render
    factoryClass="Vendor\MySitePackage\Domain\Factory\CustomFormFactory"
/>

```

### prototypeName {#apireference-frontendrendering-renderviewhelper-prototypename}

Name of the prototype the factory should use (e.g. `standard`).
If omitted the framework looks for the prototype name inside the form
definition; if none is found, `standard` is used.

## Building forms programmatically {#apireference-frontendrendering-programmatically}

Instead of writing YAML, you can create a form entirely in PHP by
implementing a custom `FormFactory`.

1.  Create a FormFactory

    Extend `AbstractFormFactory` and implement `build()`.
    Use `FormDefinition::createPage()` to add pages,
    `Page::createElement()` to add elements, and
    `FormDefinition::createFinisher()` to attach finishers.

    **EXT:my_sitepackage/Classes/Domain/Factory/CustomFormFactory.php**

    ```php
    <?php

    declare(strict_types=1);

    namespace Vendor\MySitePackage\Domain\Factory;

    use Psr\Http\Message\ServerRequestInterface;
    use TYPO3\CMS\Core\Utility\GeneralUtility;
    use TYPO3\CMS\Form\Domain\Configuration\ConfigurationService;
    use TYPO3\CMS\Form\Domain\Factory\AbstractFormFactory;
    use TYPO3\CMS\Form\Domain\Model\FormDefinition;
    use TYPO3\CMS\Form\Domain\Model\FormElements\AbstractFormElement;

    final class CustomFormFactory extends AbstractFormFactory
    {
        public function build(
            array $configuration,
            ?string $prototypeName = null,
            ?ServerRequestInterface $request = null,
        ): FormDefinition {
            $prototypeName ??= 'standard';
            $configurationService = GeneralUtility::makeInstance(
                ConfigurationService::class,
            );
            $prototypeConfiguration = $configurationService->getPrototypeConfiguration(
                $prototypeName,
            );

            $form = GeneralUtility::makeInstance(
                FormDefinition::class,
                'ContactForm',
                $prototypeConfiguration,
            );
            $form->setRenderingOption('controllerAction', 'index');

            // Page 1 – personal data
            $page1 = $form->createPage('page1');

            /** @var AbstractFormElement $name */
            $name = $page1->createElement('name', 'Text');
            $name->setLabel('Name');
            $name->createValidator('NotEmpty');

            /** @var AbstractFormElement $email */
            $email = $page1->createElement('email', 'Text');
            $email->setLabel('Email');

            // Page 2 – message
            $page2 = $form->createPage('page2');

            /** @var AbstractFormElement $message */
            $message = $page2->createElement('message', 'Textarea');
            $message->setLabel('Message');
            $message->createValidator('StringLength', ['minimum' => 5, 'maximum' => 500]);

            // Radio buttons
            /** @var AbstractFormElement $subject */
            $subject = $page2->createElement('subject', 'RadioButton');
            $subject->setProperty('options', [
                'general' => 'General inquiry',
                'support' => 'Support request',
            ]);
            $subject->setLabel('Subject');

            // Finisher – send email
            $form->createFinisher('EmailToSender', [
                'subject' => 'Contact form submission',
                'recipients' => [
                    'info@example.com' => 'My Company',
                ],
                'senderAddress' => 'noreply@example.com',
            ]);

            $this->triggerFormBuildingFinished($form);

            return $form;
        }
    }

    ```
1.  Render the form

    Reference your factory in a Fluid template:

    **EXT:my_sitepackage/Resources/Private/Templates/ContactPage.html**

    ```html
    <formvh:render
        factoryClass="Vendor\MySitePackage\Domain\Factory\CustomFormFactory"
    />

    ```

### Key classes and their responsibilities {#apireference-frontendrendering-programmatically-key-concepts}

The following table lists the most important classes you work with when
building or manipulating forms programmatically. Use your IDE's
autocompletion or the
[API documentation](https://api.typo3.org/main/namespaces/typo3-cms-form.html)
for the full method reference.

| Class | Purpose |
| --- | --- |
| `FormDefinition` | The complete form model. Create pages (`createPage()`), attach finishers (`createFinisher()`), look up elements (`getElementByIdentifier()`), and bind to a request (`bind()`). |
| `FormRuntime` | A *bound* form instance (created by `FormDefinition::bind()`). Provides access to the current page, submitted values (`getElementValue()`), and the request/response objects. This is the object available inside finishers and event listeners. |
| `Page` | One page of a multi-step form. Add elements with `createElement()`, reorder them with `moveElementBefore()` / `moveElementAfter()`. |
| `Section` | A grouping element inside a page. Same API as `Page` for managing child elements. |
| `AbstractFormElement` | Base class of all concrete elements. Most element types use `GenericFormElement`; specialized subclasses include, e.g. `FileUpload`. Set properties (`setProperty()`), add validators (`createValidator()`), define default values (`setDefaultValue()`). |
| `ConfigurationService` | Reads the merged prototype configuration. Call `getPrototypeConfiguration('standard')` to obtain the full array for a prototype. |

### Initializing elements at runtime {#apireference-frontendrendering-programmatically-initializeformelement}

Override `initializeFormElement()` in a custom form element class to
populate data (e.g. from a database) when the element is added to the form.
At that point the prototype defaults have already been applied; properties
from the YAML definition are applied **afterwards**.

> [!TIP]
> If you only need to initialize an element without writing a full custom
> class, listen to the `BeforeRenderableIsAddedToFormEvent` PSR-14
> event instead. See [PSR-14 Events](https://docs.typo3.org/permalink/typo3/cms-form:apireference-events@main).

## Working with finishers {#apireference-frontendrendering-finishers}

Custom finishers extend `AbstractFinisher`
and place their logic in `executeInternal()`. The base class provides:

-   **`parseOption(string $optionName)`**

    Resolves a finisher option, applying form-element variable replacements
    and TypoScript-style option overrides. Always prefer this over direct
    array access.

The `FinisherContext` passed to
`execute()` gives access to:

-   **`getFormRuntime()`**

    The `FormRuntime` for the current
    submission.

-   **`getFormValues()`**

    All submitted values (after validation and property mapping).

-   **`getFinisherVariableProvider()`**

    A key/value store to **share data between finishers** within the same
    request. The returned
    `FinisherVariableProvider` offers:

    -   **`add(string $finisherIdentifier, string $key, mixed $value)`**

        Store a value under a finisher-specific namespace.

    -   **`get(string $finisherIdentifier, string $key, mixed $default = null)`**

        Retrieve a previously stored value (returns `$default` if not set).

    -   **`exists(string $finisherIdentifier, string $key)`**

        Check whether a value has been stored.

    -   **`remove(string $finisherIdentifier, string $key)`**

        Remove a stored value.

-   **`cancel()`**

    Stops execution of any remaining finishers.

> [!NOTE]
> **See also**
>
> [Accessing finisher options](https://docs.typo3.org/permalink/typo3/cms-form:concepts-finishers-customfinisherimplementations-accessingoptions@main) |
> [Sharing data between finishers](https://docs.typo3.org/permalink/typo3/cms-form:concepts-finishers-customfinisherimplementations-finishercontext-sharedatabetweenfinishers@main)

## Runtime manipulation {#apireference-frontendrendering-runtimemanipulation}

EXT:form dispatches PSR-14 events at every important step of the rendering
lifecycle. Use them to modify the form, redirect page flow, or adjust
submitted values – without subclassing framework internals.

> [!NOTE]
> **See also**
>
> [PSR-14 events overview for EXT:form](https://docs.typo3.org/permalink/typo3/cms-form:apireference-events@main)
