Create a multi-pages form with ACF

How to create a frontend ACF form exploded in multiple steps?

You might not know that, but I love using ACF for small to medium WordPress sites. In a matter of minutes, we can easily create beautiful metaboxes and populate them with complex fields.

Many well-thought hooks gives us, developers, a great way to interact with ACF native logic and deal, for example, with custom validations, dynamic on-the-fly modification of fields, custom events after post save, etc.

A lesser-known feature of Advanced Custom Fields is its ability to let us generate front-end forms via a simple function. I used it intensively on some complex clients projects and it’s certainly a very useful feature to quickly set up front-end user flows without the hassle of writing hundreds of lines of code.

But throwing a super long form for the user to fill out is obviously not an ideal experience. In this proof-of-concept, we will leverage the flexibility of the acf_form() function to optimize this experience and explode a very long form into multiple smaller forms.

How to create multi-steps front-end forms with ACF

Code and demonstration

The code of this proof-of-concept is available on GitHub.

We will create a basic 3-steps form to let users request an estimate, as shown below. The form submission will create an estimate-request CPT post in our WordPress database with meta fields populated by user posted data (though that is natively handled by ACF’s acf_form() function).


Obviously, we need Advanced Custom Fields installed!

We also need an “Estimate Request” custom post type to host our form submissions data.

You can quickly create this CPT with the CPT UI plugin (JSON configuration file available here).

We also need to add ACF Field Groups (or metaboxes) to this CPT. In our example, we will be using these four Field Groups (FYI, you can import them in your installation via the the ACF admin Tools section).

Finally, you will need the necessary acf_form_head() function at the top of your current theme header.php file as mentioned in ACF documentation.

General approach

First, we will be creating a simple [acf_multiforms_example] shortcode in charge of outputting the ACF form on the front-end. The logic could be extracted into a function used in a template, or even in a Gutenblock, that’s really up to you.

Displaying a specific set of fields

The important thing to understand is that each step of our form is made of one or multiple ACF Field Groups.

Depending on the user current step in the form, only a specific set of field group(s) will be displayed.

We will dynamically tell acf_form() which set of field group(s) to display by using its field_groups parameter.

One step after another

The current “step” (or “state”) of the form will come from a $_GET parameter. By passing ?step=X in the URL, we will explicitly inform our logic that we want to display a specific form step (in other terms, that we want to present a specific set of metaboxes to the user).

To do so, we will hook on the acf/save_post action to:

  1. intercept the form submission
  2. analyze which step of the form has just been filled and decide if:
    1. we redirect the user to the next step (basically the exact same URL but with an updated ?step parameter) if it’s not the final one
    1. or, if it’s the final step, we redirect the user to the same page with a ?finished parameter and display a “Thank you” message instead of the previous form.

Creating or editing a post?

The acf_form() post_id parameter accepts 2 possible types of values:

  • new_post if we want to create a new post after the form submission (wp_insert_post())
  • or an integer to tell ACF that we are currently editing a specific existing post (wp_update_post())

This flexibility is highly valuable in our case: when a user visits our form page and submits the very first step of the form, we will let ACF create a new estimate_request post for us.

Afterwards, starting on the second step until the final form step, we will tell ACF that we want to edit the previously-created post. We obviously don’t want ACF to create a new post at every step, but instead keep on editing the post that was created after the first step.

The reference to this post created by ACF will be passed via a ?post_id=X parameter in the URL.

A word on security

Whaaaat? Passing a $_GET parameter to display an edit form for a specific post?! 

Yeah, that’s a bit risky. To avoid letting anyone edit any post on our site, we will create a random security token on the very first step (just after ACF created the form). This token, saved in a post meta on the newly created post, will be needed in the URL in order to be able to edit this specific post.

No token, or invalid token? We then ignore the ?post_id=X request and we present a brand new blank form to the user.

Modifying the token in URL

We won’t be able to edit a specific post without passing the correct token in the URL. Note that this will be done seamlessly for the user after submitting the first form step, as we will redirect him/her to the next form step with the token already in place in the URL.


The benefits of using ACF for front-end forms are obvious:

  1. don’t reinvent the wheel: no need to write complex forms HTML markup or write custom JavaScript to validate fields
  2. native AJAX validation on fields before form submission
  3. easily let users edit or create posts from the front-end
  4. focus on your specific needs by hooking on acf/save_post action

It’s pretty clear that a lot of precious developer time will be saved.

What’s next?

The code of this proof-of-concept is available on GitHub.

I hope to create a Composer-ready library (or plugin?) to automate, simplify and enhance the creation of front-end forms with ACF.

I can think of many cool features to offer a great user experience on the front-end (progress bar, “previous” button to get back to previous step, etc.) and to make the life of developers easier (auto-magic creation of Gutenblock to output a form, abstract action hook to intercept data depending on current form step state, etc.)

You have a specific feature in mind? Let me know in the comments!

You liked this content?

Make sure to share it on your social networks!