ACF Repeater Fields - How to Style Alternating Rows

ACF Repeater Fields - How to Style Alternating Rows

Last verified: October 13, 2026
18 min read
Case study
Full-stack developer

The Repeater Field in Advanced Custom Fields (ACF Pro) provides developers with a structured mechanism for managing repeated content records. Instead of registering dozens of separate post types or cluttering post bodies with unstructured HTML tables, editors can manage repeatable entries such as team rosters, product specifications, complex pricing matrices, and event schedules directly from the WordPress administrative edit screen.

Learn more about professional WordPress development services at WPPoland. However, styling repeated elements introduces distinct architectural challenges. Designers frequently require zebra striping with alternating background shades, alternating two-column layouts where image and text columns swap positions on consecutive rows, prominent callouts on every third item, or custom visual treatments reserved strictly for the opening and concluding entries of a list.

In practice, developers must choose between client-side presentation using modern CSS selectors like :nth-child() and server-side evaluation using PHP counters, get_row_index(), and the modulo operator (%). In addition, scaling repeater implementations requires deep knowledge of WordPress internals: deeply nested repeaters create hundreds of records in the wp_postmeta table, degrade database performance, and require targeted caching strategies or migration toward ACF Blocks with InnerBlocks and Block Context.

#Database architecture and wp_postmeta storage mechanics

Before writing rendering logic in theme templates, developers must understand how Advanced Custom Fields persists repeater field data within MySQL. WordPress employs an entity-attribute-value schema for post metadata, storing custom fields as individual key-value rows inside the wp_postmeta database table.

When an ACF repeater field named project_milestones contains two subfields (milestone_title and milestone_summary), and an editor saves four entries on a page, ACF creates the following database footprint:

  1. A primary count row with meta_key = 'project_milestones' and meta_value = '4'.
  2. Dedicated rows for iteration zero: project_milestones_0_milestone_title and project_milestones_0_milestone_summary.
  3. Dedicated rows for iteration one: project_milestones_1_milestone_title and project_milestones_1_milestone_summary.
  4. System reference rows prefixed with an underscore storing ACF field keys such as _project_milestones_0_milestone_title mapping to field_64fa12b8.

For a repeater containing four subfields populated with twenty rows, ACF generates over 160 individual records in wp_postmeta for a single post. Understanding this underlying storage format explains why querying, looping, and styling large repeaters can cause database performance bottlenecks if handled incorrectly.

#Core loop functions: have_rows, the_row, and get_sub_field

The standard ACF rendering workflow relies on have_rows() and the_row() to iterate through repeater items safely.

<?php if ( have_rows( 'project_milestones' ) ) : ?>
    <section class="milestones-timeline" aria-label="<?php esc_attr_e( 'Project Milestones', 'wppoland' ); ?>">
        <div class="milestones-container">
            <?php 
            while ( have_rows( 'project_milestones' ) ) : 
                the_row(); 
                $title   = get_sub_field( 'milestone_title' );
                $summary = get_sub_field( 'milestone_summary' );
                $image   = get_sub_field( 'milestone_image' );
            ?>
                <article class="milestone-card">
                    <?php if ( ! empty( $image ) && is_array( $image ) ) : ?>
                        <div class="milestone-media">
                            <?php echo wp_get_attachment_image( (int) $image['id'], 'large', false, [ 'class' => 'milestone-thumbnail' ] ); ?>
                        </div>
                    <?php endif; ?>

                    <div class="milestone-content">
                        <?php if ( ! empty( $title ) ) : ?>
                            <h3 class="milestone-title"><?php echo esc_html( $title ); ?></h3>
                        <?php endif; ?>

                        <?php if ( ! empty( $summary ) ) : ?>
                            <div class="milestone-description">
                                <?php echo wp_kses_post( $summary ); ?>
                            </div>
                        <?php endif; ?>
                    </div>
                </article>
            <?php endwhile; ?>
        </div>
    </section>
<?php endif; ?>

The function the_row() advances ACF internal row pointer and configures the row context. Inside this loop, developers must always call get_sub_field() rather than get_field(). Calling get_field() inside a repeater causes ACF to fall back to the global post context, either returning null or fetching unrelated top-level post data.

