How to Build Repeater Block in Gutenberg

Most of the time, you can’t create a dynamic webpage without having repeater fields. While developing the block you may want to give a feature to add repeatable content. In this tutorial, I am going to build a repeater block which having options to add a title, message, and image. These are the basic subfields you mostly require in the repeater. Of course, you can modify these fields as per the requirement.

Getting Started

I always preferred to build a dynamic block. This kind of block is easy to modify if your content structure is changed.

Open the terminal in your wp-content/plugins directory and run the below command to create the plugin having a dynamic block.

npx @wordpress/create-block artisansweb-block --variant=dynamic

This command will create a plugin called artisansweb-block. Activate this plugin.

Now to build a repeater, I’ll create 2 blocks. The multiple blocks can be created inside a single plugin. You don’t need to run the above command twice. I’ll show it in a moment.

The first block will have 3 fields(2 RichText and MediaUpload). Then inside the second(main) block, I’ll embed the first block using the concept of inner blocks. The inner blocks allow you to use existing blocks multiple times with additional controls like sorting, deleting, etc.

Once you’re finished with this tutorial, in the editor, you’ll be able to add the main block say Artisansweb – Repeater which will give the output as shown in the screenshot below. You can simply duplicate the child block as many times which acts exactly like a repeater.


Register Multiple Blocks Inside The Plugin

You already scaffold a plugin for the dynamic block. Under the src directory, you’ll see a few files which are required to build a block. Create 2 directories inside this src folder – block-title-message-image and block-repeater. Copy all files from the src folder and paste them into these newly created directories.

The block-title-message-image and block-repeater are the dynamic blocks we are going to build in the next steps.

Register these blocks from the plugin’s main file as follows.

function create_block_artisansweb_block_block_init() {
	register_block_type( __DIR__ . '/build' );
	register_block_type( __DIR__ . '/build/block-title-message-image' );
	register_block_type( __DIR__ . '/build/block-repeater' );
add_action( 'init', 'create_block_artisansweb_block_block_init' );

This way you can build as many blocks from the single plugin. While developing a WordPress website, it’s a better approach to develop all your blocks inside the main plugin.

Run the npm start command from your plugin’s directory(plugins/artisansweb-block). This command compiles the code from the src folder and generates the browser-supported code into the build folder.

Modify The block.json


	"name": "artisansweb/repeater",
	"title": "Artisans Web - Repeater",
	"description": "This Block allow you to insert repeatable content.",

Nothing fancy here. Everything is straightforward. The name value specified here will be used as a parent to the next block.


	"name": "artisansweb/title-message-image",
	"title": "Add Title, Message & Image",
	"description": "This Block will be used in the Repeater Block.",
	"attributes": {
		"title": {
        	"type": "string"
    	"content": {
        	"type": "string"
		"image": {
        	"type": "integer"
	"parent": [

When we create a dynamic block, to store the content of a block attributes are used. I have passed them as per the content we are going to save. Notice, the parent value as well. It is because this block will be used inside the inner blocks and available only within the parent block(artisansweb/repeater).

Build The First Block

As we are taking 3 fields into the repeater, you are required to build this block which allows you to enter content – title, message, and image. It can be done using the RichText and MediaUpload components. The usage of these components I already explained in the linked article.


import { useBlockProps, RichText, MediaUpload, MediaUploadCheck } from '@wordpress/block-editor';
import { Button, Spinner } from '@wordpress/components';
import { useSelect } from '@wordpress/data';

export default function Edit({ attributes, setAttributes }) {
	const { mediaId, media } = useSelect( select => {
		return {
			mediaId: attributes.image,
			media: select('core').getMedia(attributes.image)
    }, [attributes.image] );

	return (
		<div { ...useBlockProps() }>
				value={ attributes.title }
				allowedFormats={ [] }
				onChange={ ( title ) => setAttributes( { title } ) }
				placeholder="Enter title here.."
				value={ attributes.content }
				allowedFormats={ [] }
				onChange={ ( content ) => setAttributes( { content } ) }
				placeholder="Enter content here.."
					onSelect={ ( media ) =>
						setAttributes( { image: } )
					allowedTypes={ ['image'] }
					value={ attributes.image }
					render={ ( { open } ) => (
							{ ! mediaId && <Button variant="secondary" onClick={ open }>Upload Image</Button> }
							{ !! mediaId && ! media && <Spinner /> }
							{ !! media && media &&
								<Button variant="link" onClick={ open }>
									<img src={ media.source_url } width={200} />
					) }
			{ !! mediaId && media &&
			<Button onClick={ () => setAttributes( { image: 0 } ) } isLink isDestructive>
				Remove image

Create Repeater Using Inner Blocks

The Inner Blocks are nested blocks where you can add more than one block under the one parent block. In the next step, I’ll build the inner block and inside it allow only the block we created in the previous step. The name of the block we created is artisansweb/title-message-image as defined in block.json.


import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
import { BaseControl } from '@wordpress/components';

export default function Edit() {
	const blockProps = useBlockProps();
	const innerBlocksProps = useInnerBlocksProps(blockProps,
			allowedBlocks: ['artisansweb/title-message-image'],
			template: [
	return (
		<BaseControl label={__("Artisans Web - Repeater")}>
			<div {...innerBlocksProps} />

To save the content of inner blocks you need to pass <InnerBlocks.Content /> to the save() method of index.js as follows.


import { InnerBlocks } from '@wordpress/block-editor';

registerBlockType(, {
	edit: Edit,
	save: () => <InnerBlocks.Content />,
} );

Now you can add the Artisansweb – Repeater block to the editor. You will get a predefined set of templates to enter the content. To add more content you can simply duplicate the child block(added inside the main block). Go ahead and add your repeatable content.

Display Repeater Content On Frontend

Usually, to print the content of dynamic blocks we used either the $content or $attributes variable. But in the case of inner blocks combined with the custom block, we need to use a different approach to render the frontend.

Inside our block-repeater/render.php template, you have access to the $block variable using which we can access the content of our block. The content is available to innerBlocks property of the $block variable. The below code will print content using the $block variable.

<div <?php echo get_block_wrapper_attributes(); ?>>
	if ( isset( $block->parsed_block['innerBlocks'] ) ) {
		foreach ( $block->parsed_block['innerBlocks'] as $innerBlock ) {
			if ( !empty( $innerBlock['attrs']['title'] ) ) {
				printf("<h3>%s</h3>", $innerBlock['attrs']['title']);
			if ( !empty( $innerBlock['attrs']['content'] ) ) {
				printf("<p>%s</p>", $innerBlock['attrs']['content']);
			if ( !empty( $innerBlock['attrs']['image'] ) ) {
				printf("%s", wp_get_attachment_image($innerBlock['attrs']['image']), 'medium');

Here, I loop through the innerBlocks property and print the content stored as the attributes into the database.

This is how you can build a repeater in Gutenberg. For the sake of the tutorial, I used RichText and MediaUpload components. You are free to use any other components that fit to your needs.

If you liked this article, then please subscribe to our YouTube Channel for video tutorials.

Leave a Reply

Your email address will not be published. Required fields are marked *