The template-parts pattern for ACF Flexible Content is the cleanest way to scale a page builder built on ACF. One PHP partial per layout, one tiny dispatcher loop in the page template, zero switch statements that grow over time. After many years of running this pattern on agency sites and directory sites at scale, the structure has settled into something that new team members learn in an hour and stays maintainable for years.
Jump to:
- The directory structure
- The dispatcher loop
- A representative layout partial
- Passing extra data into a partial
- Why this pattern wins at scale
- Variations: nested partials, shared helpers
- The convention every agency I have worked with eventually adopts
The directory structure
Inside your theme (or child theme), create a dedicated directory for the Flexible Content partials:
themes/your-theme/
├── functions.php
├── page.php
├── single.php
└── template-parts/
└── flexible-content/
├── hero.php
├── cta.php
├── stat-row.php
├── testimonials.php
├── pricing.php
├── content-feed.php
├── faq.php
└── cta-banner.php
The folder is named to match the ACF field's name (page_builder if that is what you registered, or flexible-content as a generic name). Each file is named for one layout, hyphen-separated. The file name maps directly from the layout name registered in the ACF field group via a simple snake-case to kebab-case convention if you want, or just match exactly.
I prefer hyphens in filenames because that is the WordPress convention for template files, and an underscore-to-hyphen conversion is a one-liner in the dispatcher. Consistency matters more than the specific choice.
The dispatcher loop
In the page template (page.php, or the appropriate single template), the entire page-builder render is:
<?php
if ( have_rows( 'page_builder' ) ) :
while ( have_rows( 'page_builder' ) ) :
the_row();
$layout = get_row_layout();
$partial = str_replace( '_', '-', $layout );
get_template_part( 'template-parts/flexible-content/' . $partial );
endwhile;
endif;That is the whole thing. Every layout the editor adds renders through its matching partial. Adding a new layout means creating one PHP file in the partials directory. Removing one means deleting one file. No code change to the dispatcher.
The str_replace line converts the ACF layout name (snake_case by convention) to the kebab-case filename WordPress prefers. If you keep both in the same case, the str_replace is unnecessary.
A representative layout partial
<?php
// template-parts/flexible-content/hero.php
$heading = get_sub_field( 'heading' );
$subheading = get_sub_field( 'subheading' );
$cta_text = get_sub_field( 'cta_text' );
$cta_url = get_sub_field( 'cta_url' );
$background_image = get_sub_field( 'background_image' );
?>
<section class="fc-hero relative py-24 md:py-32">
<?php if ( $background_image ) : ?>
<?php echo wp_get_attachment_image( $background_image, 'large', false, [
'class' => 'absolute inset-0 w-full h-full object-cover -z-10',
'loading' => 'eager',
'fetchpriority' => 'high',
] ); ?>
<?php endif; ?>
<div class="container mx-auto px-4 max-w-4xl">
<?php if ( $heading ) : ?>
<h1 class="text-4xl md:text-6xl font-bold tracking-tight">
<?php echo esc_html( $heading ); ?>
</h1>
<?php endif; ?>
<?php if ( $subheading ) : ?>
<p class="mt-6 text-lg md:text-xl text-slate-300">
<?php echo esc_html( $subheading ); ?>
</p>
<?php endif; ?>
<?php if ( $cta_text && $cta_url ) : ?>
<a href="<?php echo esc_url( $cta_url ); ?>" class="mt-8 inline-block btn-primary">
<?php echo esc_html( $cta_text ); ?>
</a>
<?php endif; ?>
</div>
</section>The partial reads its own sub-fields via get_sub_field(). No external dependencies, no globals to manage. The have_rows/the_row state is set by the dispatcher and inherits into the partial automatically because get_template_part runs in the same execution context.
Passing extra data into a partial
get_template_part accepts an optional $args array (WP 5.5+) that the partial can access:
// In the dispatcher
get_template_part( 'template-parts/flexible-content/' . $partial, null, [
'theme' => 'dark',
'container_size' => 'wide',
] );// In the partial
$args = $args ?? [];
$theme = $args['theme'] ?? 'light';
$container_size = $args['container_size'] ?? 'standard';I use this sparingly. Most layouts get all their config from sub-fields. The $args pattern is useful for cross-cutting concerns (theme variant, container width preference) that come from the page-level rather than the layout-level.
Why this pattern wins at scale
The benefits compound across project size:
- One file per layout. Searchable, grep-able, easy for new devs to find.
- Adding a layout is a one-file change. No merge conflicts on a central switch statement.
- Removing a layout is a one-file delete. No orphaned switch cases.
- Each partial is independently testable. Mock the sub-field values, render the partial, assert the output.
- The dispatcher loop never grows. The same six-line dispatcher works whether you have three layouts or thirty.
- Junior front-end devs onboard in an afternoon. "Style this partial" is a clear, scoped task.
This is what enables the agency-scale Flexible Content systems described in Why Many Agencies Still Prefer ACF Over Gutenberg. Without the template-parts pattern, the page-builder dispatcher becomes a hundred-line switch statement that no one wants to touch.
Variations: nested partials, shared helpers
For complex layouts (a "stat row" with a nested Repeater of stats, each stat with its own sub-fields), the partial can include nested partials:
<?php
// template-parts/flexible-content/stat-row.php
?>
<section class="fc-stat-row">
<?php if ( have_rows( 'stats' ) ) : ?>
<div class="grid grid-cols-2 md:grid-cols-4 gap-8">
<?php while ( have_rows( 'stats' ) ) : the_row(); ?>
<?php get_template_part( 'template-parts/flexible-content/_partials/stat-card' ); ?>
<?php endwhile; ?>
</div>
<?php endif; ?>
</section>The underscore-prefixed _partials/ subdirectory holds reusable sub-components that are not Flexible Content layouts themselves but are useful inside several layouts. This is a convention I use; WordPress does not enforce it.
For shared helpers (a function that produces the right class names based on a layout's theme variant), I keep them in a lib/flexible-content-helpers.php included from functions.php. Functions, not classes, because the consumption pattern is plain procedural code inside the partials.
The convention every agency I have worked with eventually adopts
After enough projects, every agency that runs Flexible Content settles into roughly this same structure: a template-parts/flexible-content/ directory, one PHP file per layout, a small dispatcher loop in the page template, ACF field group registration in mu-plugins/ so it travels with the project, and the field group's layouts named to match the partial filenames.
If you are starting a fresh project, just adopt the convention from day one. The cost is zero; the long-tail maintenance benefit is large. For the broader agency stack this fits into, see The Exact Stack I would Use to Run a Small WordPress Agency Today.
Sources
Authoritative references this article was fact-checked against.
- get_template_part() function reference (WordPress Developer Resources)developer.wordpress.org
- Flexible Content field (Advanced Custom Fields docs)advancedcustomfields.com