#Server-side row targeting using get_row_index and modulo logic

When repeater items require structural markup variations, dynamic data attributes, or distinct template parts, server-side evaluation in PHP provides deterministic control.

#How get_row_index functions in ACF

Instead of declaring manual counter variables outside the loop, use the native ACF function get_row_index(). This helper returns the current row position as a 1-based index (the first row returns 1, the second returns 2).

<?php if ( have_rows( 'process_steps' ) ) : ?>
    <ol class="process-step-list">
        <?php 
        while ( have_rows( 'process_steps' ) ) : 
            the_row(); 
            $step_number = get_row_index();
            $is_even     = ( $step_number % 2 === 0 );
            $row_class   = $is_even ? 'step-layout-reversed step-even' : 'step-layout-standard step-odd';
        ?>
            <li class="process-step-item <?php echo esc_attr( $row_class ); ?>" data-step="<?php echo (int) $step_number; ?>">
                <span class="step-badge" aria-hidden="true"><?php echo esc_html( sprintf( '%02d', $step_number ) ); ?></span>
                <div class="step-body">
                    <h4><?php echo esc_html( get_sub_field( 'step_heading' ) ); ?></h4>
                    <p><?php echo esc_html( get_sub_field( 'step_detail' ) ); ?></p>
                </div>
            </li>
        <?php endwhile; ?>
    </ol>
<?php endif; ?>

#Alternating even and odd rows with the modulo operator

The PHP modulo operator (%) calculates the integer remainder of division. Evaluating $step_number % 2 === 0 identifies even rows (2nd, 4th, 6th), while $step_number % 2 !== 0 targets odd rows (1st, 3rd, 5th). This technique is essential when swapping flexbox columns or outputting conditional microdata.

#Highlighting every third item in grid layouts

For three-column catalog displays or recurring promo ribbons, evaluating $step_number % 3 === 0 targets the 3rd, 6th, and 9th entries:

$modulo_three = $step_number % 3;
if ( $modulo_three === 0 ) {
    $column_class = 'card-prominent card-tertiary';
} elseif ( $modulo_three === 1 ) {
    $column_class = 'card-regular card-primary';
} else {
    $column_class = 'card-regular card-secondary';
}

#Targeting first and last repeater items safely

Applying special badges or removing bottom border separators on terminal rows requires comparing the current index against total row count:

<?php 
$raw_rows = get_field( 'team_members' );
$total_rows = is_array( $raw_rows ) ? count( $raw_rows ) : 0;

if ( have_rows( 'team_members' ) ) : 
?>
    <div class="team-roster">
        <?php 
        while ( have_rows( 'team_members' ) ) : 
            the_row(); 
            $current_index = get_row_index();
            $classes       = [ 'team-member-node' ];

            if ( $current_index === 1 ) {
                $classes[] = 'node-first-item';
            }
            if ( $current_index === $total_rows ) {
                $classes[] = 'node-final-item';
            }
            if ( $current_index % 2 === 0 ) {
                $classes[] = 'node-zebra-even';
            } else {
                $classes[] = 'node-zebra-odd';
            }
        ?>
            <div class="<?php echo esc_attr( implode( ' ', $classes ) ); ?>">
                <p class="member-name"><?php echo esc_html( get_sub_field( 'member_name' ) ); ?></p>
                <p class="member-role"><?php echo esc_html( get_sub_field( 'member_role' ) ); ?></p>
            </div>
        <?php endwhile; ?>
    </div>
<?php endif; ?>

#Subfield logic and layout variation techniques

Beyond numeric row indexes, template designs often depend on data entered within specific subfields. Combining row counters with subfield values enables dynamic layout switching.

