ACF Flexible Content Renderer — Reusable PHP Without Switch/Case

Stop writing giant if/else chains for ACF Flexible Content. This system maps each layout to its own template file — add new layouts without touching the renderer.

PHP utility Production-tested ~15 min setup Flexible Content Template system WP 6.0+ · ACF 6.0+ Updated May 14, 2026
TL;DR

This ACF flexible content renderer PHP snippet maps each Flexible Content layout to its own PHP template file, so you can add layouts without editing a switch statement.

Use this when your Flexible Content field has more than 3 layouts, when multiple developers work on the same project, or when you want to stop scrolling through a 200-line if/else chain to make a one-line change.

01 — Building it step by step

ACF flexible content renderer — The Code

This ACF flexible content renderer snippet loads each layout from its own file — add a new layout by creating a file, no code changes to the renderer.

Don’t try to understand everything at once. We’ll build this in 5 small steps. Each step works on its own, and each one adds just one new idea. Start with Step 1 — a painful before-and-after comparison — and layer on one concept at a time.

Step 1: Bad vs Good. First, look at what you’re trying to escape — a monstrous if/else chain that grows by 30 lines every time you add a layout. Then see the 4-line replacement that will change how you build Flexible Content forever.

template-parts/sections/step-1-bad.php
php
<?php
// BAD: Every layout lives inside one giant renderer file.
// Adding a layout = scrolling to the right elseif spot + writing inline HTML.
// After 10 layouts this is 400 lines of unreadable spaghetti.

if ( have_rows( 'page_sections' ) ) {

    while ( have_rows( 'page_sections' ) ) {
        the_row();

        if ( get_row_layout() === 'hero' ) {
            ?>
            <section class="hero">
                <h1><?php the_sub_field( 'title' ); ?></h1>
                <p><?php the_sub_field( 'subtitle' ); ?></p>
            </section>
            <?php
        } else if ( get_row_layout() === 'features' ) {
            ?>
            <section class="features">
                <?php while ( have_rows( 'features_list' ) ) { the_row(); ?>
                    <div class="feature">...</div>
                <?php } ?>
            </section>
            <?php
        } else if ( get_row_layout() === 'testimonials' ) {
            ?>
            <section class="testimonials">...</section>
            <?php
        } else if ( get_row_layout() === 'cta' ) {
            ?>
            <section class="cta">...</section>
            <?php
        } else if ( get_row_layout() === 'gallery' ) {
            ?>
            <section class="gallery">...</section>
            <?php
        }

    }

}
template-parts/sections/step-1-good.php
php
<?php
// GOOD: One line per layout. Add a new file, it renders automatically.
// The layout name becomes the filename. No if/else ever again.

if ( have_rows( 'page_sections' ) ) {

    while ( have_rows( 'page_sections' ) ) {
        the_row();

        // get_row_layout() returns the layout slug: 'hero', 'features', etc.
        // get_template_part() loads template-parts/sections/hero.php, etc.
        get_template_part( 'template-parts/sections/' . get_row_layout() );

    }

}

Step 2: Prevent crashes. The 4-line version from Step 1 will throw a PHP warning if a layout template file doesn’t exist. locate_template() checks for the file before including it — no front-end errors.

template-parts/sections/step-2-safe-include.php
php
<?php

if ( have_rows( 'page_sections' ) ) {

    while ( have_rows( 'page_sections' ) ) {
        the_row();

        $layout   = get_row_layout();
        $template = 'template-parts/sections/' . $layout . '.php';

        // Only include the file if it actually exists on disk.
        if ( locate_template( $template ) ) {
            get_template_part( 'template-parts/sections/' . $layout );
        }

    }

}

Step 3: Developer-friendly debugging. When a template IS missing, logged-in admins see an HTML comment in the source code telling them exactly which file to create. Regular visitors see nothing. This saves hours of “why is my layout not showing up?” debugging.

template-parts/sections/step-3-debug-feedback.php
php
<?php

if ( have_rows( 'page_sections' ) ) {

    while ( have_rows( 'page_sections' ) ) {
        the_row();

        $layout   = get_row_layout();
        $template = 'template-parts/sections/' . $layout . '.php';

        if ( locate_template( $template ) ) {

            get_template_part( 'template-parts/sections/' . $layout );

        } else if ( current_user_can( 'administrator' ) ) {

            // Admin-only: a silent HTML comment in the source code.
            echo '<!-- [Flexible Content] Missing template: ' . esc_html( $template ) . ' -->';

        }

    }

}

