Freeform Freeform for Craft

Formatting Templates

Bootstrap 5 with Floating Labels Improved in 5.0+

This example makes use of Bootstrap's Floating Labels feature. The following example assumes you're including necessary Bootstrap 5 JS and CSS. You can place the additional CSS and JS inside the formatting template or add to your site's CSS / JS files.

Preview

Bootstrap 5 Floating Labels Example

Video: Preview of Formatting Template Examples

Templates

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

{# Render the opening form tag #}
{{ form.renderTag({
    attributes: {
        row: { class: "row" },
        success: { class: "alert alert-success" },
        errors: { class: "alert alert-danger" },
        novalidate: true,
    },
    buttons: {
        attributes: {
            submit: { class: "btn btn-primary" },
            back: { class: "btn btn-secondary" },
            save: { class: "btn btn-primary" },
        },
    },
    fields: {
        "@global": {
            attributes: {
                container: { class: "mb-3 col-12" },
                input: {
                    class: "form-control"
                },
                label: { class: "mb-1" },
                instructions: { class: "form-text text-muted mt-1" },
                error: { class: "list-unstyled m-0 fst-italic text-danger" },
            },
        },
        ":required": {
            attributes: {
                label: { "+class": "required" },
            },
        },
        ":errors": {
            attributes: {
                input: { "+class": "is-invalid" },
            },
        },
        "@text, @textarea, @email, @website, @confirm, @regex, @phone, @datetime, @dropdown, @multiple-select, @number, @password, @file-dnd, @signature": {
            attributes: {
                container: { "+class": "form-floating" },
            },
        },
        "@group": {
            attributes: {
            label: { "+class": "group-label" },
            },
        },
        "@checkbox" : {
            attributes: {
                input: { "=class": "form-check-input checkbox" },
                label: { "+class": "form-check-label" },
            },
        },
        "@dropdown" : {
            attributes: {
                input: { "+class": "form-select" },
            },
        },
        "@file" : {
            attributes: {
             input: { "+class": "form-control-file" },
            },
        },
        "@signature": {
            attributes: {
                input: {
                    "-class": "form-control",
                    "+class": "btn btn-light"
                },
            },
        },
        "@stripe": {
            attributes: {
                input: {
                    "-class": "form-control",
                },
            },
        },
    },
}) }}

{# Pull in JS overrides #}
<script>
{% include "freeform/_templates/formatting/bootstrap-5-floating-labels/_main.js" %}
</script>

{# Success and error message handling for non-AJAX forms #}
{% if not form.settings.ajax %}
    {% if form.submittedSuccessfully %}
        <div{{ form.attributes.success|raw }}>
            <p>{{ form.settings.successMessage | t('freeform') }}</p>
        </div>
    {% endif %}
    {% if form.hasErrors %}
        <div{{ form.attributes.errors|raw }}>
            <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="nav nav-tabs mb-4">
        {% for page in form.pages %}
            <li class="nav-item">
                <span class="nav-link{{ form.currentPage.index == page.index ? ' fw-bold active' : ' disabled' }}">
                    {{ 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
127
128
129
130
131
132
{% macro getFieldTemplate(type) -%}
    {%- set fieldTemplatePath = "freeform/_templates/formatting/bootstrap-5-floating-labels/fields/" -%}
    {%- if type in ["text", "email", "website", "confirm", "regex", "phone", "datetime", "number", "password"] -%}
        {{- fieldTemplatePath ~ "floating.twig" -}}
    {%- else -%}
        {{- fieldTemplatePath ~ type ~ ".twig" -}}
    {%- endif -%}
{%- endmacro %}

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

    {% for row in rows %}

        {% set width = (12 / (row|length)) %}

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

                {% do field.setParameters({
                    attributes: {
                        container: {
                            class: [
                                "col-sm-" ~ width,
                                "freeform-fieldtype-" ~ field.type,
                            ],
                        },
                    }
                }) %}

                {% 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
37
38
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);
    }
}
label.required:after {
    content: "*";
    color: #d00;
    margin-left: 3px;
}
.alert p:last-of-type {
    margin-bottom: 0;
}
.mt-n1 {
    margin-top: -0.5rem !important;
}

/* FLOATING LABELS SPECIFIC STYLES */
.form-floating > label {
    left: 12px;
}
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", ["alert", "alert-danger"]);
        freeform.setOption("errorClassList", ["list-unstyled", "m-0", "fst-italic", "text-danger"]);
        freeform.setOption("errorClassField", ["is-invalid"]);
        freeform.setOption("successClassBanner", ["alert", "alert-success"]);
    })
    // 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: "1rem",
                    spacingUnit: "0.2em",
                    tabSpacing: "10px",
                    gridColumnSpacing: "20px",
                    gridRowSpacing: "20px",
                    colorText: "#212529",
                    colorBackground: "#ffffff",
                    colorDanger: "#dc3545",
                    borderRadius: "0.375rem",
                },
                rules: {
                    '.Tab, .Input': {
                        border: '1px solid #dee2e6',
                        boxShadow: 'none',
                    },
                    '.Tab:focus, .Input:focus': {
                        border: '1px solid #0b5ed7',
                        boxShadow: 'none',
                        outline: '0',
                        transition: 'border-color .15s ease-in-out',
                    },
                    '.Label': {
                        fontSize: '1rem',
                        fontWeight: '400',
                    },
                },
            }
        );
    });
}
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
fields/
/bootstrap-5-floating-labels/fields/
{{ field.render }}
1
<div{{ field.attributes.container }}>
    <div class="form-check">
        {{ field.renderInput }}
        {{ field.renderLabel }}
        {{ field.renderInstructions }}
        {{ field.renderErrors }}
    </div>