<?php if ( have_rows( 'case_study_sections' ) ) : ?>
    <div class="case-study-flow">
        <?php 
        while ( have_rows( 'case_study_sections' ) ) : 
            the_row(); 
            $row_index     = get_row_index();
            $layout_style  = get_sub_field( 'display_variant' ); // Dropdown: 'hero', 'split', 'compact'
            $custom_accent = get_sub_field( 'brand_accent' );   // Text: hex color or theme token
            
            // Generate deterministic container classes
            $wrapper_classes = [
                'case-section',
                'section-row-' . $row_index,
                'section-variant-' . sanitize_html_class( $layout_style ?: 'standard' )
            ];

            if ( $row_index % 2 === 0 ) {
                $wrapper_classes[] = 'section-order-reversed';
            }
        ?>
            <section 
                class="<?php echo esc_attr( implode( ' ', $wrapper_classes ) ); ?>"
                style="<?php echo $custom_accent ? esc_attr( '--row-accent: ' . sanitize_hex_color( $custom_accent ) . ';' ) : ''; ?>"
            >
                <?php 
                if ( $layout_style === 'hero' && $row_index === 1 ) {
                    get_template_part( 'template-parts/case-study/variant', 'hero' );
                } else {
                    get_template_part( 'template-parts/case-study/variant', 'split' );
                }
                ?>
            </section>
        <?php endwhile; ?>
    </div>
<?php endif; ?>

By passing layout values through sanitize_html_class() and assigning CSS custom properties, your markup remains flexible, secure, and decoupled from rigid styling rules.

#Presentational styling with CSS nth-child selectors

When alternating rows require only visual changes like background shading, margin adjustments, border styling, or flexbox direction reversal, writing classes in PHP creates unnecessary markup weight. Modern CSS :nth-child() selectors handle presentational variations cleanly.

/* Standard zebra striping */
.milestones-container > .milestone-card:nth-child(even) {
    background-color: #f8fafc;
    border-color: #e2e8f0;
}

.milestones-container > .milestone-card:nth-child(odd) {
    background-color: #ffffff;
    border-color: #cbd5e1;
}

/* Reversing two-column layout on alternating rows */
.milestones-container > .milestone-card {
    display: flex;
    flex-direction: row;
    gap: 2rem;
    align-items: center;
}

.milestones-container > .milestone-card:nth-child(even) {
    flex-direction: row-reverse;
}

/* Highlighting every third card in CSS grid */
.catalog-grid > .catalog-card:nth-child(3n) {
    grid-column: span 2;
    border: 2px solid #2563eb;
    box-shadow: 0 10px 25px -5px rgba(37, 99, 235, 0.15);
}

/* Styling opening and concluding items */
.milestones-container > .milestone-card:first-child {
    border-top: 4px solid #0f172a;
}

.milestones-container > .milestone-card:last-child {
    border-bottom: 4px solid #0f172a;
}

Using CSS pseudo-classes separates styling concerns from backend template files, reduces HTML payload size, and allows stylesheets to adjust responsive behavior without modifying PHP code.

#Evaluation framework: choosing PHP logic or modern CSS

Architectural discipline requires knowing when to implement logic in PHP and when to delegate formatting to CSS:

Requirement scenarioRecommended implementationTechnical rationale
Alternating background colors or bordersPure CSS :nth-child()Zero HTML overhead, cleaner templates, responsive adaptation.
Reversing flexbox or CSS grid item directionsPure CSS :nth-child(even)Media queries can disable reversal on mobile viewports seamlessly.
Changing semantic HTML tags such as aside vs sectionPHP modulo evaluationCSS cannot alter DOM elements or semantic document hierarchy.
Loading different PHP template part partialsPHP conditional branchingget_template_part() must execute on the server during rendering.
Rendering structured JSON-LD position propertiesPHP get_row_index()Search engine schema requires numeric values embedded in output.
User-selectable row color scheme dropdownsCombined PHP and CSS custom propertiesPHP injects CSS variables, while CSS controls layout application.

#ACF Flexible Content integration for heterogeneous layouts

When repeated content requires substantially different data fields rather than minor styling variations, pushing multiple fields into a single repeater creates cluttered administrative screens. The ACF Flexible Content field offers a cleaner architecture by allowing editors to insert distinct layout blocks on demand.

