Freeform Freeform for Craft

Templating

Form Queries

The Form query returns a Form object containing its metadata and field objects. From there you can either render the form using the pre-selected formatting template by calling form.render() or take control of it by iterating over its fields and using form.renderTag and form.renderClosingTag methods.

TIP

Freeform will automatically insert javascript in the footer of the page for features such as Spam Protection, Submit disable on click, and other special field types. If you prefer to have this load inside the <form></form> tags, you can adjust the Freeform Javascript Insertion Location setting.

User Guide:

Using headless website architecture with JavaScript-based front-end frameworks such as Vue.js, Next.js, React JS and more.

User Guide:

Quick troubleshooting the most commonly reported issues with your form's appearance, behavior, or submission of the form on the front end.

Basic Dark Example

Parameters

The Form query is always constructed the same way. The first assumed parameter should contain the form handle or ID, and the second parameter (optional) contains Template Overrides and other available settings, typically used for applying a class globally to specific types of inputs, etc.

TIP

Please see the Template Overrides documentation for detailed instructions.

A basic implementation of the Form query would look like this:

{{ freeform.form("myFormHandle").render() }}
1

Once Template Overrides have been implemented, your code might look something like this:

{{ freeform.form("myFormHandle", {
    attributes: {
        novalidate: true,
        class: "my-form-class",
    },
    buttons: {
        attributes: {
            submit: { class: "form-field-button blue" },
            back: { class: "form-field-button gray" },
        },
    },
    fields: {
        "@global": {
            attributes: {
                input: {
                    class: "form-field-input",
                },
                label: {
                    class: "form-field-label",
                },
            },
        },
        ":required": {
            attributes: {
                label: { "+class": "form-field-required" },
            },
        },
        ":errors": {
            attributes: {
                input: { "+class": "form-field-is-invalid" },
            },
        },
        "@dropdown": {
            attributes: {
                container: {
                    "data-select-container": true,
                },
                input: {
                    "+class": "form-field-select fullwidth",
                },
            },
        },
        "@checkboxes, @radios" : {
            attributes: {
                input: {
                    "+class": "form-field-options",
                },
            },
        },
        "myFieldHandle": {
            value: entry.id,
        },
    },
}).render() }}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

returnUrl

Overrides the return URL for the form. You can override the return URL manually with a hidden field or checkbox, etc named formReturnUrl, allowing for a more dynamic return URL dependent on the user's choice or action, as long as you hash the value:

<label>
    <input type="checkbox" name="formReturnUrl" value="{{ 'whatever/my-url'|hash }}" />
    <span>My Label</span>
</label>
1
2
3
4
<label>
    <input type="radio" name="formReturnUrl" value="{{ 'whatever/my-url/one'|hash }}" />
    <span>Option One</span>
</label>
<label>
    <input type="radio" name="formReturnUrl" value="{{ 'whatever/my-url/two'|hash }}" />
    <span>Option Two</span>
</label>
<label>
    <input type="radio" name="formReturnUrl" value="{{ 'whatever/my-url/three'|hash }}" />
    <span>Option Three</span>
</label>
1
2
3
4
5
6
7
8
9
10
11
12
<input type="hidden" name="formReturnUrl" value="{{ 'whatever/my-url'|hash }}" />
1
User Guide:

Options for handling the Success Return URL on forms.

status

Overrides the default status of the generated submission. Can be either a handle or ID of an existing Status record.


 






{{ form.render({
    status: "pending",
    attributes: {
        class: "my-class",
        novalidate: true,
    },
}) }}
1
2
3
4
5
6
7

 






{{ form.render({
    status: 1,
    attributes: {
        class: "my-class",
        novalidate: true,
    },
}) }}
1
2
3
4
5
6
7

formattingTemplate

Allows you to override/set a formatting template for a form at template level. When using file-based email notification templates, be sure to include the file extension:

formattingTemplate: 'template-name.twig'
1

disable Revised and renamed in 5.0+

