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.
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.
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.
<?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
}
}
}
<?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.
<?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.
<?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.
<?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.
<?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.
<?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' );
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.
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.
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.
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.”
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' );
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.
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.
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.
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.
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/.
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';
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>
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' );
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.
<?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' ] ] ],
] );
} );
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.
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.
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.
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 );
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.

No comments yet. Be the first.