Forms Photo by Esther Jiao on Unsplash
John Reed

Written by John Reed


Developing custom post types for WordPress extends a seemingly simple blogging platform into a fully-fledged content management system. Adding the Advanced Custom Fields (ACF) plugin to the mix allows for the easy creation of metadata for these post types, but as the number of fields grows, usability becomes a genuine concern.

We recently developed an intake form for a client that mapped to a custom post type with hundreds of fields, so we needed to give end users a way to save their progress (making the questionnaire easier to digest) and allow admins to manage how these fields were presented. This is how we did it using the powerful acf_form() function.

Getting started

In addition to having the ACF PRO plugin installed, this demo will utilize a custom post type called “Application” created using register_post_type() as well as reference custom theme options created and stored using the acf_add_options_page() function.

Field groups for our custom post type were added and organized by subject area for the intake questionnaire.

For our theme options, we created an ACF Repeater with two sub-fields: a simple text field for the form/page title and a textarea to store the field group IDs, which we’ll use to reference the custom fields from our custom post type. This demo uses just one field group per page, but using a textarea provides the flexibility to have multiple field groups per page (we will explode the textarea into an array when we code out the form).

The Template

We will retrieve the intake pages established above in our template to start building our UI. While acf_form() will do the heavy lifting to generate our form, we also need to include acf_form_head() at the top of our page template to enqueue all ACF-related scripts and styles for the form to display correctly.

// get intake pages to display
$intake_pages = get_field( 'intake_pages', 'option' ) ?: [];

// post_id or 'new_post'
$post_id = isset( $_GET['post_id'] ) ? $_GET['post_id'] : 'new_post';

// ACF form head

// our theme header
get_header(); ?>

Next, let’s build the tabs for our intake form:

<ul class="nav nav-pills">
foreach( $intake_pages as $i => $intake_page ):
    // convert the title to a sanitized slug
    $slug = sanitize_title( $intake_page['title'] );

    // build the href with post_id and step params
    $href = site_url( sprintf( '?post_id=%s&step=%s', $post_id, $slug ) );

    // active state
    $active = ( !isset( $_GET['step'] ) && !$i ) || $_GET['step'] === $slug;

    <li class="nav-item">
        <a class="nav-link<?php echo $active ? ' active' : ''; ?>" href="<?php echo $href; ?>">
            <?php echo $intake_page['title']; ?>

    // only display the first tab for new entries
    if( 'new_post' === $post_id ) break;

endforeach; ?>

Now, we can build our form:

while ( have_posts() ) : the_post();

    foreach( $intake_pages as $i => $intake_page ):

        // convert the title to a sanitized slug
        $slug = sanitize_title( $intake_page['title'] );

        // build the href with post_id and step params
        $href = site_url( sprintf( '?post_id=%s&step=%s', $post_id, $slug ) );

        // active state
        $active = ( !isset( $_GET['step'] ) && !$i ) || $_GET['step'] === $slug;

        // prevent multiple forms from rendering on single page view
        if( ! $active ) continue;

        // convert our saved textarea value into an array
        $field_groups = explode( "\r\n", $intake_page['page_field_groups'] );

        // get next page
        $next = next( $intake_pages );

        // build the return URL
        $return = site_url( sprintf( '?post_id=%s&step=%s', $post_id, $next ? sanitize_title( $next['title'] ) : $slug ) );

        // generate our form
        acf_form( [
            'id'           => sprintf( '%s-form', $slug ),
            'post_id'      => $post_id,
            'field_groups' => $field_groups,
            'new_post' => [
                'post_type'   => 'application',
                'post_status' => 'publish',
            'return' => $return,
        ] );



One acf_form(), two use cases

The magic of this function lies in the post_id and new_post parameters. If post_id is an integer, the ACF plugin will know we are editing a specific post and will update that post when the form is submitted. When post_id is set to ‘new_post’, the new_post parameter will be used to establish the baseline parameters for this post and accepts all of the options as wp_insert_post() does. Pretty cool!

Caveats and final thoughts

It should be noted that passing a post_id as a query parameter has security implications. For the sake of simplicity, this demo does not check that the current user has permission to edit the specified post, but you’ll definitely want to do so in your application. That said, with a bit of setup and a couple of loops, you can see the power of the acf_form() function. This simple approach allowed our client to create a 10-step intake form with hundreds of custom fields, all without writing a single <form> or <input> tag. If you like what you see, contact us for help setting up a multi-step form on your website!

Advanced Custom FieldsCustom Post TypesFormsPHPWordPress