<?php if ( have_rows( 'modular_page_builder' ) ) : ?>
    <div class="page-modules-stream">
        <?php 
        while ( have_rows( 'modular_page_builder' ) ) : 
            the_row(); 
            $layout_name = get_row_layout();
            $row_index   = get_row_index();
            $wrapper_id  = 'module-' . $row_index . '-' . sanitize_html_class( $layout_name );
        ?>
            <section id="<?php echo esc_attr( $wrapper_id ); ?>" class="module-block module-<?php echo esc_attr( $layout_name ); ?>" data-row="<?php echo (int) $row_index; ?>">
                <?php 
                if ( $layout_name === 'hero_banner' ) :
                    get_template_part( 'template-parts/modules/hero', null, [ 'index' => $row_index ] );
                elseif ( $layout_name === 'feature_repeater' ) :
                    get_template_part( 'template-parts/modules/features', null, [ 'index' => $row_index ] );
                elseif ( $layout_name === 'testimonials_carousel' ) :
                    get_template_part( 'template-parts/modules/testimonials', null, [ 'index' => $row_index ] );
                endif; 
                ?>
            </section>
        <?php endwhile; ?>
    </div>
<?php endif; ?>

Using get_row_layout() inside Flexible Content loops provides cleaner separation of concerns. Each layout type maintains its own field definitions, preventing empty database cells and avoiding giant monolithic repeaters.

#Gutenberg ACF blocks with inner blocks and block context

WordPress block theme architectures leverage ACF Blocks via acf_register_block_type() or block.json. Instead of writing monolithic templates that iterate over rigid repeater rows, modern WordPress engineering implements modular container blocks with InnerBlocks and Block Context.

#Registering container and item blocks via block.json

Defining custom blocks using block.json establishes native integration with Gutenberg editor workflows:

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "wppoland/milestone-card",
  "title": "Milestone Card Item",
  "category": "theme",
  "parent": [ "wppoland/milestones-container" ],
  "usesContext": [ "wppoland/containerLayout", "wppoland/accentPalette" ],
  "acf": {
    "mode": "preview",
    "renderTemplate": "render.php"
  },
  "supports": {
    "anchor": true,
    "customClassName": true
  }
}

#Rendering block context and inner blocks

In the parent container block render template, use <InnerBlocks /> with an allowed blocks constraint:

<?php
/**
 * Parent Block Render Template: milestones-container/render.php
 *
 * @var array $block The block settings and attributes.
 */
$allowed_inner_blocks = [ 'wppoland/milestone-card' ];
$template_preset      = [
    [ 'wppoland/milestone-card', [ 'placeholder' => 'First milestone details...' ] ],
    [ 'wppoland/milestone-card', [ 'placeholder' => 'Second milestone details...' ] ],
];
?>
<div class="milestones-block-wrapper <?php echo esc_attr( $block['className'] ?? '' ); ?>">
    <InnerBlocks 
        allowedBlocks="<?php echo esc_attr( wp_json_encode( $allowed_inner_blocks ) ); ?>" 
        template="<?php echo esc_attr( wp_json_encode( $template_preset ) ); ?>"
        templateLock={false}
    />
</div>

In the child block render template (milestone-card/render.php), read inherited context without querying the parent post:

<?php
/**
 * Child Block Render Template: milestone-card/render.php
 *
 * @var array $block The block settings and attributes.
 * @var array $context The block context passed from parents.
 */
$container_layout = $context['wppoland/containerLayout'] ?? 'grid';
$accent_palette   = $context['wppoland/accentPalette'] ?? 'default';

$card_title   = get_field( 'card_title' );
$card_details = get_field( 'card_details' );
?>
<div class="milestone-block-item layout-<?php echo esc_attr( $container_layout ); ?> palette-<?php echo esc_attr( $accent_palette ); ?>">
    <h4><?php echo esc_html( $card_title ); ?></h4>
    <div class="card-details">
        <?php echo wp_kses_post( $card_details ); ?>
    </div>
</div>

By leveraging Block Context, child items automatically inherit display configurations, allowing editors to rearrange, style, and nest modular items directly inside the Gutenberg canvas.

#Performance bottlenecks of deeply nested repeaters in wp_postmeta

Deeply nested repeaters (e.g. a repeater inside another repeater) create multiplicative records in the wp_postmeta table. If an outer repeater has 15 rows, and each row contains an inner repeater with 10 rows holding 3 subfields, saving the post produces over 500 metadata entries.

