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 rowsthe_row(): Moves to next row (likethe_post())get_sub_field(): Gets value of sub-field in current rowget_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.
