How to Use ACF Repeater Fields: Complete Guide with Examples

ACF Repeater Fields create flexible content structures allowing unlimited field groups within a single post through row-based data entry. From basic repeater loops with have_rows() and get_sub_field() to nested repeaters and dynamic JavaScript manipulation, repeater fields eliminate rigid content limitations. This comprehensive guide teaches repeater field setup, template loops, nested structures, programmatic field registration, AJAX operations, and advanced techniques creating dynamic ACF-powered WordPress content.

What Are Repeater Fields?

Repeater Field Concept:

Repeater fields (ACF PRO feature) allow users to create sets of sub-fields that can be repeated multiple times:

  • Team members with name, photo, bio
  • Testimonials with quote, author, rating
  • FAQ sections with question, answer
  • Product features with icon, title, description
  • Gallery items with image, caption, link

Field Structure:

Repeater Field: "team_members"
├── Sub-field: "name" (Text)
├── Sub-field: "position" (Text)
├── Sub-field: "photo" (Image)
└── Sub-field: "bio" (Textarea)

User can add unlimited rows of these 4 fields

Creating Repeater Fields

Add Repeater via ACF UI:

  1. Custom Fields → Field Groups
  2. Add Field
  3. Field Type: Repeater (ACF PRO)
  4. Configuration:
    • Field Label: Team Members
    • Field Name: team_members
    • Sub Fields: Add multiple sub-fields
    • Button Label: “Add Team Member”
    • Min/Max: Set limits (optional)
    • Layout: Block, Table, or Row

Sub-Field Configuration:

Add sub-fields within repeater:

  • Name (Text field)
  • Position (Text field)
  • Photo (Image field)
  • Bio (Textarea field)

Repeater Layouts:

  • Block: Stack fields vertically
  • Table: Display in table format
  • Row: Horizontal layout

Basic Repeater Loop

Display Repeater Data:

<?php
if (have_rows('team_members')) :
    ?>
    <div class="team-section">
        <h2>Our Team</h2>
        <div class="team-grid">
            <?php while (have_rows('team_members')) : the_row();
                // Get sub-field values
                $name = get_sub_field('name');
                $position = get_sub_field('position');
                $photo = get_sub_field('photo');
                $bio = get_sub_field('bio');
                ?>

                <div class="team-member">
                    <?php if ($photo) : ?>
                        <div class="member-photo">
                            <img src="<?php echo esc_url($photo['sizes']['medium']); ?>"
                                 alt="<?php echo esc_attr($name); ?>" />
                        </div>
                    <?php endif; ?>

                    <h3 class="member-name"><?php echo esc_html($name); ?></h3>

                    <?php if ($position) : ?>
                        <p class="member-position"><?php echo esc_html($position); ?></p>
                    <?php endif; ?>

                    <?php if ($bio) : ?>
                        <div class="member-bio">
                            <?php echo wp_kses_post(wpautop($bio)); ?>
                        </div>
                    <?php endif; ?>
                </div>

            <?php endwhile; ?>
        </div>
    </div>
<?php endif; ?>

Repeater Functions

Key Functions:

// Check if repeater has rows
if (have_rows('field_name')) {
    // Rows exist
}

// Loop through rows
while (have_rows('field_name')) : the_row();
    // Access sub-fields
endwhile;

// Get sub-field value
$value = get_sub_field('sub_field_name');

// Get row count
$count = count(get_field('field_name'));

// Get row index (0-based)
$index = get_row_index();

FAQ Repeater Example

FAQ Section with Accordion:

<?php
if (have_rows('faq_items')) :
    ?>
    <div class="faq-section">
        <h2>Frequently Asked Questions</h2>
        <div class="faq-accordion">
            <?php
            $faq_index = 0;
            while (have_rows('faq_items')) : the_row();
                $question = get_sub_field('question');
                $answer = get_sub_field('answer');
                $faq_index++;
                ?>

                <div class="faq-item">
                    <button class="faq-question"
                            aria-expanded="false"
                            aria-controls="faq-<?php echo esc_attr($faq_index); ?>">
                        <?php echo esc_html($question); ?>
                        <span class="faq-icon">+</span>
                    </button>

                    <div id="faq-<?php echo esc_attr($faq_index); ?>"
                         class="faq-answer"
                         hidden>
                        <?php echo wp_kses_post(wpautop($answer)); ?>
                    </div>
                </div>

            <?php endwhile; ?>
        </div>
    </div>
<?php endif; ?>

JavaScript for Accordion:

