How to alternate background colors in an ACF Repeater loop? Using $i % 2 variable to add CSS classes dynamically.
EN

ACF repeater fields - How to style unique rows?

5.00 /5 - (24 votes )
Last verified: March 1, 2026
Experience: 5+ years experience
Table of Contents

The Repeater Field in Advanced Custom Fields (ACF) is one of the most powerful features for developers. It allows clients to add an unlimited number of items (e.g., “Partners”, “Agenda”, “Ingredients”) without needing separate posts.

But how do you style them? Especially if you want every second item to look different?

Understanding ACF repeater fields

ACF Repeater Fields revolutionized WordPress content management by allowing flexible, repeatable content blocks. Instead of creating separate custom post types or posts for similar content, you can create a single field group that clients can populate dynamically.

Common Use Cases:

  • Team Members: List of team profiles with photos and bios
  • Testimonials: Customer reviews with names, photos, and quotes
  • FAQ Sections: Questions and answers
  • Timeline Items: Historical events or project milestones
  • Product Features: List of features with icons and descriptions
  • Pricing Tables: Multiple pricing tiers with different features

Basic ACF repeater loop

Here’s the fundamental structure for outputting repeater field data:

<?php if( have_rows('my_repeater') ): ?>
    <ul class="slides">
    <?php while( have_rows('my_repeater') ): the_row(); 
        $image = get_sub_field('image');
        $content = get_sub_field('text');
        $title = get_sub_field('title');
        ?>
        <li class="slide">
            <?php if( $image ): ?>
                <img src="<?php echo esc_url( $image['url'] ); ?>" 
                     alt="<?php echo esc_attr( $image['alt'] ); ?>" />
            <?php endif; ?>
            
            <?php if( $title ): ?>
                <h3><?php echo esc_html( $title ); ?></h3>
            <?php endif; ?>
            
            <?php if( $content ): ?>
                <p><?php echo wp_kses_post( $content ); ?></p>
            <?php endif; ?>
        </li>
    <?php endwhile; ?>
    </ul>
<?php else: ?>
    <p>No items found.</p>
<?php endif; ?>

Key Functions:

  • have_rows(): Checks if repeater has rows
  • the_row(): Moves to next row (like the_post())
  • get_sub_field(): Gets value of sub-field in current row
  • get_row_index(): Returns current row number (0-based)

Challenge: The “zebra” pattern

Designers often want every alternating row to have a dark background or different styling. This creates visual separation and improves readability.

PHP solution: Using modulo operator

In PHP, we use a counter variable ($i) and the modulo operator (%) to determine if a number is even or odd.

<?php if( have_rows('sections') ): 
    $i = 0; // Initialize counter
?>
    <div class="sections-container">
    <?php while( have_rows('sections') ): the_row(); 
        $i++; // Increment counter
        
        // Modulo operator: returns remainder of division
        // $i % 2 == 0 means even number (2nd, 4th, 6th...)
        // $i % 2 == 1 means odd number (1st, 3rd, 5th...)
        $is_even = ( $i % 2 == 0 );
        $class = $is_even ? 'bg-dark' : 'bg-light';
        
        // Alternative: More readable
        $class = ( $i % 2 === 0 ) ? 'even-row' : 'odd-row';
    ?>
        
        <div class="row <?php echo esc_attr( $class ); ?>">
            <div class="row-content">
                <?php 
                $title = get_sub_field('title');
                $content = get_sub_field('content');
                ?>
                
                <?php if( $title ): ?>
                    <h3><?php echo esc_html( $title ); ?></h3>
                <?php endif; ?>
                
                <?php if( $content ): ?>
                    <div class="content">
                        <?php echo wp_kses_post( $content ); ?>
                    </div>
                <?php endif; ?>
            </div>
        </div>

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

Understanding modulo operator

The modulo operator (%) returns the remainder of a division:

1 % 2 = 1  // 1 divided by 2 = 0 remainder 1 (odd)
2 % 2 = 0  // 2 divided by 2 = 1 remainder 0 (even)
3 % 2 = 1  // 3 divided by 2 = 1 remainder 1 (odd)
4 % 2 = 0  // 4 divided by 2 = 2 remainder 0 (even)

Pattern:

  • Even numbers: % 2 === 0
  • Odd numbers: % 2 === 1 (or !== 0)

Advanced styling patterns

Pattern 1: Every third item different

<?php if( have_rows('items') ): 
    $i = 0;
?>
    <div class="items-grid">
    <?php while( have_rows('items') ): the_row(); 
        $i++;
        $modulo = $i % 3;
        
        if ( $modulo === 0 ) {
            $class = 'highlight-item'; // 3rd, 6th, 9th...
        } elseif ( $modulo === 1 ) {
            $class = 'normal-item'; // 1st, 4th, 7th...
        } else {
            $class = 'secondary-item'; // 2nd, 5th, 8th...
        }
    ?>
        <div class="item <?php echo esc_attr( $class ); ?>">
            <!-- Content -->
        </div>
    <?php endwhile; ?>
    </div>
