You are viewing beta documentation for Formie 4.x. View the latest stable version (3.x) →
Theming

Theme Config

Using theme config is the recommended way to customise Formie's rendered HTML.

It gives you control over the HTML tags and attributes used to build a form, including pages, rows, fields, buttons, and more. That makes it useful for changing classes, adding attributes, adjusting wrappers, or changing the tag used for a particular part of the output.

For this reason, it's well-suited to being used to output opinionated class attributes used by utility CSS frameworks like Tailwind (opens new window), or other frameworks like Bootstrap (opens new window).

For complete framework presets and larger examples, see Formie theme configs (opens new window).

Read Theming Overview first if you want to compare theme config with template overrides and custom rendering.

Overview

To gain a better understanding of what theme config is for, let's walk through the HTML output of a form. Take for example the default structure below (without every attribute shown):

<form class="formie-form" data-formie-form>
    <div class="formie-pages">
        <div class="formie-page">
            <div class="formie-page-container">
                <div class="formie-row">
                    <div class="formie-field">
                        <div class="formie-field-container">
                            <label class="formie-label">My Example Field</label>
                            <div class="formie-instructions">Some instructions</div>

                            <div class="formie-field-control">
                                <input type="text">
                            </div>
                        </div>

                       <div class="formie-field-errors">
                           <div class="formie-field-error">
                        </div>
                    </div>

Here, we have outlined the full structure of a form, containing a single field which would be rendered by craft.formie.renderForm(). While there are many HTML elements, each serve a purpose with styling, semantic structure, accessibility and general organisation.

Let's say you want to add a my-form ID attribute to the <form> element. You could use Custom Rendering or Template Overrides, but that is a heavier approach and gives you more template maintenance to keep up with.

Instead, we can use theme config to manipulate the attributes for this form.

{{ craft.formie.renderForm('contactForm', {
    themeConfig: {
        form: {
            attributes: {
                id: 'my-form',
            },
        },
    },
}) }}