Step 4: Make it reusable. Wrap everything into a function so you can call it from any page template — front-page.php, page.php, single.php — each with a different Flexible Content field name. No copy-pasting the loop everywhere.

template-parts/sections/step-4-function.php
php
<?php

function render_flexible_sections( $field_name = 'page_sections', $template_dir = 'template-parts/sections' ) {

    if ( ! have_rows( $field_name ) ) {
        return;
    }

    while ( have_rows( $field_name ) ) {
        the_row();

        $layout   = get_row_layout();
        $template = $template_dir . '/' . $layout . '.php';

        if ( locate_template( $template ) ) {
            get_template_part( $template_dir . '/' . $layout );
        } else if ( current_user_can( 'administrator' ) ) {
            echo '<!-- [Flexible Content] Missing template: ' . esc_html( $template ) . ' -->';
        }

    }

}

// Usage in your page template:
// render_flexible_sections( 'page_sections' );
//
// Or with a different field name:
// render_flexible_sections( 'cms', 'template-parts/home' );

Step 5: Everything together. Steps 1–4 are teaching code — each one introduces a single idea. This is the complete production renderer: all steps combined, with WordPress filter hooks added so plugins and child themes can override layout paths or handle missing templates. The inline step comments are gone.

template-parts/sections/step-5-production.php
php
<?php
/**
 * Full file: inc/render-flexible-sections.php
 * Step 5 — all layers combined into one production-ready function.
 * Require from functions.php: require_once get_template_directory() . '/inc/render-flexible-sections.php';
 *
 * Usage: render_flexible_sections( 'page_sections' );
 */

function render_flexible_sections( $field_name = 'page_sections', $template_dir = 'template-parts/sections' ) {

    if ( ! have_rows( $field_name ) ) {
        return;
    }

    while ( have_rows( $field_name ) ) {
        the_row();

        $layout = get_row_layout();

        // Let plugins override which template file gets loaded for this layout.
        $template = apply_filters(
            'flexible_renderer_template_path',
            $template_dir . '/' . $layout . '.php',
            $layout,
            $field_name
        );

        if ( locate_template( $template ) ) {

            get_template_part( $template_dir . '/' . $layout );

        } else {

            // Fire an action so developers can log or handle missing templates.
            do_action( 'flexible_renderer_missing_template', $template, $layout );

            if ( current_user_can( 'administrator' ) ) {
                echo '<!-- [Flexible Content] Missing template: ' . esc_html( $template ) . ' -->';
            }

        }

    }

}

Optional helper functions. These are not required — they just keep your layout files cleaner by handling the outer <section> wrapper once instead of repeating it in every template file.

template-parts/sections/section-helpers.php
php
<?php
/**
 * Optional helpers — wrap each layout consistently.
 *
 * These are NOT required. They just keep your layout files cleaner by
 * handling the outer <section> tag once instead of repeating it in every file.
 *
 * Usage inside each layout template (e.g. template-parts/sections/hero.php):
 *
 *   section_start( 'hero', 'hero--dark' );
 *   // ... hero content here ...
 *   section_end();
 */

/**
 * Opens a <section> tag with layout-specific classes.
 *
 * @param string $layout  The layout slug (e.g. 'hero').
 * @param string $classes Extra CSS classes (optional).
 */
function section_start( $layout, $classes = '' ) {
	printf(
		'<section class="page-section page-section--%s %s">',
		esc_attr( $layout ),
		esc_attr( $classes )
	);
}

/**
 * Closes the current section.
 */
function section_end() {
	echo '</section>';
}

Where to put the code

Place the Step 5 function in inc/render-flexible-sections.php and require it from your theme’s functions.php. In your page template, replace your entire Flexible Content loop with one call: render_flexible_sections( 'page_sections' );

02 — How it works

How ACF flexible content renderer Works — Why Each Layer Exists

Here is how this ACF flexible content renderer works step by step: We built this in 5 layers. Each layer solves a specific pain point from the previous version.

This ACF flexible content renderer implementation works as follows.