<?php endif; ?>

Pattern 2: First and last item special

<?php if( have_rows('items') ): 
    $i = 0;
    $total = count( get_field('items') );
?>
    <div class="items-list">
    <?php while( have_rows('items') ): the_row(); 
        $i++;
        $classes = array();
        
        if ( $i === 1 ) {
            $classes[] = 'first-item';
        }
        if ( $i === $total ) {
            $classes[] = 'last-item';
        }
        if ( $i % 2 === 0 ) {
            $classes[] = 'even-item';
        } else {
            $classes[] = 'odd-item';
        }
        
        $class_string = implode( ' ', $classes );
    ?>
        <div class="item <?php echo esc_attr( $class_string ); ?>">
            <!-- Content -->
        </div>
    <?php endwhile; ?>
    </div>
<?php endif; ?>

Pattern 3: Row index for complex logic

<?php if( have_rows('sections') ): ?>
    <div class="sections">
    <?php while( have_rows('sections') ): the_row(); 
        $row_index = get_row_index(); // 1-based index
        $row_number = $row_index - 1; // 0-based for calculations
        
        // Different template for first 3 items
        if ( $row_index <= 3 ) {
            get_template_part( 'template-parts/section', 'featured' );
        } else {
            get_template_part( 'template-parts/section', 'standard' );
        }
    ?>
    <?php endwhile; ?>
    </div>
<?php endif; ?>

CSS n-th child: The modern way

In 2026, if the styling change is purely visual (colors, padding, borders), CSS :nth-child() is often cleaner and more performant than PHP logic.

Basic nth-child patterns

/* Every even row (2nd, 4th, 6th...) */
.row:nth-child(even) {
    background-color: #f5f5f5;
}

/* Every odd row (1st, 3rd, 5th...) */
.row:nth-child(odd) {
}

/* Every 3rd item */
.item:nth-child(3n) {
    border-left: 3px solid #0073aa;
}

/* First 3 items */
.item:nth-child(-n+3) {
    font-weight: bold;
}

/* Every item after the 5th */
.item:nth-child(n+6) {
    opacity: 0.8;
}

Advanced CSS patterns

/* Zebra striping */
.sections-container .row:nth-child(even) {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: #fff;
}

.sections-container .row:nth-child(odd) {
    background: #fff;
    color: #333;
}

/* Hover effects on alternating rows */
.row:nth-child(even):hover {
    transform: translateX(10px);
}

.row:nth-child(odd):hover {
}

/* Different layouts */
.row:nth-child(3n+1) {
    grid-column: 1 / 3; /* Span 2 columns */
}

.row:nth-child(3n+2),
.row:nth-child(3n+3) {
}

When to use PHP vs CSS

Use PHP when:

  • Different HTML structure for odd/even rows
  • Loading different template parts
  • Conditional logic beyond styling (e.g., different fields)
  • Dynamic class names based on field values
  • Complex calculations (e.g., “every 5th item after the 10th”)

Use CSS when:

  • Pure visual styling (colors, spacing, borders)
  • Simple patterns (every 2nd, 3rd, etc.)
  • Performance matters (CSS is faster than PHP loops)
  • Responsive design (CSS media queries)

Complete example: Team Members grid

Here’s a complete, production-ready example:

<?php if( have_rows('team_members') ): ?>
    <div class="team-grid">
    <?php 
    $i = 0;
    while( have_rows('team_members') ): the_row(); 
        $i++;
        $name = get_sub_field('name');
        $role = get_sub_field('role');
        $photo = get_sub_field('photo');
        $bio = get_sub_field('bio');
        $email = get_sub_field('email');
        
        // Determine layout: featured members (first 3) get larger cards
        $is_featured = ( $i <= 3 );
        $card_class = $is_featured ? 'team-card featured' : 'team-card';
        $card_class .= ( $i % 2 === 0 ) ? ' even' : ' odd';
    ?>
        <article class="<?php echo esc_attr( $card_class ); ?>" data-index="<?php echo $i; ?>">
            <?php if( $photo ): ?>
                <div class="team-photo">
                    <img src="<?php echo esc_url( $photo['sizes']['medium'] ); ?>" 
                         alt="<?php echo esc_attr( $name ); ?>"
                         srcset="<?php echo esc_url( $photo['sizes']['medium'] ); ?> 300w,
                                 <?php echo esc_url( $photo['sizes']['large'] ); ?> 600w"
                         sizes="(max-width: 600px) 300px, 600px" />
                </div>
            <?php endif; ?>
            
            <div class="team-info">
                <?php if( $name ): ?>
                    <h3 class="team-name"><?php echo esc_html( $name ); ?></h3>
                <?php endif; ?>
                
                <?php if( $role ): ?>
                    <p class="team-role"><?php echo esc_html( $role ); ?></p>
                <?php endif; ?>
                
                <?php if( $bio ): ?>
                    <div class="team-bio">
                        <?php echo wp_kses_post( $bio ); ?>
                    </div>
                <?php endif; ?>
                
                <?php if( $email ): ?>
                    <a href="mailto:<?php echo esc_attr( $email ); ?>" class="team-email">
                        Contact
                    </a>
                <?php endif; ?>
            </div>
        </article>
    <?php endwhile; ?>
    </div>
