You have an ACF Repeater field full of rows, slides, team members, a list of features, and now you need to print it in a theme template. A plain get_field() will not cut it: a Repeater needs a loop. This DevAnswers guide covers every practical way to query and display Repeater data, from the standard loop to nested repeaters and options pages. If custom fields are new to you, our guide to using custom fields in templates sets the scene.
The Basic Repeater Loop
You display a Repeater with two functions that mirror the standard WordPress loop. have_rows() checks whether the field has any rows, and the_row() steps to the next one. ACF designed them to feel native, much like have_posts() and the_post(). The examples below use the colon and endwhile form of the loop because it interleaves cleanly with HTML in a template, but the brace form works identically if you prefer it. Inside the loop, the_sub_field() prints a sub field from the current row:
<?php if ( have_rows('team_members') ) : ?>
<ul>
<?php while ( have_rows('team_members') ) : the_row(); ?>
<li><?php the_sub_field('name'); ?> - <?php the_sub_field('role'); ?></li>
<?php endwhile; ?>
</ul>
<?php endif; ?>
One rule to keep in mind: the_row() only exposes the current row to sub field functions, never a parent or child row. If you are not sure where this snippet belongs, our guide to adding code to WordPress shows the safe places to put it.
get_sub_field() vs the_sub_field()
the_sub_field() echoes a value immediately. get_sub_field() returns it instead, which is what you want whenever you need to store, test, or transform the value before printing it:
<?php while ( have_rows('team_members') ) : the_row();
$name = get_sub_field('name');
$role = get_sub_field('role');
if ( $role === 'Lead' ) {
echo '<strong>' . esc_html( $name ) . '</strong>';
} else {
echo esc_html( $name );
}
endwhile; ?>
In other words, the_sub_field('x') is essentially echo get_sub_field('x'), with one important safety difference. Since ACF 6.2.6, the echo functions like the_sub_field() escape their output by default, so the basic loop examples above are safe to print as-is. get_sub_field(), on the other hand, returns the value without escaping it for HTML, so you must escape it yourself before printing, wrapping text in esc_html() and URLs in esc_url() exactly as the example above and the options-page snippet later do. Skipping that step on a get_sub_field() value is the most common security slip in ACF templates: a sub field a contributor can edit is untrusted input, and printing it raw opens the door to stored XSS. Our guide to escaping ACF output covers the safe patterns in full.
Loop a Repeater with get_field()
The have_rows() loop is idiomatic, but you can also pull the entire Repeater as a plain array with get_field() and walk it with foreach. Each row is an associative array keyed by sub field name:
<?php
// DevAnswers.net
$members = get_field('team_members');
if ( $members ) {
foreach ( $members as $member ) {
echo esc_html( $member['name'] ) . ' - ' . esc_html( $member['role'] ) . '<br>';
}
}
This is handy when you need the rows outside a template, want to sort or filter them before printing, or are handing the data to another function or a REST response. A common case is building a JSON payload for a block or a front-end script: you fetch the rows once with get_field(), shape them in PHP, and pass a clean array along instead of echoing markup in the middle of a loop. Like the loop, get_field() returns formatted values by default; if you specifically need the raw stored values, pass false for its $format_value parameter. Reach for the array when a task genuinely calls for handling the rows as data, and keep the have_rows() loop for ordinary template output.
Nested Repeaters
A Repeater can contain another Repeater, and the loop nests the same way. Call have_rows() again with the child field name inside the parent’s the_row():
<?php while ( have_rows('sections') ) : the_row(); ?>
<h3><?php the_sub_field('section_title'); ?></h3>
<?php if ( have_rows('items') ) : ?>
<ul>
<?php while ( have_rows('items') ) : the_row(); ?>
<li><?php the_sub_field('item_label'); ?></li>
<?php endwhile; ?>
</ul>
<?php endif; ?>
<?php endwhile; ?>
Because the_row() always scopes sub field functions to the current row, the inner the_sub_field('item_label') reads from the child Repeater, not the parent. When a sub field is an image or a link, see our guide to getting an image URL from an ACF Repeater.
Repeaters on an Options Page
A Repeater does not have to live on a post. If you store one on an ACF options page, useful for headers, footers, or global settings, the data is saved globally in the wp_options table. To read it, pass 'option' as the second argument so ACF looks there instead of the current post:
<?php if ( have_rows('social_links', 'option') ) : ?>
<?php while ( have_rows('social_links', 'option') ) : the_row(); ?>
<a href="<?php echo esc_url( get_sub_field('url') ); ?>"><?php the_sub_field('network'); ?></a>
<?php endwhile; ?>
<?php endif; ?>
The same 'option' argument works with get_field() too, if you prefer the array approach for global data.
Common Pitfalls
When a Repeater loop prints nothing, the cause is almost always one of a short list of mistakes. Work through these before assuming ACF is at fault:
- Field name mismatch.
have_rows()takes the Repeater’s field name, not its label. A typo, or passing the label by mistake, returns no rows and no error. - The data really is empty.
get_field()on an empty Repeater returnsfalse, not an empty array, which is why theif ( $members )guard belongs before everyforeach. - Querying the wrong object. Outside the main loop (a widget, the footer, an options page) pass the post ID or
'option'as the second argument so ACF knows where to look. - Sub field called outside the loop.
the_sub_field()andget_sub_field()only work betweenthe_row()andendwhile; called anywhere else they return nothing.
Summary
Reach for the have_rows() and the_row() loop for everyday template output, and get_field() when you need the rows as an array to process. Nest loops by calling have_rows() again, target options pages with the 'option' argument, and escape every value on output. With those four patterns, any Repeater, however deeply structured, comes down to a familiar loop. If your rows include media, our guide on how to display an ACF image field explains how to handle the different return formats.
Comments