Freeform Freeform for Craft

Formatting Templates

Tailwind CSS 3 Improved in 5.0+

The following example assumes you're including necessary Tailwind CSS 3 components. Due to the nature of Tailwind, this likely won't be useable as-is for most customers, but will serve as a good starting point for creating your own. You can place the CSS and JS inside the formatting template or add to your site's CSS / JS files.

Preview

Tailwind CSS 3 Example

Video: Preview of Formatting Template Examples

Templates

/tailwind-3/
{# Pull in CSS and field rendering #}
<style>
{% include "freeform/_templates/formatting/tailwind-3/_main.css" %}
</style>
{% import "freeform/_templates/formatting/tailwind-3/_row.twig" as rowMacro %}

{# Render the opening form tag #}
{{ form.renderTag({
    attributes: {
        row: { class: "flex flex-wrap -mx-2 mb-4" },
        success: { class: "bg-green-100 border border-green-400 font-bold text-green-700 px-4 py-3 rounded relative mb-4" },
        errors: { class: "bg-red-100 border border-red-400 font-bold text-red-700 px-4 py-3 rounded relative mb-4" },
    },
    buttons: {
        attributes: {
            container: { class: "flex flex-wrap -mx-2 mb-4" },
            column: { class: "ml-2" },
            buttonWrapper: {},
            submit: { class: "bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mr-2" },
            back: { class: "bg-gray-400 hover:bg-gray-500 text-white font-bold py-2 px-4 rounded mr-2" },
            save: { class: "bg-gray-600 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded mr-2" },
        },
    },
    fields: {
        "@global": {
            attributes: {
                container: { class: "w-full px-2" },
                label: { class: "block text-slate-800 text-base font-medium mb-1" },
                input: {
                    novalidate: true,
                    class: [
                        "md:mb-0 mb-4",
                        "appearance-none block w-full",
                        "text-slate-800 rounded py-2 px-3 leading-tight",
                        "placeholder:font-light placeholder:text-slate-400",
                        "border border-slate-400 focus:outline-none focus:bg-white focus:border-blue-600",
                    ],
                },
                instructions: { class: "block text-slate-500 text-sm -mt-1.5 mb-1" },
                error: { class: "freeform-errors block w-full text-sm text-red-500 mt-1" },
            },
        },
        ":required": {
            attributes: {
                label: { "+class": "required" },
            },
        },
        ":errors": {
            attributes: {
                input: { "+class": "border-red-500" },
            },
        },
        "@checkbox, @checkboxes, @radios, @opinion-scale, @signature, @table": {
            attributes: {
                input: { "-class": "appearance-none block w-full" },
            },
        },
        "@signature": {
            attributes: {
                input: { "+class": "rounded py-1 px-2 mr-1 hover:bg-slate-400" },
            },
        },
        "@checkbox" : {
            attributes: {
                label: {
                    "-class": "block text-slate-800 text-base font-medium mb-1",
                    "+class": "ml-2 font-medium text-slate-700"
                },
            },
        },
        "@checkbox, @checkboxes, @radios": {
            attributes: {
                input: { "+class": "w-4 h-4 text-blue-600 bg-slate-100 border-slate-300 rounded" },
            },
        },
        "@stripe": {
            attributes: {
                input: { "-class": "rounded py-2 px-3 leading-tight placeholder:font-light placeholder:text-slate-400 border border-slate-400 focus:outline-none focus:bg-white focus:border-blue-600" },
            },
        },
    },
}) }}

{# Pull in JS overrides #}
<script>
{% include "freeform/_templates/formatting/tailwind-3/_main.js" %}
</script>

{# 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 class="mb-0">
                    {% 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="flex border-b-2 border-slate-300 mb-6">
        {% for page in form.pages %}
            <li class="mr-1">
                <span class="inline-block rounded-t py-2 px-5 {{ form.currentPage.index == page.index ? 'bg-slate-300 border-l border-t border-r border-slate-300 text-slate-800 font-semibold' : 'bg-white text-slate-500 font-normal' }}">{{ page.label }}</span>
            </li>
        {% endfor %}
    </ul>
{% endif %}

{# Display form field rows and columns #}
{{ rowMacro.render(form.rows, form) }}

{# 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
{% macro getFieldTemplate(type) -%}
    {% set fieldTemplatePath = "freeform/_templates/formatting/tailwind-3/fields/" %}
    {{- fieldTemplatePath ~ type ~ ".twig" -}}
{%- endmacro %}

{% macro render(rows, form) %}
    {% import _self as self %}

    {% for row in rows %}

        {% set columnCount = row|length %}

        <div{{ form.attributes.row }}>
            {% for field in row %}

                {% do field.setParameters({
                    attributes: {
                        container: {
                            class: [
                                columnCount in [2, 3, 4] ? "md:w-1/" ~ columnCount,
                                columnCount in [2, 3, 4] ? "lg:w-1/" ~ columnCount,
                                columnCount in [2, 3, 4] ? "xl:w-1/" ~ columnCount,
                                columnCount == 1 ? "md:w-full lg:w-full xl:w-full"
                            ],
                        },
                    }
                }) %}

                {% include [self.getFieldTemplate(field.type), self.getFieldTemplate("_default")] %}

            {% endfor %}
        </div>

    {% endfor %}

{% endmacro %}
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
/* LAYOUT */
html, body {
    font-family: sans-serif;
}
* {
    box-sizing: border-box;
}
.required::after {
    content: "*";
    color: #d00;
    margin-left: 5px;
}
button[type=submit].freeform-processing {
    display: inline-flex;
    flex-wrap: nowrap;
    align-items: center;
}
button[type=submit].freeform-processing:before {
    content: "";
    display: block;
    flex: 1 0 11px;
    width: 11px;
    height: 11px;
    margin-right: 10px;
    border-style: solid;
    border-width: 2px;
    border-color: transparent transparent #fff #fff;
    border-radius: 50%;
    animation: freeform-processing .5s linear infinite;
}
@keyframes freeform-processing {
    0% {
        transform: rotate(0);
    }
    100% {
        transform: rotate(1turn);
    }
}
.input-group-one-line > div {
    float: left;
    margin-right: 15px;
}
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
var form = document.querySelector('[data-id="{{ form.anchor }}"]');
if (form) {
    // Styling for AJAX responses
    form.addEventListener("freeform-ready", function (event) {
        var freeform = event.freeform;
        freeform.setOption("errorClassBanner", ["bg-red-100", "border", "border-red-400", "font-bold", "text-red-700", "px-4", "py-3", "rounded", "relative", "mb-4"]);
        freeform.setOption("errorClassList", ["errors", "text-red-500", "text-sm", "italic"]);
        freeform.setOption("errorClassField", ["border-red-500"]);
        freeform.setOption("successClassBanner", ["bg-green-100", "border", "border-green-500", "font-bold", "text-green-700", "px-4", "py-3", "rounded", "relative", "mb-4"]);
    })
    // Styling for Stripe Payments field
    form.addEventListener("freeform-stripe-appearance", function (event) {
        event.elementOptions.appearance = Object.assign(
            event.elementOptions.appearance,
            {
                variables: {
                    colorPrimary: "#0d6efd",
                    fontFamily: "-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\",\"Noto Color Emoji\"",
                    fontSizeBase: "16px",
                    spacingUnit: "0.2em",
                    tabSpacing: "10px",
                    gridColumnSpacing: "20px",
                    gridRowSpacing: "20px",
                    colorText: "#212529",
                    colorBackground: "#ffffff",
                    colorDanger: "rgb(239 68 68)",
                    borderRadius: "5px",
                },
                rules: {
                    '.Tab, .Input': {
                        border: '1px solid rgb(148 163 184)',
                        boxShadow: 'none',
                    },
                    '.Tab:focus, .Input:focus': {
                        border: '1px solid #0b5ed7',
                        boxShadow: 'none',
                        outline: '0',
                        transition: 'border-color .15s ease-in-out',
                    },
                    '.Label': {
                        fontSize: '16px',
                        fontWeight: '500',
                    },
                },
            }
        );
    });
}
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
fields/
/tailwind-3/fields/
{{ field.render }}
1
<div{{ field.attributes.container }}>

<div class="flex items-center">
    {{ field.renderInput }}
    {{ field.renderLabel }}
</div>

{{ field.renderInstructions }}
{{ field.renderErrors }}

</div>
1
2
3
4
5
6
7
8
9
10
11
{% do field.setParameters({
    attributes: {
        container: {
            class: [
                field.oneLine ? "input-group-one-line"
            ]
        }
    }
}) %}

<div{{ field.attributes.container }}>

{{ field.renderLabel }}
{% for index, option in field.options %}
    <div class="flex items-center mb-1">
        <input type="checkbox"
                name="{{ field.handle }}[]"
                value="{{ option.value }}"
                id="{{ field.idAttribute }}-{{ index }}"
                class="{{ field.attributes.input.get("class") }}{{ field.hasErrors ? ' border-red-500' }}"
                {{ option.value in field.value ? "checked" }}
        />
        <label for="{{ field.idAttribute }}-{{ index }}" class="ml-2 font-normal text-slate-600">
            {{ option.label|t('freeform')|raw }}
        </label>
    </div>
{% endfor %}
{{ field.renderInstructions }}
{{ field.renderErrors }}

</div>
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
{% do field.setParameters({
    attributes: {
        container: {
            class: [
                field.oneLine ? "input-group-one-line"
            ]
        }
    }
}) %}

<div{{ field.attributes.container }}>

{{ field.renderLabel }}
{% for index, option in field.options %}
    <div class="flex items-center mb-1">
        <input type="radio"
                name="{{ field.handle }}"
                value="{{ option.value }}"
                id="{{ field.idAttribute }}-{{ index }}"
                class="{{ field.attributes.input.get("class") }}{{ field.hasErrors ? ' border-red-500' }}"
                {{ option.value == field.value ? "checked" }}
        />
        <label for="{{ field.idAttribute }}-{{ index }}" class="ml-2 font-normal text-slate-600">
            {{ option.label|t('freeform')|raw }}
        </label>
    </div>
{% endfor %}
{{ field.renderInstructions }}
{{ field.renderErrors }}

</div>
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
{% import "freeform/_templates/formatting/tailwind-3/_row.twig" as rowMacro %}

<div{{ field.attributes.container }}>
    {{ field.renderLabel }}
    {{ field.renderInstructions }}

    {{ rowMacro.render(field.layout, form) }}
</div>
1
2
3
4
5
6
7
8
{% set tableColumnCount = field.tableLayout|length %}

{{ field.render({
    addButtonLabel: "Add +",
    removeButtonLabel: "x",
    tableAttributes: {
        table: { class: "table" },
        row: { class: "table-row" },
        column: { class: [
            tableColumnCount in [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] ? "w-1/" ~ tableColumnCount,
            tableColumnCount == 1 ? "w-full",
            "table-col pb-1"
        ] },
        label: { class: [
            "text-left font-medium tracking-wide text-slate-800 text-sm mb-2 pr-5"
        ] },
        input: { class: [ field.attributes.input.get("class"), "mr-5 w-11/12 block" ] },
        dropdown: { class: [ field.attributes.input.get("class"), "mr-5 w-11/12 block" ] },
        checkbox: { class: [ field.attributes.input.get("class"), "mr-5" ] },
        removeButton: { class: "bg-red-500 hover:bg-red-700 text-white font-normal py-1 px-3 rounded" },
        addButton: { class: "bg-green-500 hover:bg-green-700 text-white font-normal py-1 px-3 rounded mt-2" },
    },
}) }}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<div{{ field.attributes.container }}>

{{ field.renderLabel }}

<ul class="w-full text-sm font-medium border border-slate-400 rounded flex">
    {% for scale in field.scales %}
        <li class="w-full">
            <input type="radio" name="{{ field.handle }}"
                value="{{ scale.value }}"
                id="{{ field.idAttribute }}-{{ loop.index }}"
                {{ field.value == scale.value ? "checked" }}
                class="hidden peer"
             />
            <label for="{{ field.idAttribute }}-{{ loop.index }}"
                class="inline-flex text-center w-full py-2 px-3 text-slate-800 bg-white cursor-pointer peer-checked:bg-slate-200 hover:bg-slate-100
                    {% if loop.index == 1 %}
                        border-r border-slate-400 rounded-tl rounded-bl
                    {% elseif loop.index == field.scales|length %}
                        rounded-tr rounded-br
                    {% else %}
                        border-r border-slate-400
                    {% endif %}">
                {{ scale }}
            </label>
        </li>
    {% endfor %}
</ul>

{% if field.legends %}
    <ul class="w-full text-sm font-medium flex text-slate-500">
        {% for legend in field.legends %}
            <li class="w-full
                {% if loop.index == 1 %}
                    text-left
                {% elseif loop.index == field.legends|length %}
                    text-right
                {% else %}
                    text-center
                {% endif %}">
                {{ legend }}
            </li>
        {% endfor %}
    </ul>
{% endif %}

{{ field.renderInstructions }}
{{ field.renderErrors }}

</div>
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
<div{{ field.attributes.container }}>

{{ field.renderLabel }}
<div class="relative">
    {{ field.renderInput }}
    <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-slate-800">
        <svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"/></svg>
    </div>
</div>
{{ field.renderInstructions }}
{{ field.renderErrors }}

</div>
1
2
3
4
5
6
7
8
9
10
11
12
13

Tailwind CSS is typically installed via CLI, but for the purposes of this example template, we use a CDN. The following CDN link for Tailwind CSS 3 is for v3.3.3, which may no longer be the latest version. Please see official Tailwind CSS 3 documentation for latest versions and CDN links.

<!-- Latest compiled and minified CSS -->
<script src="https://cdn.tailwindcss.com/3.3.3"></script>
1
2

Live Demo

The demo below is a live demo site that shows most of what the Demo Templates include (some sections and data has been limited).