Skip to content

Repeater Field

A dynamic repeatable row builder for collecting structured multi-item data.

Overview

The Repeater field renders a list of rows where each row contains a defined set of sub-fields. The user can add, remove, and reorder rows. It returns an array of associative arrays — one per row — each containing the values of its sub-fields. Use it for team members, testimonials, pricing table rows, FAQ items, feature lists, custom menu items, or any other structured repeatable content in your theme.

Field Registration

[
    'id'           => 'team_members',
    'type'         => 'repeater',
    'title'        => __( 'Team Members', 'your-textdomain' ),
    'subtitle'     => __( 'Add team members to display on the About page', 'your-textdomain' ),
    'min'          => 0,
    'max'          => 12,
    'button_label' => __( 'Add Team Member', 'your-textdomain' ),
    'fields'       => [
        [
            'id'    => 'name',
            'type'  => 'text',
            'title' => __( 'Name', 'your-textdomain' ),
        ],
        [
            'id'    => 'role',
            'type'  => 'text',
            'title' => __( 'Role', 'your-textdomain' ),
        ],
        [
            'id'    => 'photo',
            'type'  => 'image',
            'title' => __( 'Photo', 'your-textdomain' ),
            // Returns: { id, url, width, height, alt, title } — same as the Image field
        ],
        [
            'id'    => 'bio',
            'type'  => 'textarea',
            'title' => __( 'Bio', 'your-textdomain' ),
        ],
    ],
]

Field Options

OptionTypeRequiredDescription
idstringUnique field identifier — used as the option key
typestringMust be repeater
titlestringLabel shown above the repeater
subtitlestringSmaller descriptive text shown below the label
descstringHelp text shown below the repeater
fieldsarrayArray of sub-field definitions
minintMinimum number of rows — user cannot delete below this count. Default: 0
maxintMaximum number of rows — Add Row button is hidden when this count is reached. Default: unlimited
button_labelstringCustom label for the Add Row button. Default: 'Add Row'
defaultarrayArray of default row data — each item is an associative array matching the sub-field IDs
requiredarrayConditional logic rules — see Conditional Logic

Supported Sub-field Types

The Repeater passes each sub-field directly through FieldRenderer — any registered field type works as a sub-field with two exceptions:

Fully supported:

texttextareanumberspinnersliderselectbutton_setradiocheckboxselect_imagetoggleswitchcolorgradient_pickerimagegalleryicontypographydimensionsspacingborderbackgroundlinkdate_pickersocial_mediacode_editorgroup

Display-only (no stored value — usable for UI structure):

infosectionraw

Not supported as a sub-field:

repeater — nesting a repeater inside a repeater is not supported.

Note: Complex sub-fields like imagegallerylinkbackground, and group return their full structured array shapes inside each row — the same shapes as when used as standalone fields.

Return Value

Type: array

Returns an indexed array of associative arrays — one per row — each containing the sub-field values keyed by sub-field ID. Returns an empty array if no rows have been added.

$members = themeplus_get_option( 'team_members', [] );
// Returns:
// [
//     [
//         'name'  => 'Alice',
//         'role'  => 'Designer',
//         'photo' => [
//             'id'     => 42,
//             'url'    => 'https://example.com/wp-content/uploads/alice.jpg',
//             'width'  => 300,
//             'height' => 300,
//             'alt'    => 'Alice',
//             'title'  => 'Alice',
//         ],
//         'bio'   => 'Alice is a senior designer...',
//     ],
//     [
//         'name'  => 'Bob',
//         'role'  => 'Developer',
//         'photo' => [],   // empty array when no image selected
//         'bio'   => 'Bob is a backend developer...',
//     ],
// ]

Usage Examples

Rendering team members

$members = themeplus_get_option( 'team_members', [] );

if ( ! empty( $members ) ) {
    echo '';
    foreach ( $members as $member ) {
        $name  = $member['name'] ?? '';
        $role  = $member['role'] ?? '';
        $photo = $member['photo'] ?? [];   // array, not an ID
        $bio   = $member['bio']  ?? '';

        echo '';

        if ( ! empty( $photo['url'] ) ) {
            echo '';
        }

        echo '' . esc_html( $name ) . '';
        echo ''  . esc_html( $role ) . '';
        echo ''   . esc_html( $bio  ) . '';
        echo '';
    }
    echo '';
}

FAQ accordion