<?php else: ?>
    <p class="no-items">No team members found.</p>
<?php endif; ?>

Accompanying CSS:

.team-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: 2rem;
    margin: 2rem 0;
}

.team-card {
    background: #fff;
    border-radius: 8px;
    overflow: hidden;
    box-shadow: 0 2px 8px rgba(0,0,0,0.1);
    transition: transform 0.3s, box-shadow 0.3s;
}

.team-card:hover {
    transform: translateY(-4px);
    box-shadow: 0 8px 16px rgba(0,0,0,0.2);
}

/* Featured cards (first 3) */
.team-card.featured {
    grid-column: span 2;
}

/* Zebra striping */
.team-card.even {
    background: #f8f9fa;
}

.team-card.odd {
}

/* Responsive: Featured cards become normal on mobile */
@media (max-width: 768px) {
    .team-card.featured {
        grid-column: span 1;
    }
}

Performance optimization

Cache repeater data

For repeater fields that don’t change often, cache the output:

function get_cached_repeater_output( $field_name ) {
    $cache_key = 'repeater_' . $field_name . '_' . get_the_ID();
    $output = get_transient( $cache_key );
    
    if ( false === $output ) {
        ob_start();
        // Your repeater loop here
        $output = ob_get_clean();
        set_transient( $cache_key, $output, HOUR_IN_SECONDS );
    }
    
    return $output;
}

Limit repeater items

If you have many items, consider pagination:

<?php 
$items = get_field('items');
$items_per_page = 12;
$current_page = get_query_var( 'paged' ) ? get_query_var( 'paged' ) : 1;
$offset = ( $current_page - 1 ) * $items_per_page;
$paginated_items = array_slice( $items, $offset, $items_per_page );

foreach ( $paginated_items as $item ):
    // Output item
endforeach;
?>

Best practices

1. Always escape output

// Good
echo esc_html( $title );
echo esc_url( $image['url'] );
echo wp_kses_post( $content );

// Bad
echo $title; // XSS vulnerability

2. Check if fields exist

if ( $image && ! empty( $image['url'] ) ) {
    // Use image
}

3. Use get_row_index() for debugging

$row_index = get_row_index();
error_log( "Processing row $row_index" );

4. Combine PHP and CSS

Use PHP for logic, CSS for styling:

// PHP: Add data attribute
<div class="item" data-index="<?php echo $i; ?>">
/* CSS: Style based on data attribute if needed */
.item[data-index="1"] {
    /* Special styling */
}

Troubleshooting

Problem: Counter not working

Solution: Ensure counter is initialized before the loop:

$i = 0; // Must be before while()
while( have_rows() ): 
    $i++;

Problem: CSS nth-child not working

Solution: Check for wrapper elements affecting nth-child:

/* If items are wrapped, target the wrapper's children */
.container > .item:nth-child(even) { }

Problem: Modulo returns wrong values

Solution: Use strict comparison:

// Good
if ( $i % 2 === 0 ) { }

// Avoid (loose comparison)
if ( $i % 2 == 0 ) { }

Summary

Styling ACF Repeater Fields requires understanding both PHP logic and CSS selectors. The modulo operator (%) is your friend for creating alternating patterns in PHP, while CSS :nth-child() is perfect for pure visual styling.

Key Takeaways:

  • Use modulo operator for PHP-based alternating patterns
  • Prefer CSS :nth-child() for visual-only changes
  • Combine both approaches for complex layouts
  • Always escape output for security
  • Cache repeater output for performance
  • Use get_row_index() for debugging and complex logic

In 2026, with modern CSS capabilities, prefer CSS solutions when possible, but don’t hesitate to use PHP when you need different HTML structures or complex conditional logic.

Article FAQ

Frequently Asked Questions

Practical answers to apply the topic in real execution.

SEO-ready GEO-ready AEO-ready 3 Q&A
What should you know about ACF repeater fields - How to style unique rows?
ACF repeater fields - How to style unique rows? is relevant when you want a more stable WordPress setup, better performance, and fewer production issues.
How do you implement ACF repeater fields - How to style unique rows?
Start with a baseline audit, define scope and constraints, then roll out improvements in small, testable steps.
Why is ACF repeater fields - How to style unique rows important?
The biggest gains usually come from technical quality, clear information structure, and regular verification.

Need an FAQ tailored to your industry and market? We can build one aligned with your business goals.

Let’s discuss

Related Articles