Allows you to disable email notifications, API integrations and Element integrations for a form by passing an object of items you wish to disable, typically used for editing submissions, e.g.:

  • adminNotifications: true - disable Admin notifications
  • conditionalNotifications: true New in 5.0+ - disable Conditional notifications
  • userSelectNotifications: true Renamed in 5.0+ - disable User Select notifications
  • emailFieldNotifications: true Renamed in 5.0+ - disable Email Fields notifications
  • api: true - disable any CRM and Email Marketing integrations
  • elements: true Renamed in 5.0+ - disable any Element integrations

    WARNING

    This will not disable Element validation, e.g. required fields, username already in use, etc.

  • payments: true - disable Payment integrations
  • webhooks: true - disable any Webhooks
  • captchas: true New in 5.0+ - disable any Captchas
  • honeypots: true New in 5.0+ - disable the Freeform Honeypot
  • javascriptTest: true New in 5.0+ - disable the Freeform Javascript Test
  • submitButtons New in 5.0+ - disable the automatic insertion of Submit buttons

You can also just set this to true (disable: true) to disable all items (except submitButtons) at once.

disable: {
	adminNotifications: true,
	emailFieldNotifications: true,
	api: true,
    captchas: true
}
1
2
3
4
5
6

relations Pro

This is a powerful feature that allows you to relate the posted submission to any other Craft Element, as long as it contains a Freeform Submissions element field type. This allows you to quickly turn Freeform into a plugin replacement for Comments, Reviews, Ratings, and more!

You can specify as many element ID's to relate to as you wish. All you have to do is specify the Freeform Submissions Field handle and supply an array of element ID's or just a single element ID that you wish to relate submissions for.

relations: {
	myFreeformSubmissionsFieldHandle: [55, 106],
	myOtherFreeformSubmissionFieldHandle: 251
}
1
2
3
4

The above would map the posted submission to the Elements with ID's 55, 106 and 251. More specifically, adding the submission to the field with handle myFreeformSubmissionsFieldHandle for elements with ID's 55 and 106 and one to the field with handle myOtherFreeformSubmissionFieldHandle for element with ID 251.

fieldIdPrefix

Adds a prefix value on field outputs. Helpful if you have more than 1 form on the same template and are sharing fields.

fieldIdPrefix: 'myform-'
1

dynamicNotification

Allows using a dynamic template level notification for a more fine-grained control.

dynamicNotification: {
	recipients: ["admin@example.com", "support@example.com"],
	template: "my-notification-template.twig"
}
1
2
3
4
  • Hard code values or pass a value from another element such as an Entry.
  • For Database entry based templates, specify the handle for template.
  • For Twig file based templates, specify the full file name including .html for template.

WARNING

When using PHP Sessions for form context, this feature may not work properly if the page is cached with something like Varnish, etc. In order for it to work correctly, you would have to refresh the tokens similar to what is described in this guide.

postForwarding Pro

Allows you to override POST Forwarding settings (in form builder) for the form at template level.

postForwarding: {
	url: "https://somesite.com/stuff",
	triggerPhrase: "success"
}
1
2
3
4

submissionToken Pro

Provide the submission token from the submission object to switch the form into edit mode and allow editing of submissions on front end.

Template Overrides New in 5.0+

You can control the attributes, values and more at the template level for forms, fields and buttons. Each one of them is entirely optional. There are several namespaces:

  • attributes - whatever you specify here will be set as an attribute on the form.
  • buttons - controls the output of the Submit button(s).
  • fields - controls the output of all Fields.
  • captchas - add attributes to the main Captcha wrapper automatically inserted by Freeform when using reCAPTCHA or hCaptcha.

TIP

Please see the Template Overrides documentation for detailed instructions.

Custom Form Properties

You can pass custom properties and values into the form payload and then display or perform conditionals on them. For example, in your main template (where showFormTitle is just a custom property):

{{ form.render({ showFormTitle: true }) }}
1

Then in your formatting template, use:

{% if form.properties.showFormTitle %}
	{{ form.name }}
{% endif %}
1
2
3
User Guide:

Need to pass a custom property to a formatting template, e.g. toggle display of the form title, etc?

TIP

If displaying the exact same form more than once in a single template, some of the <form> tag attributes set on one form may carry over to other ones. To work around this, you can unset the attribute on the other forms (unless they have their own attributes set). For example, if one form has class: 'something', it may end up applying to other instances of the form, but you can add class: null to those others to work around it.

Usage in Templates

Render the form using its formatting template:

{{ freeform.form("myForm").render() }}
1

Automated Rendering of Forms

Render the form using its formatting template, but overriding some classes:

