ACF Form is an awesome tool. It can be used in many scenarios. But when it comes to front-end display, it can become tricky to make it compatible with the actual WordPress theme. In this tutorial we’ll see how to make ACF Form use Bootstrap 4 logic.

Deregister native ACF Form front styles

First, we need to deregister ACF styles added by acf_form_head() to avoid conflicts. To proceed, we will use wp_deregister_style().

add_action('wp_enqueue_scripts', 'acf_form_deregister_styles');
function acf_form_deregister_styles(){
    
    // Deregister ACF Form style
    wp_deregister_style('acf-global');
    wp_deregister_style('acf-input');
    
    // Avoid dependency conflict
    wp_register_style('acf-global', false);
    wp_register_style('acf-input', false);
    
}

Bootstrap row, success message & submit button

The filter acf/validate_form let use customize the default behavior of every acf_form() calls. We will first check if any argument has been manually set inside acf_form(), so we will still have the choice to bypass our Bootstrap 4 default behavior.

Using the official ACF Form documentation, we will wrap form fields with <div class="row"></div>. On our way, we will also add alert alert-success on the sucess message. And finally do the same for the submit button, and append btn btn-primary classes.

add_filter('acf/validate_form', 'acf_form_bootstrap_styles');
function acf_form_bootstrap_styles($args){
    
    // Before ACF Form
    if(!$args['html_before_fields'])
        $args['html_before_fields'] = '<div class="row">'; // May be .form-row
    
    // After ACF Form
    if(!$args['html_after_fields'])
        $args['html_after_fields'] = '</div>';
    
    // Success Message
    if($args['html_updated_message'] == '<div id="message" class="updated"><p>%s</p></div>')
        $args['html_updated_message'] = '<div id="message" class="updated alert alert-success">%s</div>';
    
    // Submit button
    if($args['html_submit_button'] == '<input type="submit" class="acf-button button button-primary button-large" value="%s" />')
        $args['html_submit_button'] = '<input type="submit" class="acf-button button button-primary button-large btn btn-primary" value="%s" />';
    
    return $args;
    
}

Wrap fields with form-group, col-12 & adding form-control

Now that our row has been set, we must add a default fallback column class: col-12. Bootstrap adds form-group to add some bottom margin, and of course, form-control on each input to apply specific styles.

When it comes to modify a field before rendering, acf/prepare_field is the best choice. We will target specifically ACF Form in the front-end via the is_admin() conditional.

Every fields wrappers now have a default form group col-12 classes (100% of the row). The great thing is that you can add custom breakpoint column width directly in your ACF field administration. Just add col-md-6 in the wrapper input, and the following classes will be applied: form group col-12 col-md-6 (50% of the row starting at tablet breakpoint).

add_filter('acf/prepare_field', 'acf_form_fields_bootstrap_styles');
function acf_form_fields_bootstrap_styles($field){
    
    // Target ACF Form Front only
    if(is_admin() && !wp_doing_ajax())
        return $field;
    
    // Add .form-group & .col-12 fallback on fields wrappers
    $field['wrapper']['class'] .= ' form-group col-12';
    
    // Add .form-control on fields
    $field['class'] .= ' form-control';
    
    return $field;
    
}

Adding text-danger on required

In ACF, there is a hook for (almost) everything. Here is an example on how to add text-danger class on the “required” asterisk:

add_filter('acf/get_field_label', 'acf_form_fields_required_bootstrap_styles');
function acf_form_fields_required_bootstrap_styles($label){
    
    // Target ACF Form Front only
    if(is_admin() && !wp_doing_ajax())
        return $label;
    
    // Add .text-danger
    $label = str_replace('<span class="acf-required">*</span>', '<span class="acf-required text-danger">*</span>', $label);
    
    return $label;
    
}

Adding alert-danger on errors

This step is not mandatory, but if you want to add CSS classes on error messages, you will have to add some JavaScript, as the messages are spawned there. Note that the only way to add form-control on Date picker & Google Maps fields is also to use JavaScript.

jQuery(document).ready(function($){
    
    // Check ACF
    if(typeof acf === 'undefined')
        return;
    // Date picker & Google Maps compatibility
    $('.acf-google-map input.search, .acf-date-picker input.input').addClass('form-control');
    
    // Clean errors on submission
    acf.addAction('validation_begin', function($form){
        $form.find('.acf-error-message').remove();
    });
    
    // Add alert alert-danger & move below field
    acf.addAction('invalid_field', function(field){
        field.$el.find('.acf-notice.-error').addClass('alert alert-danger').insertAfter(field.$el.find('.acf-input'));
    });
    
});

Fine tune the submit button & spinner