#The performance cost of postmeta bloat

  1. Slow post save operations: Every update issues individual SQL queries or batched operations that lock tables and invalidate post caches.
  2. Object cache bloat: Persistent object cache systems like Redis or Memcached serialize the entire post metadata array into a single cache bucket. Storing hundreds of kilobytes of nested repeater data increases memory footprints and serialization overhead.
  3. Database query degradation: Running WP_Query queries with meta_query filters against tables saturated with millions of repeater subkeys degrades database index efficiency.

#Caching repeater output with WordPress Transients API

When complex repeaters render expensive markup, cache the generated HTML using transients to bypass loop processing during frontend requests:

function wppoland_get_rendered_repeater_timeline( int $post_id ): string {
    $cache_key = 'wppoland_timeline_render_' . $post_id;
    $cached_output = get_transient( $cache_key );

    if ( false !== $cached_output ) {
        return $cached_output;
    }

    if ( ! have_rows( 'project_milestones', $post_id ) ) {
        return '';
    }

    ob_start();
    ?>
    <div class="cached-timeline-stream">
        <?php 
        while ( have_rows( 'project_milestones', $post_id ) ) : 
            the_row(); 
            $index = get_row_index();
        ?>
            <article class="timeline-row <?php echo ( $index % 2 === 0 ) ? 'timeline-row-even' : 'timeline-row-odd'; ?>">
                <h3><?php echo esc_html( get_sub_field( 'milestone_title' ) ); ?></h3>
                <div><?php echo wp_kses_post( get_sub_field( 'milestone_summary' ) ); ?></div>
            </article>
        <?php endwhile; ?>
    </div>
    <?php
    $cached_output = ob_get_clean();

    // Cache compiled HTML for 12 hours (43200 seconds)
    set_transient( $cache_key, $cached_output, 43200 );

    return $cached_output;
}

// Invalidate cache immediately when the post is updated
add_action( 'save_post', function( int $post_id ): void {
    if ( ! wp_is_post_revision( $post_id ) && ! wp_is_post_autosave( $post_id ) ) {
        delete_transient( 'wppoland_timeline_render_' . $post_id );
    }
} );

#High-speed batch array processing with get_field

When dealing with large repeaters where object caching is unavailable, fetching the entire dataset into memory via get_field() avoids repetitive calls to the_row() and get_sub_field():

$milestones = get_field( 'project_milestones' );

if ( ! empty( $milestones ) && is_array( $milestones ) ) {
    echo '<div class="direct-array-timeline">';
    foreach ( $milestones as $zero_index => $row ) {
        $row_number = $zero_index + 1;
        $row_class  = ( $row_number % 2 === 0 ) ? 'item-even' : 'item-odd';
        $title      = $row['milestone_title'] ?? '';
        $summary    = $row['milestone_summary'] ?? '';

        echo '<div class="timeline-entry ' . esc_attr( $row_class ) . '">';
        echo '<h4>' . esc_html( $title ) . '</h4>';
        echo '<p>' . esc_html( $summary ) . '</p>';
        echo '</div>';
    }
    echo '</div>';
}

Reading the dataset as a native PHP array executes in a fraction of the time required by iterative loop functions, significantly improving Time to First Byte (TTFB).

#Defensive template development and security sanitation

Repeater templates handle arbitrary user input and require rigorous output escaping to protect against cross-site scripting (XSS) vulnerabilities.

  1. Text escaping: Always wrap text subfields in esc_html() or esc_attr().
  2. Rich text formatting: Run WYSIWYG or multiline textarea subfields through wp_kses_post().
  3. Image attachment resolution: Configure image subfields to return Image ID rather than URLs, then render responsive images using wp_get_attachment_image(). This ensures native srcset, sizes, and lazy-loading attributes are populated correctly.
  4. Link sanitization: When subfields contain URL strings, validate them with esc_url().
$cta_link = get_sub_field( 'action_url' );
if ( ! empty( $cta_link ) ) : ?>
    <a href="<?php echo esc_url( $cta_link ); ?>" class="btn-action" target="_blank" rel="noopener noreferrer">
        <?php echo esc_html( get_sub_field( 'action_label' ) ?: __( 'Learn More', 'wppoland' ) ); ?>
    </a>
