Learn how to style alternating ACF repeater rows with PHP modulo logic or CSS :nth-child(), with practical examples for WordPress themes.
EN

ACF Repeater Fields - How to Style Alternating Rows

5.00 /5 - (24 votes )
Last verified: May 1, 2026
10min read
Case study
Full-stack developer

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.

Learn more about professional WordPress development at WPPoland. But how do you style them? Especially if you want every second item to look different?

In practice, the quickest fix is to use a simple counter plus modulo logic when you need different classes or templates, or CSS :nth-child() when the change is purely visual.

If you only need alternating colours or spacing, start with CSS :nth-child(). If each repeater row needs different classes, markup, or conditional templates, use a PHP counter with the modulo operator inside the ACF loop.

#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.

Next step

Turn the article into an actual implementation

This block strengthens internal linking and gives readers the most relevant next move instead of leaving them at a dead end.

Want this implemented on your site?

If you want to convert the article into a working site improvement, redesign, or build plan, I can define the scope and implement it.

Related cluster

Explore other WordPress services and knowledge base

Strengthen your business with professional technical support in key areas of the WordPress ecosystem.

Article FAQ

Frequently Asked Questions

Practical answers to apply the topic in real execution.

SEO-ready GEO-ready AEO-ready 3 Q&A
What is the easiest way to alternate ACF repeater row styles?
The simplest option is usually CSS :nth-child() if you only need visual changes. Use PHP with a counter or modulo check when the markup or template logic also needs to change.
How do you check whether an ACF repeater row is even or odd?
Increment a counter inside the repeater loop and use $i % 2 === 0 for even rows and $i % 2 !== 0 for odd rows.
Should you use PHP or CSS for ACF repeater zebra striping?
Use CSS when the difference is purely presentational. Use PHP when you need different classes, templates, or conditional output per repeater row.

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

Let’s discuss

Related Articles

Stop writing messy if-statements. Learn the difference between in_category and has_term, how to handle recursive child categories efficiently, and optimize your conditional tags.
development

WordPress conditional logic for taxonomies

Stop writing messy if-statements. Learn the difference between in_category and has_term, how to handle recursive child categories efficiently, and optimize your conditional tags.

Practical notes on WP_Query: when get_posts beats new WP_Query, why meta_query on unindexed keys collapses at scale, and how to paginate custom loops without hitting 404s.
development

A working guide to WP_Query and the loop (2026 performance edition)

Practical notes on WP_Query: when get_posts beats new WP_Query, why meta_query on unindexed keys collapses at scale, and how to paginate custom loops without hitting 404s.

has_term() and is_tax() are often confused. See the complete guide to conditional logic for categories, tags, and custom taxonomies.
development

Check if a post belongs to a taxonomy term

has_term() and is_tax() are often confused. See the complete guide to conditional logic for categories, tags, and custom taxonomies.