{{ freeform.form("myForm", {
    attributes: {
        novalidate: true,
        class: "my-form-class",
    },
    buttons: {
        attributes: {
            submit: { class: "form-field-button blue" },
            back: { class: "form-field-button gray" },
        },
    },
    fields: {
        "@global": {
            attributes: {
                input: {
                    class: "form-field-input",
                },
                label: {
                    class: "form-field-label",
                },
            },
        },
        ":required": {
            attributes: {
                label: { "+class": "form-field-required" },
            },
        },
        ":errors": {
            attributes: {
                input: { "+class": "form-field-is-invalid" },
            },
        },
        "@dropdown": {
            attributes: {
                container: {
                    "data-select-container": true,
                },
                input: {
                    "+class": "form-field-select fullwidth",
                },
            },
        },
        "@checkboxes, @radios" : {
            attributes: {
                input: {
                    "+class": "form-field-options",
                },
            },
        },
        "myFieldHandle": {
            value: entry.id,
        },
    },
}).render() }}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

Get the form object and manually iterate through fields:

Manually Iterate

Manually iterate through form fields inside your regular template:

{# Set the form and overrides #}
{% set form = freeform.form("myFormHandle", {
    attributes: {
        row: { class: "freeform-row" },
        success: { class: "freeform-form-success" },
        errors: { class: "freeform-form-errors" },
    },
    buttons: {
        attributes: {
            container: { class: "freeform-button-container" },
            column: { class: "freeform-button-column" },
            buttonWrapper: { class: "freeform-button-wrapper" },
            submit: { class: "freeform-button-submit" },
            back: { class: "freeform-button-back" },
            save: { class: "freeform-button-save" },
        },
    },
    fields: {
        "@global": {
            attributes: {
                container: { class: "freeform-column" },
                input: {
                    novalidate: true,
                    class: "freeform-input",
                },
                label: { class: "freeform-label" },
                instructions: { class: "freeform-instructions" },
                error: { class: "freeform-errors" },
            },
        },
        ":required": {
            attributes: {
                label: { "+class": "freeform-required" },
            },
        },
        ":errors": {
            attributes: {
                input: { "+class": "is-invalid has-validation" },
            },
        },
        "@group": {
            attributes: {
                label: { "+class": "group-label" },
            },
        },
        "@signature": {
            attributes: {
                input: { "-class": "freeform-input" },
            },
        }
    },
}) %}

{# Render the opening form tag #}
{{ form.renderTag() }}

{# Success and error message handling for non-AJAX forms #}
{% if not form.settings.ajax %}
    {% if form.submittedSuccessfully %}
        <div{{ form.attributes.success }}>
            <p>{{ form.settings.successMessage | t('freeform') }}</p>
        </div>
    {% endif %}
    {% if form.hasErrors %}
        <div{{ form.attributes.errors }}>
            <p>{{ form.settings.errorMessage | t('freeform') }}</p>

            {% if form.errors|length %}
                <ul>
                    {% for error in form.errors %}
                        <li>{{ error }}</li>
                    {% endfor %}
                </ul>
            {% endif %}
        </div>
    {% endif %}
{% endif %}

{# Render page tabs if multi-page #}
{% if form.pages|length > 1 %}
    <ul class="freeform-pages">
        {% for page in form.pages %}
            <li {% if form.currentPage.index == page.index %}class="active"{% endif %}>
                {% if form.currentPage.index == page.index %}
                    <b>{{ page.label }}</b>
                {% else %}
                    {{ page.label }}
                {% endif %}
            </li>
        {% endfor %}
    </ul>
{% endif %}

{# Build form rows and fields #}
{% macro renderRows(form, rows) %}
    {% for row in rows %}
        {% set width = (12 / (row|length)) %}
        <div{{ form.attributes.row }}>
            {% for field in row %}
                {% do field.setParameters({
                    attributes: {
                        container: { class: [
                            "freeform-column-" ~ width,
                            "freeform-fieldtype-" ~ field.type,
                        ]},
                    },
                }) %}
                {% if field.type == "group" %}
                    <div class="freeform-group">
                        <label{{ field.attributes.label }}>
                            {{ field.label }}
                        </label>
                        <div>
                            {{ _self.renderRows(form, field.layout) }}
                        </div>
                    </div>
                {% else %}
                    {{ field.render }}
                {% endif %}
            {% endfor %}
        </div>
    {% endfor %}
{% endmacro %}

{# Display form field rows and columns #}
{{ _self.renderRows(form, form.rows) }}

{# Render the closing form tag #}
{{ form.renderClosingTag }}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129

Manual Rendering of Forms

Form formatting can also be extremely manual, if that is something you prefer. Here's an example of different levels of manual you can use:

{% set form = freeform.form("myForm") %}

{{ form.renderTag({
    returnUrl: "contact/success"},
    disable: ["submitButtons"]
}) }}

    {% if form.hasErrors %}
        <div class="freeform-form-has-errors">
            {{ "There was an error submitting this form"|t }}

            {% if form.errors|length %}
                <ul>
                    {% for error in form.errors %}
                        <li>{{ error }}</li>
                    {% endfor %}
                </ul>
            {% endif %}
        </div>
    {% endif %}

    {% set firstName = form.get("firstName") %}
    {% set company = form.get("company") %}
    {% set lastName = form.get("lastName") %}
    {% set myDropdown = form.get("myDropdown") %}

    <label>{{ firstName.label }}</label>
    <input name="{{ firstName.handle }}" value="{{ firstName.value }}" />
    {{ firstName.renderErrors() }}

    <label>{{ lastName.label }}</label>
    <input name="{{ lastName.handle }}" value="{{ lastName.value }}" />
    {{ lastName.renderErrors() }}

    {{ company.renderLabel() }}
    {{ company.renderInput() }}
    {{ company.renderErrors() }}

    <label>Email Address</label>
    <input name="email" />
    {{ form.get("email").renderErrors() }}

    <label>Phone</label>
    <input name="phone" />
    {% if form.get("phone").hasErrors %}
        This field is required!
    {% endif %}

    <label>My Dropdown Field</label>
    <select name="{{ myDropdown.handle }}">
    {% for option in myDropdown.options %}
        <option value="{{ option.value }}" {{ option.value in field.value ? "selected" }}>{{ option.label }}</option>
    {% endfor %}
    </select>

    {# The submit buttons will no longer be automatically inserted by Freeform with `disable: ["submitButtons"]` #}

    {# We need to set the `page` variable from the `form.pages.current` in order for this to work #}
    {% set page = form.pages.current %}

    {# Complete buttons can be rendered with `page.buttons.renderSubmit`, etc, but for maximum control, you can just get the properties attributes only #}
    <button {{ page.buttons.submitRenderProps({ 'data-optional': 'attributes here' }) }}>
        Submit
    </button>
    {% if form.pages.current.buttons.back %}
        <button {{ page.buttons.backRenderProps }}>
            Back
        </button>
    {% endif %}
    {% if form.pages.current.buttons.save %}
        <button {{ page.buttons.saveRenderProps }}>
            Save
        </button>
    {% endif %}

{{ form.renderClosingTag }}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76

Manual Multi-page Forms

You can manually format multi-page forms as well. The key is to wrap page context with form.currentPage.index conditionals. Here's an example of different levels of manual you can use:

{% set form = freeform.form("myFormHandle") %}

{{ form.renderTag({
    disable: ["submitButtons"]
}) }}

    {# Display any general errors upon submit #}
    <div class="form-heading-errors">
        {% if form.hasErrors %}
            <div class="freeform-form-has-errors">
                {{ "There was an error submitting this form"|t }}

                {% if form.errors|length %}
                    <ul>
                        {% for error in form.errors %}
                            <li>{{ error }}</li>
                        {% endfor %}
                    </ul>
                {% endif %}
            </div>
        {% endif %}
    </div>

    {# Set up your needed form page fields #}
    {% set firstName = form.get("firstName") %}
    {% set lastName = form.get("lastName") %}
    {% set company = form.get("company") %}
    {% set email = form.get("email") %}
    {% set phone = form.get("phone") %}
    {% set state = form.get("state") %}

    {# Show page tabs for visual reference only - these cannot be linked #}
    <ul class="nav nav-tabs">
        <li class="nav-item">
            <span class="nav-link{% if form.currentPage.index == 0 %} active font-weight-bold{% else %} disabled{% endif %}">Page 1</span>
        </li>
        <li class="nav-item">
            <span class="nav-link{% if form.currentPage.index == 1 %} active font-weight-bold{% else %} disabled{% endif %}">Page 2</span>
        </li>
    </ul>

    {# Show page 1 contents only #}
    {% if form.currentPage.index == 0 %}

        <div class="form-field">
            {# Very manual #}
            <label>{{ firstName.label }}</label>
            <input name="firstName" value="{{ firstName.value }}" />
            {{ firstName.renderErrors() }}
        </div>

        <div class="form-field">
            <label>{{ lastName.label }}</label>
            <input name="lastName" value="{{ lastName.value }}" />
            {{ lastName.renderErrors() }}
        </div>

        <div class="form-field">
            {# Somewhat manual #}
            {{ company.renderLabel() }}
            {{ company.renderInput() }}
            {{ company.renderErrors() }}
        </div>

        {# The submit buttons will no longer be automatically inserted by Freeform with `disable: ["submitButtons"]` #}

        {# We need to set the `page` variable from the `form.pages.current` in order for this to work #}
        {% set page = form.pages.current %}

        {# Complete buttons can be rendered with `page.buttons.renderSubmit`, etc, but for maximum control, you can just get the properties attributes only #}
        <button {{ page.buttons.submitRenderProps({ 'data-optional': 'attributes here' }) }}>
            Submit
        </button>
        {% if form.pages.current.buttons.back %}
            <button {{ page.buttons.backRenderProps }}>
                Back
            </button>
        {% endif %}
        {% if form.pages.current.buttons.save %}
            <button {{ page.buttons.saveRenderProps }}>
                Save
            </button>
        {% endif %}

    {# Show page 2 contents only #}
    {% elseif form.currentPage.index == 1 %}

        <div class="form-field">
            {# Completely manual #}
            <label>Email Address</label>
            <input name="email" value="{{ email.value }}" />
            {{ form.get("email").renderErrors() }}
        </div>

        <div class="form-field">
            {# Manual errors #}
            <label>Phone</label>
            <input name="phone" />
            {% if form.get("phone").hasErrors %}
                This field is required!
            {% endif %}
        </div>

        <div class="form-field">
            {# Manual multi-option field #}
            <label>State</label>
            <select name="state">
                {# You may also manually hardcode each option as well, as long as these options exist inside the form builder #}
                {% for option in state.options %}
                    <option value="{{ option.value }}" {{ option.value in field.value ? "selected" }}>{{ option.label }}</option>
                {% endfor %}
            </select>
        </div>

        {# The submit buttons will no longer be automatically inserted by Freeform with `disable: ["submitButtons"]` #}

        {# We need to set the `page` variable from the `form.pages.current` in order for this to work #}
        {% set page = form.pages.current %}

        {# Complete buttons can be rendered with `page.buttons.renderSubmit`, etc, but for maximum control, you can just get the properties attributes only #}
        <button {{ page.buttons.submitRenderProps({ 'data-optional': 'attributes here' }) }}>
            Submit
        </button>
        {% if form.pages.current.buttons.back %}
            <button {{ page.buttons.backRenderProps }}>
                Back
            </button>
        {% endif %}
        {% if form.pages.current.buttons.save %}
            <button {{ page.buttons.saveRenderProps }}>
                Save
            </button>
        {% endif %}

    {% endif %}

{{ form.renderClosingTag }}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137

Iterate over Fields instead of Rows

You can also iterate over fields directly with layout.fields instead of Row objects:

{% set form = freeform.form("myForm") %}

{% for field in form.layout.fields %}
  <div>{{ field.label }}</div>
{% endfor %}
1
2
3
4
5

Disabling Notifications and more

Rendering the form that allows editing a specific submission and disables all of the email notifications, api integrations, payments and element integrations:

{{ freeform.form("myForm").render({
    submissionToken: mySubmissionTokenVariable,
    disable: {
        api: true,
        elements: true,
        adminNotifications: true,
        emailFieldNotifications: true,
    },
}) }}
1
2
3
4
5
6
7
8
9

Relating Submissions to other Elements

You can feed Element ID(s) however you like, but the below example shows how you might pass off a loaded Entry ID to the Freeform form so that the submission will be related to it.

If you have a Craft Entry with a slug of submitted-contacts that contains a Freeform Submissions Field with a handle of contactFormSubmissions, and you would like to attach each submission to the Craft Entry field, you could do something like the following:






 
 
 


{# Fetch the entry however you wish #}
{% set entry = craft.entries.slug("submitted-contacts").one %}

{# Bind the entry relationship to the form #}
{{ freeform.form("contactForm").render({
	relations: {
		contactFormSubmissions: entry.id,
	},
}) }}
1
2
3
4
5
6
7
8
9