<?php endif; ?>

#REST API and headless WordPress repeater payload optimization

As decoupled WordPress architectures gain traction with frameworks like Next.js, Nuxt, and Astro, developers frequently consume ACF repeater data via the WP REST API or WPGraphQL. By default, enabling show_in_rest on an ACF field group exposes repeater structures inside the acf JSON object.

However, default REST API responses for repeaters often output redundant metadata, unformatted raw values, or deeply nested structures that inflate JSON payloads.

#Normalizing repeater responses with register_rest_field

To speed up API delivery and eliminate client-side parsing friction, register custom REST fields that format repeater rows before transmission:

add_action( 'rest_api_init', function(): void {
    register_rest_field( 'project', 'milestones_stream', [
        'get_callback' => function( array $post_arr ): array {
            $post_id   = $post_arr['id'];
            $raw_items = get_field( 'project_milestones', $post_id );

            if ( empty( $raw_items ) || ! is_array( $raw_items ) ) {
                return [];
            }

            return array_map( function( array $row, int $index ): array {
                $image_id = $row['milestone_image']['id'] ?? 0;
                return [
                    'index'     => $index + 1,
                    'is_even'   => ( ( $index + 1 ) % 2 === 0 ),
                    'title'     => sanitize_text_field( $row['milestone_title'] ?? '' ),
                    'summary'   => wp_kses_post( $row['milestone_summary'] ?? '' ),
                    'thumbnail' => $image_id ? wp_get_attachment_image_url( $image_id, 'medium_large' ) : null,
                ];
            }, $raw_items, array_keys( $raw_items ) );
        },
        'schema' => [
            'description' => 'Optimized project milestones formatted for headless consumers.',
            'type'        => 'array',
            'context'     => [ 'view', 'embed' ],
        ],
    ] );
} );

This server-side transformation provides headless components with pre-indexed, sanitized properties, removing the need for frontend clients to calculate modulo parity or resolve attachment IDs manually.

#Systematic debugging and troubleshooting of repeater loops

When repeater rows fail to render or style properly, developers should systematically verify the execution path:

  1. Verify loop initiation: If have_rows() returns false despite data existing in the database, confirm that the correct post ID is supplied when querying outside standard template loops, such as have_rows( 'my_field', $custom_post_id ).
  2. Confirm row pointer progression: Missing or misplaced the_row() calls freeze the loop index or cause infinite execution loops. Always ensure the_row() executes directly after the while statement.
  3. Inspect CSS specificity and DOM hierarchies: When CSS :nth-child() rules fail to apply, inspect the rendered DOM in browser developer tools. Intermediate container wrappers inserted by layout engines or plugins frequently break direct child relationships.
  4. Inspect active object cache entries: If edits in the WordPress admin do not reflect on frontend repeater rows, clear transient keys or flush persistent Redis object cache buckets.

#Architectural conclusion and implementation checklist

Mastering Advanced Custom Fields repeaters requires balancing server-side template logic with efficient frontend styling and database awareness. Apply the following checklist to ensure high performance across your WordPress projects:

  1. Verify loop scope: Settle row context using have_rows() and the_row(), and retrieve values strictly with get_sub_field().
  2. Rely on native indexing: Use get_row_index() for deterministic 1-based positioning and modulo calculations.
  3. Prioritize CSS pseudo-classes: Delegate zebra striping, column reversal, and spacing to :nth-child() selectors.
  4. Escape all outputs: Protect site integrity using esc_html(), esc_attr(), esc_url(), and wp_kses_post().
  5. Mitigate database overhead: Cache rendered repeater markup with Transients or process raw arrays with get_field() for high-volume datasets.
  6. Modernize with Gutenberg: Transform deeply nested repeaters into modular ACF Blocks utilizing InnerBlocks and Block Context for improved editorial experience.

For enterprises migrating complex legacy field structures, auditing slow database queries, or refactoring WooCommerce templates into high-performance block architectures, consult our specialized WordPress maintenance services and development team at WPPoland.

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.

Frequently asked questions

Practical answers to apply the topic in real execution.

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