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.
// ACF flexible content renderer — production-tested PHP snippet by Mosharaf Hossain
<?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 elseif ( get_row_layout() === 'features' ) : ?>
<section class="features">
<?php while ( have_rows( 'features_list' ) ) : the_row(); ?>
<div class="feature">...</div>
<?php endwhile; ?>
</section>
<?php elseif ( get_row_layout() === 'testimonials' ) : ?>
<section class="testimonials">...</section>
<?php elseif ( get_row_layout() === 'cta' ) : ?>
<section class="cta">...</section>
<?php elseif ( get_row_layout() === 'gallery' ) : ?>
<section class="gallery">...</section>
<?php endif;
endwhile;
endif;
// ACF flexible content renderer
<?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() );
endwhile;
endif;
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.
// ACF flexible content renderer
<?php
/**
* Step 2 — Prevent crashes when a layout file is missing.
*
* New in this step:
* locate_template() checks if the file exists before including it.
* Without this, a missing file = a PHP warning on the front end.
*/
if ( have_rows( 'page_sections' ) ) :
while ( have_rows( 'page_sections' ) ) : the_row();
// Build the expected template path from the layout name.
$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 );
}
endwhile;
endif;
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.
// ACF flexible content renderer
<?php
/**
* Step 3 — Add debug feedback for developers.
*
* New in this step:
* When a layout template doesn't exist, logged-in admins see an HTML
* comment telling them exactly which file is missing. Helpful during
* development. Invisible to regular visitors.
*/
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 );
} elseif ( current_user_can( 'administrator' ) ) {
// Admin-only: a silent HTML comment in the source code.
echo '<!-- [Flexible Content] Missing template: ' .
esc_html( $template ) . ' -->';
}
endwhile;
endif;
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.
// ACF flexible content renderer
<?php
/**
* Step 4 — Wrap everything into a reusable function.
*
* Place this in your theme's functions.php or inc/ folder.
* Now you can call it from ANY page template — front-page.php,
* page.php, single.php — with different Flexible Content field names.
*
* New in this step:
* $field_name parameter — use 'page_sections', 'homepage_blocks', etc.
* $template_dir parameter — change the folder if needed.
*/
function render_flexible_sections( $field_name = 'page_sections', $template_dir = 'template-parts/sections' ) {
// Bail early if the Flexible Content field is empty.
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 );
} elseif ( current_user_can( 'administrator' ) ) {
echo '<!-- [Flexible Content] Missing template: ' .
esc_html( $template ) . ' -->';
}
endwhile;
}
// ── Usage in your page template ──────────────────
// The function replaces 30+ lines of if/else with one call:
// render_flexible_sections( 'page_sections' );
//
// Or with a different field name:
// render_flexible_sections( 'homepage_blocks', 'template-parts/home' );
Step 5: Extensibility with WordPress filters. Add apply_filters() and do_action() hooks so plugins and child themes can register custom layout paths or handle missing templates in their own way. This is the version you ship to clients.
// ACF flexible content renderer
<?php
/**
* Step 5 — The full production version with filter hooks.
*
* Place this in inc/render-flexible-sections.php and require it from functions.php.
*
* Filters allow plugins, child themes, or custom modules to:
* 1. Override the template path for a specific layout.
* 2. Do something custom when a template is missing (log it, notify).
*
* Usage in your page template:
* 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();
/**
* Filter: flexible_renderer_template_path
*
* Let other developers override which file gets loaded for a layout.
* Example: a plugin could return 'plugin-parts/hero.php' for the 'hero' layout.
*
* @param string $template Default template path.
* @param string $layout The layout slug (e.g. 'hero').
* @param string $field_name The ACF Flexible Content field name.
*/
$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 {
/**
* Action: flexible_renderer_missing_template
*
* Fires when a layout template is not found. Hook in to log the
* missing file or display a fallback.
*
* @param string $template The path that was tried.
* @param string $layout The layout slug.
*/
do_action( 'flexible_renderer_missing_template', $template, $layout );
if ( current_user_can( 'administrator' ) ) {
echo '<!-- [Flexible Content] Missing template: ' .
esc_html( $template ) . ' -->';
}
}
endwhile;
}
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.
// ACF flexible content renderer
<?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
To integrate this ACF flexible content renderer into your project: The renderer works with any Flexible Content field group. Here's how to set up your files and fields.
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.
Using the sample ACF JSON file
Copy the JSON below into a new file at:
wp-content/themes/your-theme/acf-json/group_page_sections.json
The "local": "json" key tells ACF to keep this field group synced from the JSON file — changes stay version-controlled and won’t be overwritten by database edits.
After placing the file, go to Custom Fields → Tools → Import Field Groups and click Import. You’ll then see:
- The field group Page Sections with a Flexible Content field called Page Sections
- Two sample layouts: Hero and Features with a few sample sub-fields
- The layouts appear on all Pages (you can edit the
locationkey to change this)
Add more layouts through the ACF admin. Each new layout you create needs a matching PHP file at template-parts/sections/{layout-name}.php.
// ACF flexible content renderer — production-tested PHP snippet by Mosharaf Hossain
{
"key": "group_page_sections",
"title": "Page Sections",
"fields": [
{
"key": "field_page_sections_flexible",
"label": "Page Sections",
"name": "page_sections",
"type": "flexible_content",
"instructions": "Build your page by adding section layouts.",
"button_label": "Add Section",
"layouts": {
"layout_hero": {
"key": "layout_hero",
"name": "hero",
"label": "Hero",
"display": "block",
"sub_fields": [
{
"key": "field_hero_title",
"label": "Title",
"name": "title",
"type": "text"
},
{
"key": "field_hero_subtitle",
"label": "Subtitle",
"name": "subtitle",
"type": "textarea"
}
]
},
"layout_features": {
"key": "layout_features",
"name": "features",
"label": "Features",
"display": "block",
"sub_fields": [
{
"key": "field_features_list",
"label": "Features",
"name": "features_list",
"type": "repeater",
"sub_fields": [
{
"key": "field_feature_title",
"label": "Feature Title",
"name": "feature_title",
"type": "text"
}
]
}
]
}
}
}
],
"location": [
[
{
"param": "post_type",
"operator": "==",
"value": "page"
}
]
],
"menu_order": 0,
"position": "normal",
"style": "default",
"label_placement": "top",
"instruction_placement": "label",
"hide_on_screen": "",
"active": true,
"description": "Sample Flexible Content field group with two layouts — perfect for testing the renderer system.",
"show_in_rest": 0,
"local": "json"
}
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.