{# Rendering #}
<form id="my-form" class="formie-form" data-formie-form>
    <div class="formie-pages">
        <div class="formie-page">
            ...

Here is a more involved example:

  • Remove all of Formie's classes on the <form> element and add id="my-form"
  • Change one wrapper to a <fieldset>
  • Add padding to the field wrapper
  • Add a red border to the <input> element for all fields (Tailwind)
{{ craft.formie.renderForm('contactForm', {
    themeConfig: {
        form: {
            resetClass: true,
            attributes: {
                id: 'my-form',
            },
        },

        pages: {
            tag: 'fieldset',
        },

        field: {
            attributes: {
                class: 'p-4 w-full mb-4',
            },
        },

        fieldInput: {
            attributes: {
                class: 'border border-red-500',
            },
        },
    },
}) }}

{# Rendering #}
<form id="my-form">
    <fieldset class="formie-pages">
        <div class="formie-page">
            <div class="formie-page-container">
                <div class="formie-row">
                    <div class="formie-field p-4 w-full mb-4">
                        <div class="formie-field-container">
                            <label class="formie-label">My Example Field</label>
                            <div class="formie-instructions">Some instructions</div>

                            <div class="formie-field-control">
                                <input type="text" class="border border-red-500">
                            </div>
                        </div>

                       <div class="formie-field-errors">
                           <div class="formie-field-error">
                        </div>
                    </div>

Detailed Reference

See Theme Tag Reference for the complete lookup, including types and field-specific options.

Targeting a Field Type

You can also target a field type directly. This lets you apply the same tag config only to one type of field instead of all fields.

{{ craft.formie.renderForm('contactForm', {
    themeConfig: {
        singleLineText: {
            field: {
                attributes: {
                    class: 'just-for-text',
                },
            },
        },
    },
}) }}

These keys use the field class' camelCase name. Available field type keys include:

  • address
  • agree
  • calculations
  • categories
  • checkboxes
  • dateTime
  • dropdown
  • emailAddress
  • entries
  • fileUpload
  • forms
  • group
  • heading
  • hiddenField
  • html
  • multiLineText
  • name
  • number
  • password
  • payment
  • phoneNumber
  • products
  • radioButtons
  • recipients
  • repeater
  • section
  • signature
  • singleLineText
  • submissions
  • summary
  • surveyLikert
  • surveyRank
  • surveyRating
  • table
  • tags
  • users
  • variants

Some fields also expose their inner sub-fields as their own targetable keys.

Available sub-field keys include:

  • address1
  • address2
  • address3
  • addressAutoComplete
  • addressCity
  • addressCountry
  • addressState
  • addressZip
  • dateDate
  • dateTime
  • dateAmPmDropdown
  • dateAmPmNumber
  • dateDayDropdown
  • dateDayNumber
  • dateDropdown
  • dateHourDropdown
  • dateHourNumber
  • dateMinuteDropdown
  • dateMinuteNumber
  • dateMonthDropdown
  • dateMonthNumber
  • dateNumber
  • dateSecondDropdown
  • dateSecondNumber
  • dateYearDropdown
  • dateYearNumber
  • nameFirst
  • nameLast
  • nameMiddle
  • namePrefix

That lets you target one part of the field without affecting the rest.

{{ craft.formie.renderForm('contactForm', {
    themeConfig: {
        address1: {
            fieldInput: {
                attributes: {
                    class: 'street-line',
                },
            },
        },

        addressCountry: {
            fieldInput: {
                attributes: {
                    class: 'country-select',
                },
            },
        },
    },
}) }}

The same pattern applies to other fields with inner sub-fields, such as Name and some Date/Time input modes.

Examples

Below are some common examples of what theme config can do.

Appending Attributes

We can append attributes to an element, ensuring that existing ones are kept in-place.

{{ craft.formie.renderForm('contactForm', {
    themeConfig: {
        field: {
            attributes: {
                class: 'appended-class',
                'data-custom': 'my-field',
            },
        },
    },
}) }}

{# Rendering #}
<div class="formie-field appended-class" data-formie-field-type="single-line-text" data-custom="my-field">
    ...

Resetting Classes

If you do not want to retain Formie's default classes on a tag, use resetClass. Top-level resetClasses can reset multiple slots.

{{ craft.formie.renderForm('contactForm', {
    themeConfig: {
        field: {
            resetClass: true,
            attributes: {
                class: 'my-field',
            },
        },
    },
}) }}

{# Rendering #}
<div class="my-field">
    ...

Removing Tags

If you do not want to render a tag at all, you can return a falsey value.

{{ craft.formie.renderForm('contactForm', {
    themeConfig: {
        fieldInstructions: false,
        fieldErrors: null,
    },
}) }}

{# Rendering #}
<div class="formie-field">
    <label class="formie-label">My Example Field</label>
    <div class="formie-field-content">
        <div class="formie-field-control">
            <input type="text">
        </div>
    </div>
</div>

Conditional Values

Theme config supports structured conditional values using if, then, and else.

For example, if you want to add a class only when a Recipients field is hidden:

{{ craft.formie.renderForm('contactForm', {
    themeConfig: {
        recipients: {
            field: {
                attributes: {
                    class: [
                        'hidden-field',
                        {
                            if: 'field.isHidden',
                            then: 'is-hidden',
                        },
                    ],
                },
            },
        },
    },
}) }}

The hidden-field class is always added, while is-hidden is only added when field.isHidden is truthy.

You can also use more explicit comparisons:

{
    if: {
        context: 'page.buttonsPosition',
        equals: 'left',
    },
    then: 'buttons-left',
    else: 'buttons-other',
}

Conditions can read supported properties of form, field, page, currentPage, row, and submission. Use the if / then / else syntax shown above; Twig expressions and method calls are not supported.

Prepend and Append

Use prepend and append for icons, short labels and other small decorative additions. Text is HTML-escaped by default:

'fieldLabel' => [
    'prepend' => ['tag' => 'span', 'text' => '<Required>'],
],

In PHP or Twig render options, use html when you need markup rather than text. Never put visitor-supplied content in html. Config passed from a browser cannot include raw HTML, tag changes or event-handler attributes. For larger markup changes, use Template Overrides.

Attribute Authority

Theme config preserves the attributes Formie needs for submission, JavaScript behaviour and accessibility. PHP developers needing to override those attributes can use EVENT_MODIFY_SLOT_TAG; see Theme and Rendering Architecture for the event order.

Use false or null to remove optional attributes. CSS custom property names in cssVars must begin with --.

Ajax / Client State Classes

Some UI states change in the browser without a full server re-render — multipage tab changes, hidden pages, loading buttons, and validation errors on Ajax forms. Twig conditionals in themeConfig will not re-evaluate for those updates.

Formie handles this with state class keys. Define them at the top level of themeConfig (not inside a slot such as pageTabLink). Formie embeds the resolved classes on data-formie-theme-classes, and the browser package toggles them as state changes. The Browser Theme State reference lists the available keys and default classes.

KeyApplied toWhen
pageHidden[data-formie-page]Page is not active (pageInactive alias)
tabCurrent[data-formie-tab]Tab is the current page
tabComplete[data-formie-tab]Tab is before the current page
tabError[data-formie-tab]Tab page has field errors
tabLinkCurrent[data-formie-tab-link]Tab link is the current page (pageTabLinkActive alias)
tabLinkInactive[data-formie-tab-link]Tab link is not the current page (pageTabLinkInactive alias)
loadingSubmit buttonsSubmit/save in progress
fieldLayoutError[data-formie-field]Field has errors
fieldControlErrorInputs inside the fieldField control has errors

Use pageTabLink for shared tab link classes. Use tabLinkCurrent / tabLinkInactive for state-specific classes — especially with Tailwind, where parent-state utilities cannot be injected through the link slot alone.

'themeConfig' => [
    'pageTabLink' => [
        'attributes' => [
            'class' => ['py-4', 'px-1', 'border-b-2', 'font-medium', 'text-sm'],
        ],
    ],
    'tabLinkInactive' => [
        'attributes' => [
            'class' => ['border-transparent', 'text-gray-500'],
        ],
    ],
    'tabLinkCurrent' => [
        'attributes' => [
            'class' => ['border-indigo-500', 'text-indigo-600'],
        ],
    ],
    'pageHidden' => [
        'attributes' => [
            'class' => ['hidden'],
        ],
    ],
],

Tailwind JIT only emits utilities it can see at build time. Safelist classes used in PHP themeConfig, or prefer Twig renderForm() theme config when possible. You can also style stable .formie-* hooks with @apply instead of injecting utilities.

Config Definitions

There are 3 methods for how you define a theme config, which are shown below in order of priority (with the first being the lowest priority).

  1. Plugin configuration settings with PHP.
  2. Twig rendering using craft.formie.renderForm().
  3. Event definitions in a PHP module.

Although each method is a different way to define a theme config, it all ends up being treated the same - just a different way of registering this configuration. Your project could even use a combination of all methods!

Render-Time (Twig)

Used with craft.formie.renderForm() through Render Options, this is the most direct way to provide theme config in a template.

{{ craft.formie.renderForm('contactForm', {
    themeConfig: {
        form: {
            attributes: {
                class: 'border border-red-500',
            },
        },
        field: {
            attributes: {
                class: 'border border-blue-500',
            },
        },
        fieldInput: {
            tag: 'div',
            resetClass: true,
            attributes: {
                class: 'border border-blue-500',
            },
        },
    },
}) }}

Events (PHP)

You can also use Form Events to define theme config in PHP. This gives you more control than Twig, but it also means writing module or plugin code.

For form-level tags:

use verbb\formie\elements\Form;
use verbb\formie\events\ModifyFormSlotTagEvent;
use yii\base\Event;

Event::on(Form::class, Form::EVENT_MODIFY_SLOT_TAG, function(ModifyFormSlotTagEvent $event) {
    $form = $event->form;
    $tag = $event->tag;
    $key = $event->key;
    $context = $event->context;

    // For the field `<form>` element, change the tag and add attributes
    if ($event->key === 'form') {
        $event->tag->tag = 'div';
        $event->tag->attributes['class'][] = 'p-4 w-full mb-4';
    }
});

For field-level tags:

use verbb\formie\base\Field;
use verbb\formie\fields\SingleLineText;
use verbb\formie\events\ModifyFieldSlotTagEvent;
use yii\base\Event;

// Provide config for all fields
Event::on(Field::class, Field::EVENT_MODIFY_SLOT_TAG, function(ModifyFieldSlotTagEvent $event) {
    $field = $event->field;
    $tag = $event->tag;
    $key = $event->key;
    $context = $event->context;

    // For the main field element, replace the class attribute entirely
    if ($event->key === 'field') {
        $event->tag->attributes['class'] = 'p-4 w-full mb-4';
    }

    // For the field instructions element, don't render the element
    if ($event->key === 'fieldInstructions') {
        $event->tag = null;
    }
});

// Single-line-text field-specific config
Event::on(SingleLineText::class, SingleLineText::EVENT_MODIFY_SLOT_TAG, function(ModifyFieldSlotTagEvent $event) {
    $field = $event->field;
    $tag = $event->tag;
    $key = $event->key;
    $context = $event->context;

    // For the field `<input>` element, change the tag and add attributes
    if ($event->key === 'fieldInput') {
        $event->tag->tag = 'span';
        $event->tag->attributes['class'][] = 'p-4 w-full mb-4';
    }
});

Plugin Configuration (PHP)

You can also define theme config globally through the plugin configuration file. Because this has the lowest priority, render-time config and events can still override it.

<?php

return [
    'themeConfig' => [
        // For the field `<form>` element, change the tag and add attributes
        'form' => [
            'resetClass' => true,
            'tag' => 'div',
            'attributes' => [
                'class' => 'p-4 w-full mb-4',
            ],
        ],

        // For the main field element
        'field' => [
            'attributes' => [
                'class' => 'p-4 w-full mb-4',
            ],
        ],

        // Specifically for a single-line-text field, we want the label uppercased
        'singleLineText' => [
            'fieldLabel' => [
                'attributes' => [
                    'class' => 'uppercase',
                ],
            ],
        ],
    ],
];

Combining with Template Overrides

Even if you want to use Template Overrides, you can still keep theme config in play. For example, the default Twig template for a Single-Line Text field is very small:

{{ fieldtag('fieldInput', {
    value: value ?? false,
}) }}

Well, that's certainly short and sweet! All the HTML for this field is actually defined at the class level, so there's very little HTML we need to write to output the <input> HTML element. This allows us to provide you with the ability to easily override our default config.

You have two choices here; 1. Add your own attributes to the fieldInput tag, or 2. Write your own HTML.

Retain Theme Config

It might be a good idea to have your custom changes still allow theme config to be used. You can do this by extending the theme config used by the fieldtag() Twig function.

The fieldtag() Twig function and the {% fieldtag %} Twig tag are almost identical to Craft's own tag() and {% tag %} functionality.

{{ fieldtag('fieldInput', {
    value: value ?? false,
    class: 'my-custom-class',
    'data-custom-field': true,
}) }}

{# or #}
{% fieldtag 'fieldInput' with {
    value: value ?? false,
    class: 'my-custom-class',
    'data-custom-field': true,
} %}
    Some content
{% endfieldtag %}

BYO Twig

Of course, there's nothing stopping you from writing your own HTML and Twig, you'll just lose the ability to use theme config for this field. However, it may very well be easier if you have complex logic for this field, want full control over output, or just find it easier to manage.

{# Just HTML #}
<input type="text" value="{{ value ?? false }}" class="my-custom-class" data-custom-field>

{# Using `tag()` #}
{{ tag('input', {
    type: 'text',
    value: value ?? false,
    class: 'my-custom-class',
    'data-custom-field': true,
}) }}