The submit button & spinner still needs some tweaks. ACF Form adds a disabled class on the acf-button during the form validation and submission process. This behavior is natively supported by WordPress in the backend in order to avoid click spam during submission. But not in the front-end. Let’s fix that with the CSS property pointer-events: none.

As we deregistered ACF styles, we will also have to re-introduce the spinner gif. For compatibility, we will use the native WordPress administration spinner gif.

.acf-button.disabled{
    pointer-events: none;
}
.acf-loading, .acf-spinner {
    height: 20px;
    width: 20px;
    vertical-align: text-top;
    background: transparent url(/wp-admin/images/spinner.gif) no-repeat 50% 50%;
    display:none;
}

Pack it all…

Our ACF Bootstrap Form is now ready! The HTML render exactly as excepted, using all Bootstrap 4 form components.

<form id="acf-form" class="acf-form" action="" method="post">
    
    <div id="acf-form-data" class="acf-hidden">
        <input id="_acf_screen" name="_acf_screen" value="acf_form" type="hidden">
        <input id="_acf_post_id" name="_acf_post_id" value="new_post" type="hidden">
        <input id="_acf_validation" name="_acf_validation" value="1" type="hidden">
        <input id="_acf_form" name="_acf_form" value="..." type="hidden">
        <input id="_acf_nonce" name="_acf_nonce" value="6aa82f255b" type="hidden">
        <input id="_acf_changed" name="_acf_changed" value="0" type="hidden">
    </div>
    
    <div class="acf-fields acf-form-fields -top">
    
        <!-- .row -->
        <div class="row">
            
            <!-- .form-group .col-12 -->
            <div 
                class="acf-field acf-field-text acf-field-5ccc199a22617 is-required form-group col-12" 
                data-name="name" 
                data-type="text" 
                data-key="field_5ccc199a22617" 
                data-required="1">
                
                <div class="acf-label">
                    <label for="acf-field_5ccc199a22617">
                    
                        <!-- .text-danger -->
                        Name <span class="acf-required text-danger">*</span>
                        
                    </label>
                </div>
                
                <div class="acf-input">
                    <div class="acf-input-wrap">
                    
                        <!-- .form-control -->
                        <input type="text" id="acf-field_5ccc199a22617" class="form-control" name="acf[field_5ccc199a22617]" required="required">
                        
                    </div>
                </div>
                
            </div>
            
            <!-- .form-group .col-12 -->
            <div 
                class="acf-field acf-field-email acf-field-5ccc19a022618 is-required form-group col-12" 
                data-name="email" 
                data-type="email" 
                data-key="field_5ccc19a022618" 
                data-required="1">
                
                <div class="acf-label">
                    <label for="acf-field_5ccc19a022618">
                    
                        <!-- .text-danger -->
                        E-Mail <span class="acf-required text-danger">*</span>
                        
                    </label>
                </div>
                
                <div class="acf-input">
                    <div class="acf-input-wrap">
                    
                        <!-- .form-control -->
                        <input type="email" id="acf-field_5ccc19a022618" class="form-control" name="acf[field_5ccc19a022618]" required="required">
                        
                    </div>
                </div>
                
            </div>
            
            <!-- .form-group .col-12 -->
            <div 
                class="acf-field acf-field-textarea acf-field-5ccc19a922619 is-required form-group col-12" 
                data-name="text" 
                data-type="textarea" 
                data-key="field_5ccc19a922619" 
                data-required="1">
                
                <div class="acf-label">
                    <label for="acf-field_5ccc19a922619">
                        
                        <!-- .text-danger -->
                        Text <span class="acf-required text-danger">*</span>
                        
                    </label>
                </div>
                
                <div class="acf-input">
                
                    <!-- .form-control -->
                    <textarea id="acf-field_5ccc19a922619" class="form-control" name="acf[field_5ccc19a922619]" rows="8" required="required"></textarea>
                    
                </div>
            </div>
            
            <div 
                class="acf-field acf-field-text acf-field--validate-email form-group col-12" 
                style="display:none !important;" 
                data-name="_validate_email" 
                data-type="text" 
                data-key="_validate_email">
                
                <div class="acf-label">
                    <label for="acf-_validate_email">
                        Validate Email
                    </label>
                </div>
                
                <div class="acf-input">
                    <div class="acf-input-wrap">
                        <input type="text" id="acf-_validate_email" class="form-control" name="acf[_validate_email]">
                    </div>
                </div>
                
            </div>
         
        <!-- /.row -->
        </div>
        
    </div>
    
    <!-- Submit -->
    <div class="acf-form-submit">
        
        <!-- .btn .btn-primary -->
        <input type="submit" class="acf-button button button-primary button-large btn btn-primary" value="Submit"> 
        <span class="acf-spinner"></span>
        
    </div>
</form>