We built this in 5 layers. Each layer solves a specific pain point from the previous version. Let’s walk through why each layer exists.

1

The problem: if/else chains that grow forever

The problem is real: a 10-layout Flexible Content field means 10 elseif blocks, each with inline HTML. Adding layout #11 means finding the right spot in a 400-line file, writing another elseif, and pasting HTML. One typo and every layout below it breaks.

The solution: get_template_part() already knows how to include a PHP file. get_row_layout() returns the layout slug. If your layout is named "hero", the function includes template-parts/sections/hero.php. When you add a new layout named "pricing" and create template-parts/sections/pricing.php, it renders automatically — zero changes to the renderer.

2

File existence check — prevent PHP warnings

The naive one-liner get_template_part( 'template-parts/sections/' . get_row_layout() ) works — until someone creates a layout in ACF but forgets to create the PHP file. WordPress throws a warning, and if WP_DEBUG is on, your page shows an error.

locate_template() returns the full file path if the template exists, or an empty string if it doesn’t. Wrapping the include in an if ( locate_template() ) check silently skips layouts with no matching file.

3

Admin feedback — know which file to create

Skipping silently is safe, but during development you need to know which file is missing. The current_user_can( 'administrator' ) check outputs an HTML comment — invisible to visitors, visible in View Source — telling you exactly which file path to create.

This is the difference between spending 20 minutes wondering why your new “pricing” layout doesn’t appear and knowing instantly: “Oh, I need to create template-parts/sections/pricing.php.”

4

Wrap in a function — use anywhere

Once the loop works, you don’t want to paste it into every page template. Wrapping it in a function with parameters gives you:

  • $field_name — the ACF Flexible Content field name. Defaults to "page_sections" but you can pass "homepage_blocks" or any other field name.
  • $template_dir — the folder path. Defaults to "template-parts/sections" but you can use "template-parts/home" for a homepage-specific renderer.

Now your template is one line: render_flexible_sections( 'page_sections' );

5

Filter hooks — let plugins register layouts

Production sites have plugins. A client installs a “Team Members” plugin that wants to add its own Flexible Content layout. Without hooks, the plugin developer has no way to register their template path.

Two hooks solve this:

  • apply_filters( 'flexible_renderer_template_path', ... ) — A filter that lets any code modify which file gets loaded. A plugin returns "plugins/team-members/templates/member-grid.php" for the "member_grid" layout.
  • do_action( 'flexible_renderer_missing_template', ... ) — An action that fires when a template is missing. Hook in to log it, email the admin, or show a styled fallback.
6

Section helpers — consistent wrapper markup

The helpers are optional, but they solve a real annoyance: every layout file starts with <section class="page-section page-section--hero"> and ends with </section>. If you later decide to change section to div, you edit every file.

With section_start() and section_end(), your layout files look like:

section_start( 'hero', 'hero--dark' );n    // your content herensection_end();

Change the HTML wrapper once in the helper, and every layout updates.

7

Common mistakes that break the renderer

The biggest beginner mistake: using get_template_part() or locate_template() outside the have_rows() loop. These functions must be called INSIDE while( have_rows() ) : the_row() because get_row_layout() only works when a Flexible Content row is active.

Second mistake: forgetting to bail early with if ( ! have_rows( $field_name ) ) { return; }. Without this, the function does nothing silently — but it’s cleaner to exit explicitly so the reader knows this is intentional.

03 — Setup & ACF fields

ACF flexible content renderer — Setup & Integration

One PHP registration snippet, one renderer function, one file per layout. Everything wires together in about 10 minutes.

This ACF flexible content renderer implementation works as follows.

The renderer works with any Flexible Content field group. Here’s the folder structure and a sample ACF JSON file to get you started with a working setup.

wp-content/themes/your-theme/
│   ├── inc/
│   │   └── render-flexible-sections.php
│   ├── template-parts/
│   │   └── sections/
│   │   │   ├── hero.php
│   │   │   ├── features.php
│   │       └── cta.php
    └── functions.php
1

Create the folder structure

Create the folder structure shown above. The inc/ folder holds the renderer function. template-parts/sections/ holds one PHP file per layout. The ACF JSON file goes in acf-json/.

2

Save the renderer function