document.querySelectorAll(".faq-question").forEach(function (button) {
    button.addEventListener("click", function () {
        const expanded = this.getAttribute("aria-expanded") === "true";
        const answer = this.nextElementSibling;

        this.setAttribute("aria-expanded", !expanded);
        answer.hidden = expanded;
        this.querySelector(".faq-icon").textContent = expanded ? "+" : "−";
    });
});

Nested Repeaters

Repeater Within Repeater:

<?php
// Parent repeater: Departments
if (have_rows('departments')) :
    while (have_rows('departments')) : the_row();
        $dept_name = get_sub_field('department_name');
        ?>

        <div class="department">
            <h2><?php echo esc_html($dept_name); ?></h2>

            <?php
            // Child repeater: Employees
            if (have_rows('employees')) :
                ?>
                <div class="employees">
                    <?php while (have_rows('employees')) : the_row();
                        $emp_name = get_sub_field('employee_name');
                        $emp_role = get_sub_field('employee_role');
                        ?>

                        <div class="employee">
                            <h3><?php echo esc_html($emp_name); ?></h3>
                            <p><?php echo esc_html($emp_role); ?></p>
                        </div>

                    <?php endwhile; ?>
                </div>
            <?php endif; ?>
        </div>

    <?php endwhile;
endif;
?>

Slider Repeater Example

Image Slider with Repeater:

<?php
if (have_rows('slider_images')) :
    ?>
    <div class="image-slider">
        <div class="slider-container">
            <?php
            $slide_index = 0;
            while (have_rows('slider_images')) : the_row();
                $image = get_sub_field('slide_image');
                $caption = get_sub_field('slide_caption');
                $link = get_sub_field('slide_link');
                $slide_index++;
                ?>

                <div class="slide" data-slide="<?php echo esc_attr($slide_index); ?>">
                    <?php if ($link) : ?>
                        <a href="<?php echo esc_url($link); ?>">
                    <?php endif; ?>

                    <?php if ($image) : ?>
                        <img src="<?php echo esc_url($image['sizes']['large']); ?>"
                             alt="<?php echo esc_attr($image['alt']); ?>" />
                    <?php endif; ?>

                    <?php if ($link) : ?>
                        </a>
                    <?php endif; ?>

                    <?php if ($caption) : ?>
                        <div class="slide-caption">
                            <?php echo esc_html($caption); ?>
                        </div>
                    <?php endif; ?>
                </div>

            <?php endwhile; ?>
        </div>

        <button class="slider-prev" aria-label="Previous slide">‹</button>
        <button class="slider-next" aria-label="Next slide">›</button>
    </div>
<?php endif; ?>

Programmatic Repeater Registration

Register Repeater via PHP:

function mytheme_register_repeater_fields() {
    if (function_exists('acf_add_local_field_group')) {
        acf_add_local_field_group(array(
            'key'    => 'group_testimonials',
            'title'  => 'Testimonials',
            'fields' => array(
                array(
                    'key'   => 'field_testimonials',
                    'label' => 'Testimonials',
                    'name'  => 'testimonials',
                    'type'  => 'repeater',
                    'layout' => 'block',
                    'button_label' => 'Add Testimonial',
                    'sub_fields' => array(
                        array(
                            'key'   => 'field_testimonial_quote',
                            'label' => 'Quote',
                            'name'  => 'quote',
                            'type'  => 'textarea',
                        ),
                        array(
                            'key'   => 'field_testimonial_author',
                            'label' => 'Author',
                            'name'  => 'author',
                            'type'  => 'text',
                        ),
                        array(
                            'key'   => 'field_testimonial_rating',
                            'label' => 'Rating',
                            'name'  => 'rating',
                            'type'  => 'number',
                            'min'   => 1,
                            'max'   => 5,
                        ),
                        array(
                            'key'   => 'field_testimonial_photo',
                            'label' => 'Photo',
                            'name'  => 'photo',
                            'type'  => 'image',
                            'return_format' => 'array',
                        ),
                    ),
                ),
            ),
            'location' => array(
                array(
                    array(
                        'param'    => 'post_type',
                        'operator' => '==',
                        'value'    => 'page',
                    ),
                ),
            ),
        ));
    }
}
add_action('acf/init', 'mytheme_register_repeater_fields');

Accessing Repeater Data Outside Loop

Get All Repeater Rows:

<?php
// Get repeater field as array
$team_members = get_field('team_members');

