ACF frontend form to a CPT

ACF frontend form to a CPT

Here’s how you can make a frontend submission form to save content into a custom post type and then display it on your website.

This one was a lot of fun to build, and I’m quite happy with it.

I’ve taken what I did for a client recently and made it a bit more generic for the purposes of this tutorial.
What use cases would a form like this have? Well, you can build your own testimonial submission form to display throughout the site, you can build a user submitted directory, and (with some work) you can even make some sort of client portal page.

The videos are in three parts.
One, intro and setting up the frontend form.
Two, setting up the CPT and customizing the form.
Three, displaying the content using either a post loop module or a shortcode.

OK, so because I like having at least some text instructions along with the video, I’ll add videos with some text explanations.

1. The setup & the frontend form

The Video

You’ll need ACF for this. Even the free version works great. So get that installed, and make sure you’re using the latest updated version.

The plugin linked below has all the code used in this tutorial, but you’ll still have to change it to suit your purposes, but it’s a great start, so please download it.

Just a heads up, the plugin includes the “must install” plugin list function, and when activated, it will urge you to please install ACF and the ACF columns plugin.

Once you’ve installed ACF, you’ll need to set up a CPT that will store all the submissions. This order is different from the video, but that’s mainly because I was just too eager to get into the frontend from code haha So, just to stick with the video order, we’ll go into the frontend form shortcode now, and get to the CPTs later.

Here’s the code that goes in either

  • Child theme functions.php
  • Plugin main php