[
    'id'      => 'faq_items',
    'type'    => 'repeater',
    'title'   => __('FAQ Items', 'your-textdomain'),
    'fields'  => [
        [
            'id'    => 'question',
            'type'  => 'text',
            'title' => __('Question', 'your-textdomain'),
        ],
        [
            'id'    => 'answer',
            'type'  => 'textarea',
            'title' => __('Answer', 'your-textdomain'),
        ],
    ],
]
$faq_items = themeplus_get_option( 'faq_items', [] );

if ( ! empty( $faq_items ) ) {
    echo '<dl class="faq">';
    foreach ( $faq_items as $item ) {
        $question = $item['question'] ?? '';
        $answer   = $item['answer']   ?? '';

        if ( ! $question ) continue;

        echo '<dt class="faq__question">' . esc_html( $question ) . '</dt>';
        echo '<dd class="faq__answer">'   . esc_html( $answer   ) . '</dd>';
    }
    echo '</dl>';
}

Testimonials slider

[
    'id'      => 'testimonials',
    'type'    => 'repeater',
    'title'   => __('Testimonials', 'your-textdomain'),
    'fields'  => [
        [
            'id'    => 'quote',
            'type'  => 'textarea',
            'title' => __('Quote', 'your-textdomain'),
        ],
        [
            'id'    => 'author',
            'type'  => 'text',
            'title' => __('Author Name', 'your-textdomain'),
        ],
        [
            'id'    => 'company',
            'type'  => 'text',
            'title' => __('Company / Role', 'your-textdomain'),
        ],
        [
            'id'    => 'avatar',
            'type'  => 'image',
            'title' => __('Avatar', 'your-textdomain'),
        ],
        [
            'id'      => 'rating',
            'type'    => 'select',
            'title'   => __('Rating', 'your-textdomain'),
            'default' => '5',
            'options' => [
                '5' => '★★★★★',
                '4' => '★★★★☆',
                '3' => '★★★☆☆',
            ],
        ],
    ],
]

With a default row

[
    'id'      => 'social_links_custom',
    'type'    => 'repeater',
    'title'   => __('Custom Social Links', 'your-textdomain'),
    'default' => [
        ['label' => 'GitHub', 'url' => '', 'icon' => 'fa-brands fa-github'],
    ],
    'fields'  => [
        [
            'id'    => 'label',
            'type'  => 'text',
            'title' => __('Label', 'your-textdomain'),
        ],
        [
            'id'    => 'url',
            'type'  => 'text',
            'title' => __('URL', 'your-textdomain'),
        ],
        [
            'id'      => 'icon',
            'type'    => 'icon',
            'title'   => __('Icon', 'your-textdomain'),
            'default' => 'fa-brands fa-github',
        ],
    ],
]

With a conditional field

[
    'id'      => 'enable_testimonials',
    'type'    => 'toggle',
    'title'   => __('Enable Testimonials Section', 'your-textdomain'),
    'default' => false,
],
[
    'id'       => 'testimonials',
    'type'     => 'repeater',
    'title'    => __('Testimonials', 'your-textdomain'),
    'fields'   => [
        ['id' => 'quote',  'type' => 'textarea', 'title' => __('Quote', 'your-textdomain')],
        ['id' => 'author', 'type' => 'text',     'title' => __('Author', 'your-textdomain')],
    ],
    'required' => ['enable_testimonials', '==', true],
],

Notes

  • Always check ! empty( $rows ) before looping — the field returns an empty array when no rows have been added.
  • Use min to guarantee a minimum number of rows — useful when your template always expects at least one item. Use max to cap rows and keep the panel manageable. Use button_label to make the Add Row button self-explanatory (e.g. 'Add Team Member''Add FAQ Item').
  • The image sub-field returns a structured array { id, url, width, height, alt, title } — use $row['photo']['url'] directly, not wp_get_attachment_image( $row['photo'] ).
  • Always use the ?? null coalescing operator when reading sub-field values from each row — individual keys may be absent if a row was saved before a sub-field was added.
  • Sub-field IDs must be unique within the repeater but do not need to be globally unique across the entire options panel.
  • Rows are returned in the order the user arranged them in the panel — use this for drag-and-drop ordered content like feature lists or pricing tiers.
  • Avoid registering too many sub-fields per row — five to eight sub-fields per row keeps the panel usable. For complex structured data, consider splitting into multiple repeaters.

On This Page