Copy the Step 5 function code into inc/render-flexible-sections.php. In your theme’s functions.php, add: require_once get_template_directory() . '/inc/render-flexible-sections.php';

3

Create one PHP file per layout

For each Flexible Content layout, create a PHP file named after the layout slug. If your layout is called "hero", create template-parts/sections/hero.php. Inside, use get_sub_field() to pull the layout’s fields:

<?phpn$title    = get_sub_field( 'title' );  // ACF field type: Textn$subtitle = get_sub_field( 'subtitle' ); // ACF field type: Textarean?>nn<h1><?php echo esc_html( $title ); ?></h1>n<p><?php echo esc_html( $subtitle ); ?></p>
4

Call it from your page template

In your page template (e.g., front-page.php), replace your entire Flexible Content loop with:

<?php render_flexible_sections( 'page_sections' ); ?>

If your Flexible Content field has a different name, pass it: render_flexible_sections( 'homepage_blocks' );

5

Test it — create a layout and check the front end

Create one layout template to test. Go to any Page in the admin, add a Hero layout, fill in the fields, and publish. Visit the page — your hero.php file should render.

If nothing appears, View Source and look for an HTML comment starting with [Flexible Content] Missing template:. That tells you the exact file path to create.

Registering the Flexible Content field in PHP. acf_add_local_field_group() creates the field group and all layouts in functions.php — no importing, no JSON files to maintain. The example below creates a page_sections FC field with a Hero layout. Add more layouts to the layouts array following the same pattern — each layout name must match a file in template-parts/sections/ because the renderer loads {name}.php automatically.

functions.php
php
<?php
// Paste the code below into your theme's functions.php.
// Do NOT copy the <?php line above — your functions.php already has one.

// functions.php
// Registers the Page Sections FC field group in PHP.
// Add more layouts to the layouts array — each name maps to a file
// in template-parts/sections/{name}.php that the renderer loads automatically.

add_action( 'acf/init', function() {

	acf_add_local_field_group( [
		'key'    => 'group_page_sections',
		'title'  => 'Page Sections',
		'fields' => [
			[
				'key'          => 'field_page_sections_flexible',
				'label'        => 'Page Sections',
				'name'         => 'page_sections',
				'type'         => 'flexible_content',
				'button_label' => 'Add Section',
				'layouts'      => [
					[
						'key'        => 'layout_hero',
						'name'       => 'hero', // loads template-parts/sections/hero.php
						'label'      => 'Hero',
						'display'    => 'block',
						'sub_fields' => [
							[ 'key' => 'field_hero_title',    'name' => 'title',    'label' => 'Title',    'type' => 'text' ],
							[ 'key' => 'field_hero_subtitle', 'name' => 'subtitle', 'label' => 'Subtitle', 'type' => 'textarea' ],
							[ 'key' => 'field_hero_image',    'name' => 'image',    'label' => 'Image',    'type' => 'image', 'return_format' => 'id' ],
						],
					],
					[
						'key'        => 'layout_features',
						'name'       => 'features', // loads template-parts/sections/features.php
						'label'      => 'Features',
						'display'    => 'block',
						'sub_fields' => [
							[ 'key' => 'field_features_heading', 'name' => 'heading', 'label' => 'Heading', 'type' => 'text' ],
						],
					],
					// Add more layouts here following the same pattern.
				],
			],
		],
// Change 'page' to your post type — e.g. 'post', 'project', or your CPT slug.
		'location' => [ [ [ 'param' => 'post_type', 'operator' => '==', 'value' => 'page' ] ] ],
	] );

} );
04 — Making it your own

Making This ACF flexible content renderer Your Own

Customise this ACF flexible content renderer to match your specific needs: The core system is working — now adapt it to your folder structure, plugins, and content types.

1

Use a different template folder

To use a different folder structure, change the second parameter:

// Load from template-parts/home/ insteadnrender_flexible_sections( 'page_sections', 'template-parts/home' );

Or make it global in your function: change the default value of $template_dir.

2

Let plugins register their own layouts

To support layouts from a plugin, use the filter hook. In your plugin:

add_filter( 'flexible_renderer_template_path', function( $template, $layout, $field_name ) {n    if ( $layout === 'member_grid' ) {n        return 'plugins/team-members/templates/member-grid.php';n    }n    return $template;n}, 10, 3 );