add_action('wp_head', 'add_acf_form_head');
function add_acf_form_head() {

add_shortcode( 'frontend_form', 'display_frontend_form' );

function display_frontend_form() {

        'post_id'       => 'new_post',
        'post_title'    => true,
        'form' => true, 
        'new_post'      => array(
            'post_type'     => 'ai_review',
            'post_status'   => 'publish'
        'field_groups' => array(132),
        'submit_value'  => 'Submit Review'
    return ob_get_clean(); 

Do NOT add the code into other third party live theme/plugins. Once they update, the code is gone. Please use a child theme or a custom plugin you know what’s inside. The plugin downloaded above already has this code, so all you’ll need to do is modify it to fit your needs.

The main point you’ll need to make sure you modify is the form ID. There’s a number that should go in the array for field_groups.

2. The CPT & feeding the form into the CPT

The Video

Now that the form is set up, we’ll need to get the CPT to match the post_type from the form. Please note, the actual name of the CPT isn’t as important as matching the “Post Type Key” with the post type.

Once that and the field group all match with the CPT, you’re ready to test it.

Take the shortcode [frontend_form] and drop it into a page and see if it works.

Then try submitting something to see if it’s saved into the CPT.

If everything works so far, you’re doing great!

A little extra thing I added for my form was hiding the title field. If users are entering their details, the ‘title’ might feel out of place. So with the following JavaScript you can take a field of your choice and inject that into the title, and it will be helpful when you view the list on the backend.
You’ll need to find the ID of the ACF field you want to take text from and insert that into the textField’s getElementById.

The JavaScript is below. You’ll need to parse out of php to add the script tag, and then parse back in, and that’s why the <?php and ?> are in the snippet below.

        document.addEventListener('DOMContentLoaded', function() {
          var typingTimer;
          var typingDelay = 1500;
          var textField = document.getElementById('acf-field_647ed4bb031d1');
          var titleField = document.getElementById('acf-_post_title');
          textField.addEventListener('input', function() {
            typingTimer = setTimeout(doneTyping, typingDelay);

          function doneTyping() {
            var text = textField.value;
            titleField.value = text;

If you want to add some CSS that will hide the title field, add this CSS (including the <style> tag) DIRECTLY after the </script> tag.

	#acf-form .acf-_post_title {
		position: absolute;
		width: 0;
		height: 0;
		padding: 0;

Now you’re all done. All that’s left is to display it on the frontend.

If you’re comfortable using post loop modules (such as in Oxygen, Breakdance, and Bricks builders) then you’re all good to go. You can watch me try it in Breakdance in the next video.

If you’re using a builder that doesn’t have such customizability, then you’re gonna need a shortcode. Definitely watch the next video.

3. Displaying the CPT Content

The video

The code below will allow you to display the submitted content via shortcode. You’ll need to customize the html and include the php variables in the right places, but it beats struggling with a stubborn 3rd party plugin or theme.

In the video, I walk you through most of the lines in the code. That should definitely help you understand what to modify.

function custom_reviews_shortcode($atts) {

    $a = shortcode_atts( array(
        'limit' => 5,
    ), $atts );

    $query_args = array(
        'post_type' => 'ai_review',
        'posts_per_page' => $a['limit'],
        'orderby' => 'date',
        'order' => 'DSC'

    $query = new WP_Query($query_args);

    if ($query->have_posts()) {

        $output = '<div class="dd-module_review"><ul class="testimonial-archive">';

        while ($query->have_posts()) {
            $name = get_field('name_field');
            $phone_number = get_field('phone_number');
            $phone = str_replace(["-", " "], "", $phone_number);
            $name_parts = explode(" ", $name);
                if(count($name_parts) > 1) {
                    $name_first = $name_parts[0];
                    $name_last = substr($name_parts[1], 0, 1);
                } else {
                    $name_last = "";
            $name_ini = substr($name, 0, 1);
            $output .= '
            <li class="dd-module_review">
                <div class="dd-module_review review-details">
                    <h3><span class="namename">
                        '.$name_first.' '.$name_last.'
                    <a class="dd-module_phone" href="tel:'.$phone.'" target="_self">'.$phone_number.'</a>

        $output .= '</ul></div>';


        return $output;

    } else {

        return 'No reviews found.';


add_shortcode('custom_reviews', 'custom_reviews_shortcode');

That will allow you to display the content with a shortcode. It even has attributes, so you can display only a certain number of submissions. Like this: [custom_reviews limit=”2″]


So I hope this has helped inspire you to make some cool solutions for your clients! Please leave comments if you have any questions!

Please find me on socials or email.


Leave a Reply

Your email address will not be published. Required fields are marked *

  1. Hi fellow breakdancer,

    Thanks for this detailed walkthrough and the 0.1 version of your plugin.

    Unfortunatley I get this when activating it: Plugin could not be activated because it triggered a fatal error.

    I customised it before referring to the correct field group, the name of my local CPT etc.

    I have a WP multisite installation, version 6.5.2

    I use ACF Pro 6.2.9 so I got the error I changed this call:

    ‘name’ => ‘Advanced Custom Fields Pro (ACF)’,
    ‘slug’ => ‘advanced-custom-fields-pro’,

    but still have that message.

    What else might I try

    • Hi Tom, it could be because of a WP issue where zip files compressed on a Mac causes issues.

      Add this snippet to a plugin file or the breakdance zero theme, or in a code snippet plugin and allow it to load like a functions file

      add_filter( 'unzip_file_use_ziparchive', '__return_false' );

  2. After form submission option ‘return’ is default to add ?updated=true at the end of current url.
    But if option ‘return’ is set to some url an error occurs:
    Cannot modify header information – headers already sent by… wp-includes/template-canvas.php:13 … wp-includes/pluggable.php on line 1435

    But if removed:
    add_action(‘wp_head’, ‘add_acf_form_head’);
    function add_acf_form_head() {

    and acf_form_head(); is added inside function display_frontend_form() after ob_start();

    then error is gone.

  3. This is great. I want to achieve exactly what’s presented in this tutorial/post. I have ACF Pro and came across the Advanced Forms add-on plugin that does most of what you have implemented with these few lines of code, to wit, insert the form in a page via shortcode.

    ACF allows associating a fieldset with a CPT but using that method would mean allowing users with certain roles/capabilities access to the dashboard, which is not an option of choice.

    My main challenge now will be fiddling with your plugin code to handle about 30 fields that I want to retrieve from a questionnaire and flow in what resembles paragraphs.

    So thank you!

  4. I really appreciate your tutorial. I am using this a little differently – not for reviews but for a submitted form for a hiking trip. Everything is working perfectly, except.. after submitting the form – which does submit properly on the backend – the screen goes white and does not give me a “It’s posted” notification. I am using the plugin you provided but it appears that it is missing a bit of code to reset the screen with a Succeeded notification. Any help would be great appreciated.

  5. Thanks a lot for the code example! 🙂 Your timing is great, I just needed to add a short form of a certain CPT beneath a single post page of another CPT, and this works perfectly. I am glad I only need WP + ACF to get this working, without any other plugin. I had already stumbled on the acf_form_head() function here: . But I still had to place it beneath and relate it to a parent CPT post, exactly what you are doing.

    Some additions:

    – I had to add a hidden text field to the form to capture the post id of the parent post, to give it a 1 to many relation between the posts for further behind the scenes coding.

    – I did not use a layout builder, but a single-cpt custom template file, in which I just used the shortcode:
    echo do_shortcode( ‘[display_review_form]’ ); . So no extra plugin needed, super lightweight.

    – I preferred not to add it to wp_head action, because that will load it everywhere, so I loaded the acf_form_head() just before the shortcode.

    – I preferred not to use javascript, but just disable the title (by removing it from the ‘supports’ argument in the register_post_type function ) and give it a default title via the wp_insert_post_data filter. You could give it the other value u used in this function as well, to reach the same result.

    Thanks again, great work!


    PS. none of your comment fields contains a *, while they are required 😉

New tutorials

Why no ads?

Hi, I'm PK, and I maintain and publish content on this site for two reasons.

  1. To give back to the community. I learned so much from people's tutorials and StackOverflow that I wanted to contribute and help others.

  2. To provide a more structured learning experience. People struggle to find the course that guides them from start to finish, based off of real life experience, and my courses provide that.

The only "ads" I have here are for my own courses, and maybe an affiliate link, but that's it. They fund the website resources and provide more motivation for me to produce better content.

Any bit of interest helps. Even sharing with your friends, suggesting my courses to your developer or designer, or subscribing to my YT channel, or joining our Discord. Thanks and I'll see you around!

There's a newsletter!

Sign up to the newsletter for the occasional updates on courses, products, tutorials, and sales.

Happy Pride Month!

Happy Pride Month!

Pretty simple really.

Happy pride month to everyone who celebrates it!

I've been seeing a lot of nasty comments on social media recently, so I wanted to add some extra pixels that lean towards love, empathy, and inclusivity to help balance it out.