if ($team_members) :
    ?>
    <div class="team-list">
        <?php foreach ($team_members as $member) : ?>
            <div class="member">
                <h3><?php echo esc_html($member['name']); ?></h3>
                <p><?php echo esc_html($member['position']); ?></p>
            </div>
        <?php endforeach; ?>
    </div>
<?php endif; ?>

Conditional Display Based on Row Count

Show Different Layouts:

<?php
$features = get_field('product_features');
$feature_count = $features ? count($features) : 0;

if ($feature_count > 0) :
    // Choose layout based on count
    $grid_class = $feature_count <= 3 ? 'grid-three' : 'grid-four';
    ?>

    <div class="features <?php echo esc_attr($grid_class); ?>">
        <?php foreach ($features as $feature) : ?>
            <div class="feature">
                <?php if ($feature['icon']) : ?>
                    <img src="<?php echo esc_url($feature['icon']['url']); ?>"
                         alt="" class="feature-icon" />
                <?php endif; ?>

                <h3><?php echo esc_html($feature['title']); ?></h3>
                <p><?php echo esc_html($feature['description']); ?></p>
            </div>
        <?php endforeach; ?>
    </div>

<?php endif; ?>

Filtering and Sorting Repeater Data

Custom Order and Filters:

<?php
$team_members = get_field('team_members');

if ($team_members) :
    // Sort by name alphabetically
    usort($team_members, function($a, $b) {
        return strcmp($a['name'], $b['name']);
    });

    // Filter only featured members
    $featured_members = array_filter($team_members, function($member) {
        return isset($member['is_featured']) && $member['is_featured'];
    });

    foreach ($featured_members as $member) :
        ?>
        <div class="member featured">
            <h3><?php echo esc_html($member['name']); ?></h3>
        </div>
    <?php endforeach;
endif;
?>

AJAX Add/Remove Rows

Dynamic Row Management:

// JavaScript to add row via AJAX
jQuery(document).ready(function ($) {
    $("#add-testimonial").on("click", function (e) {
        e.preventDefault();

        $.ajax({
            url: ajaxurl,
            type: "POST",
            data: {
                action: "add_repeater_row",
                post_id: $("#post_id").val(),
                field_key: "field_testimonials",
                nonce: $("#testimonial_nonce").val()
            },
            success: function (response) {
                if (response.success) {
                    location.reload();
                }
            }
        });
    });
});

PHP Handler:

function mytheme_add_repeater_row() {
    check_ajax_referer('testimonial_nonce', 'nonce');

    $post_id = absint($_POST['post_id']);
    $field_key = sanitize_text_field($_POST['field_key']);

    if (!current_user_can('edit_post', $post_id)) {
        wp_send_json_error('Insufficient permissions');
    }

    $rows = get_field($field_key, $post_id);
    $rows[] = array(
        'quote'  => '',
        'author' => '',
        'rating' => 5,
    );

    update_field($field_key, $rows, $post_id);

    wp_send_json_success();
}
add_action('wp_ajax_add_repeater_row', 'mytheme_add_repeater_row');

Best Practices

Always Check for Rows:

// Good
if (have_rows('field_name')) {
    while (have_rows('field_name')) : the_row();
        // Display content
    endwhile;
}

// Bad - no check, may cause errors
while (have_rows('field_name')) : the_row();
    // Display content
endwhile;

Escape All Output:

<?php echo esc_html(get_sub_field('text_field')); ?>
<?php echo esc_url(get_sub_field('url_field')); ?>
<?php echo esc_attr(get_sub_field('attribute_field')); ?>

Use Row Index for Unique IDs:

<?php while (have_rows('items')) : the_row(); ?>
    <div id="item-<?php echo esc_attr(get_row_index()); ?>">
        <?php the_sub_field('content'); ?>
    </div>
<?php endwhile; ?>

Conclusion

ACF Repeater Fields provide unlimited content flexibility through row-based field groups using have_rows() loops and get_sub_field() retrieval. Create nested repeaters for complex hierarchies, register fields programmatically with acf_add_local_field_group(), manipulate repeater data as arrays for sorting and filtering, and implement AJAX operations for dynamic row management. Repeater fields eliminate rigid content structures enabling editors to add unlimited team members, testimonials, FAQ items, features, and custom content patterns maintaining consistent data structure.

  1. ACF Repeater Field Documentation
  2. have_rows() Function
  3. get_sub_field() Reference
  4. ACF PRO Features
  5. add_row() Function

Call to Action

Custom field configurations need protection. Backup Copilot Pro backs up your WordPress ACF repeater field settings and content automatically. Safeguard your flexible content structures—start your free 30-day trial today!