Now when the editor adds a “member_grid” layout, the renderer loads the plugin’s template file instead of looking in the theme.

3

Log missing templates instead of showing HTML comments

To log missing templates instead of showing an HTML comment, hook into the action:

add_action( 'flexible_renderer_missing_template', function( $template, $layout ) {n    error_log( 'Flexible Content: missing template for layout "' . $layout . '" at ' . $template );n    // Or: wp_mail( get_option( 'admin_email' ), 'Missing layout', $template );n}, 10, 2 );
4

Use it on posts, options pages, or products

The renderer works with any content type that has ACF Flexible Content. Pass the field name:

// On a single post with flexible contentnrender_flexible_sections( 'article_sections' );nn// On an options pagenrender_flexible_sections( 'theme_settings_sections' );nn// On a WooCommerce productnrender_flexible_sections( 'product_sections' );

3 things that will break your renderer:

1. Calling render_flexible_sections() outside the WordPress loop. have_rows() needs a post context. If you’re on a custom query or a 404 page, the function silently does nothing. Wrap in an is_singular() check if needed.

2. Changing layout slugs in ACF without renaming files. The renderer maps layout slug → filename. If you rename a layout from “hero” to “home_hero” in ACF, you must rename hero.php to home_hero.php.

3. Using get_field() inside layout templates instead of get_sub_field(). Inside a Flexible Content layout file, always use get_sub_field(). get_field() returns the page-level value, which is usually empty inside a layout.

Performance notes:

  • locate_template() is fast: It caches the template path internally, so checking the same file across multiple pages doesn’t re-scan the filesystem.
  • No extra queries: The renderer doesn’t run any additional database queries. It only calls ACF functions (have_rows(), the_row(), get_row_layout()) which read from ACF’s internal cache.
  • Template files are lazy: get_template_part() only includes a file when the layout is actually used on the page. If you have 20 layouts defined but only 3 are used, only 3 files are loaded.
  • Avoid expensive operations in the loop: The while( have_rows() ) loop itself is fine. Just don’t put WP_Query or remote API calls inside it — those belong inside the individual layout templates, where they only run if the layout is active.
0 comments

No comments yet. Be the first.

Leave a Reply

Table of Contents

ACF flexible content renderer — Code Snippet

ACF flexible content renderer — code snippet by Mosharaf Hossain

This ACF flexible content renderer is a production-tested PHP snippet for WordPress developers. See the ACF Flexible Content docs for related official documentation.

The ACF flexible content renderer snippet replaces the standard switch or if/elseif chain for ACF Flexible Content layouts with a file-based convention: each layout name maps to a template file in template-parts/sections/, loaded automatically. Adding a new layout means creating a new file — no changes to the renderer function, no switch case to update, no deployment of modified PHP logic for what is purely a content change.

The problem with the switch-based ACF renderer is that it grows without bound. A site that starts with three Flexible Content layouts has a three-case switch statement. After twelve months it has fifteen cases, four of which include their own nested conditionals. The renderer function becomes a source of merge conflicts because every new layout requires editing it. When two developers add layouts on different branches simultaneously, there is a conflict in the switch statement that neither can resolve without reading the other’s work.

The convention-over-configuration approach works as follows: the layout name in ACF admin is converted to a template file path. A layout named hero_banner loads template-parts/sections/hero-banner.php. A layout named text_image_split loads template-parts/sections/text-image-split.php. The renderer uses str_replace to convert underscores to hyphens and locate_template to resolve the path, honouring child theme overrides automatically.

Each layout template receives the ACF sub-field context automatically. Sub-fields are available via get_sub_field inside the loaded template, the same as inside a standard have_rows loop, because the renderer calls get_template_part inside the loop. The template file has full access to the layout context without needing explicit variable passing via extract or function arguments.

A filter hook on the template path allows plugin developers and child themes to remap layout names to different template locations. Place the renderer function in inc/flexible-renderer.php and require from functions.php. Layout templates go in template-parts/sections/.

Tested on WordPress 6.0+ with ACF 6.0+ and PHP 8.0+. Browse all ACF snippets at Code Snippets or see the Dynamic ACF Post Source snippet. This renderer powers every page template in the Vitamines project.

Related: ACF Post Source snippet — and browse the full code snippet library.

Chat on WhatsApp