</div>
1
2
3
4
5
6
7
8
<div{{ field.attributes.container }}>

    {{ field.renderLabel }}

    {% if field.oneLine %}<div>{% endif %}

    {% for index, option in field.options %}
        <div class="form-check{{ field.oneLine ? " form-check-inline" }}">
            <input type="checkbox"
                   name="{{ field.handle }}[]"
                   value="{{ option.value }}"
                   id="{{ field.idAttribute }}-{{ index }}"
                   class="form-check-input{{ field.hasErrors ? " is-invalid" }}"
                    {{ option.value in field.value ? "checked" }}
            />
            <label class="form-check-label" for="{{ field.idAttribute }}-{{ index }}">
                {{ option.label|t('freeform')|raw }}
            </label>
        </div>
    {% endfor %}

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

    {{ field.renderLabel }}

    {% if field.oneLine %}<div>{% endif %}

    {% for index, option in field.options %}
        <div class="form-check{{ field.oneLine ? " form-check-inline" }}">
            <input type="radio"
                   name="{{ field.handle }}"
                   value="{{ option.value }}"
                   id="{{ field.idAttribute }}-{{ index }}"
                   class="form-check-input{{ field.hasErrors ? " is-invalid" }}"
                    {{ option.value == field.value ? "checked" }}
            />
            <label class="form-check-label" for="{{ field.idAttribute }}-{{ index }}">
                {{ option.label|t('freeform')|raw }}
            </label>
        </div>
    {% endfor %}

    {% if field.oneLine %}</div>{% 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
{% import "freeform/_templates/formatting/bootstrap-5-floating-labels/_row.twig" as rowMacro %}

<div{{ field.attributes.container }}>
    <div class="card">
        <div class="card-header">
            {{ field.renderLabel }}
            {{ field.renderInstructions }}
        </div>
        <div class="card-body pb-0">
            {{ rowMacro.render(field.layout, form) }}
        </div>
    </div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
{{ field.render({
    tableAttributes: {
        table: { class: "table table-sm table-borderless" },
        input: { class: "form-control" },
        dropdown: { class: "form-control form-select" },
        checkbox: { class: "form-check-input" },
        removeButton: { class: "btn btn-sm btn-danger" },
        addButton: { class: "btn btn-sm btn-success" },
    },
}) }}
1
2
3
4
5
6
7
8
9
10
<div{{ field.attributes.container }}>

{% set inputAttributes = ('height:' ~ (25 * field.rows|default(5)) ~ 'px') %}

{% do field.setParameters({
    attributes: {
        input: { 
            "placeholder": field.placeholder,
            "style": inputAttributes
        },
    }
}) %}

{{ field.renderInput -}}
{{- field.renderLabel }}
{{ field.renderInstructions }}
{{ field.renderErrors }}

</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div{{ field.attributes.container }}>

{{ field.renderInput -}}
{{- field.renderLabel }}
{{ field.renderInstructions }}
{{ field.renderErrors }}

</div>
1
2
3
4
5
6
7
8
<div{{ field.attributes.container }}>

{% set inputAttributes = ('height:' ~ (25 * 5) ~ 'px') %}

{% do field.setParameters({
    attributes: {
        input: { 
            "style": inputAttributes
        },
    }
}) %}

{{ field.renderInput -}}
{{- field.renderLabel }}
{{ field.renderInstructions }}
{{ field.renderErrors }}

</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div{{ field.attributes.container }}>

{% do field.setParameters({
    attributes: {
        input: { "placeholder": field.placeholder },
    }
}) %}

{{ field.renderInput -}}
{{- field.renderLabel }}
{{ field.renderInstructions }}
{{ field.renderErrors }}

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

The following CDN links for Bootstrap 5 are for v5.3.1, which may no longer be the latest version. Please see official Bootstrap 5 documentation for latest versions and CDN links.

<!-- Latest compiled and minified CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9" crossorigin="anonymous">

<!-- Latest compiled and minified JavaScript -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-HwwvtgBNo3bZJJLYd8oVXjrBZt8cqVSpeBNS5n7C8IVInixGAoxmnlMuBnhbgrkm" crossorigin="anonymous"></script>
1
2
3
4
5

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).