get_row_layout() returns the name of the current Flexible Content layout inside a have_rows() loop. The name is whatever you registered as the layout's "Name" in the field group editor (snake_case by convention). The two canonical patterns for dispatching off that name are template parts (cleanest at scale) and switch statements (fine for small handfuls of layouts). If get_row_layout returns nothing, you forgot the_row() or you are outside the loop entirely.
Jump to:
- The basic call
- Dispatch pattern 1: template parts
- Dispatch pattern 2: switch statement
- Mapping layout names to renderer callbacks
- Why get_row_layout might return nothing
- Reading the layout name without entering the loop
The basic call
Inside a have_rows() loop, after the_row(), get_row_layout() returns the active layout's name:
if ( have_rows( 'page_builder' ) ) {
while ( have_rows( 'page_builder' ) ) {
the_row();
$layout = get_row_layout();
// $layout === 'hero' or 'cta' or 'stat_row' etc.
}
}The name returned matches what you typed into the "Name" field in the layout's field group editor. Match-cased; use snake_case as your convention.
Dispatch pattern 1: template parts
This is the canonical agency pattern. Each layout has a matching PHP partial under template-parts/flexible-content/{layout-name}.php. The loop just maps layout name to partial:
if ( have_rows( 'page_builder' ) ) {
while ( have_rows( 'page_builder' ) ) {
the_row();
$layout = get_row_layout();
get_template_part( 'template-parts/flexible-content/' . $layout );
}
}The partial reads its own sub-fields via get_sub_field():
<?php
// template-parts/flexible-content/hero.php
$heading = get_sub_field( 'heading' );
$subheading = get_sub_field( 'subheading' );
?>
<section class="hero">
<h1><?php echo esc_html( $heading ); ?></h1>
<p class="lead"><?php echo esc_html( $subheading ); ?></p>
</section>Why template parts win at scale: adding a new layout means creating one PHP file. Removing one means deleting one file. No central switch statement to edit, no merge conflicts when two devs add layouts simultaneously. This is the pattern every agency project I have shipped in the last decade uses, covered in the broader context of Why Many Agencies Still Prefer ACF Over Gutenberg.
Dispatch pattern 2: switch statement
For small handfuls of layouts (three or four), inline dispatch is fine:
if ( have_rows( 'page_builder' ) ) {
while ( have_rows( 'page_builder' ) ) {
the_row();
switch ( get_row_layout() ) {
case 'hero':
$heading = get_sub_field( 'heading' );
printf( '<h1>%s</h1>', esc_html( $heading ) );
break;
case 'cta':
$text = get_sub_field( 'text' );
$url = get_sub_field( 'url' );
printf( '<a href="%s">%s</a>', esc_url( $url ), esc_html( $text ) );
break;
default:
// unknown layout
}
}
}The default case matters: if you change a layout name in the field group but forget to update the template, the default branch catches it silently. Log unknown layouts in dev so you notice:
default:
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( 'Unknown flex layout: ' . get_row_layout() );
}Mapping layout names to renderer callbacks
For projects where the layouts are programmatically defined (rare on agency work, common on more bespoke setups), an associative-array dispatch keeps things tidy:
$renderers = [
'hero' => 'render_hero',
'cta' => 'render_cta',
'stat_row' => 'render_stat_row',
];
if ( have_rows( 'page_builder' ) ) {
while ( have_rows( 'page_builder' ) ) {
the_row();
$layout = get_row_layout();
if ( isset( $renderers[ $layout ] ) && function_exists( $renderers[ $layout ] ) ) {
call_user_func( $renderers[ $layout ] );
}
}
}
function render_hero() {
$heading = get_sub_field( 'heading' );
printf( '<h1>%s</h1>', esc_html( $heading ) );
}I rarely use this pattern in practice; template parts are simpler and easier for new team members to find.
Why get_row_layout might return nothing
Three causes:
- You are outside the
have_rows()loop.get_row_layout()only works in row context. Calling it elsewhere returns false. - You called
have_rows()but forgotthe_row(). Withoutthe_row(), the row pointer never advances andget_row_layout()returns false. Covered in detail in ACF Flexible Content Loop Not Working? Here is the Fix. - The field is not a Flexible Content field.
get_row_layout()only returns a value for Flexible Content fields. For Repeater rows there is no layout name; the row is just an indexed position.
Quick diagnostic:
while ( have_rows( 'page_builder' ) ) {
the_row();
$layout = get_row_layout();
if ( ! $layout ) {
// Bail and figure out why
error_log( 'get_row_layout returned nothing' );
continue;
}
// ...
}Reading the layout name without entering the loop
If you want to know which layouts a Flexible Content field contains for a post WITHOUT actually rendering them, read the field as an array and pluck the acf_fc_layout keys:
$rows = get_field( 'page_builder', $post_id );
if ( $rows ) {
$layout_names = array_column( $rows, 'acf_fc_layout' );
// $layout_names === ['hero', 'cta', 'stat_row']
}Useful for analytics ("which layouts get used most often across the site?") and for cache invalidation logic ("if this page contains a pricing layout, clear the pricing cache too"). For the WP-CLI patterns to run audits like that across thousands of posts, see Using AI with WP-CLI for Faster WordPress Operations.
The acf_fc_layout key is also what you write when persisting Flexible Content data via update_field(), covered in How to Update ACF Fields Programmatically. Same key for both directions.
Sources
Authoritative references this article was fact-checked against.
- get_row_layout() function reference (Advanced Custom Fields docs)advancedcustomfields.com
- Flexible Content field (Advanced Custom Fields docs)advancedcustomfields.com





