SOLVED: Background Image Dynamic Data Not Rendering in Builder for First Iteration of Custom Query Loop

Browser: Chrome Version 148.0.7778.97 (Official Build) (64-bit)
OS: Windows 11

Hi,

I’ve developed a custom plugin that integrates Carbon Fields with Bricks Builder by implementing my own Dynamic Data Tag providers (for default, image, and gallery fields) and a custom Query Loop provider for Carbon Fields repeaters — all registered through the official Bricks filter hooks (bricks/dynamic_data/register_providers, bricks/setup/control_options, bricks/query/run, bricks/query/loop_object).

Everything works perfectly on the frontend: Dynamic Data Tags resolve correctly, the custom Query Loop iterates through all repeater entries, and images render as expected — whether used in Image Elements or as CSS background images.

However, I’ve discovered a Builder-only rendering issue specifically affecting background-image with dynamic data on the first iteration of my custom query loop.

Summary:

When using a custom query loop provider (registered via bricks/query/run filter) nested inside a Terms Query Loop, dynamic data bound to a Background Image (background-image CSS property) does not render in the Builder for the first loop iteration. All subsequent iterations render correctly.

The frontend renders correctly for all iterations — this is a Builder preview-only issue.

Using an Image Element (<img> tag) with the same dynamic data tag works correctly in both Builder and frontend.

Steps to Reproduce:

  1. Register a custom query loop type via bricks/setup/control_options and bricks/query/run filters (e.g., a Carbon Fields repeater on term meta).

  2. Create a Terms Query Loop (e.g., Product Categories).

  3. Inside the Terms loop, add a Slider (Nestable) with Query Loop set to the custom query type.

  4. Inside the Slider’s Slide element, set a Background Image using a dynamic data tag that resolves to an image ID from the custom query (e.g., {crb_product_cat_gallery__image}).

  5. Open the page in the Bricks Builder.

Expected Result:

All slides in the Slider should display their background images in the Builder preview, matching the frontend output.

Actual Result:

  • First slide of each category: Background image is blank/missing in the Builder.

  • Subsequent slides: Background images render correctly in the Builder.

  • Frontend: All slides render correctly (all background images visible).

  • Image Element (same dynamic tag): Works correctly in both Builder and frontend.

Analysis

The issue is caused by the difference in how Bricks renders core query types vs custom query providers in Query::render() (query.php):

Core types (post, term, user) have dedicated rendering paths that fully establish WordPress global context before the render callback:

// Post loop — the_post() sets up global $post, featured image, etc.

$query_result->the_post();

$this->loop_object = get_post();

Custom providers (ACF, Meta Box, Carbon Fields, etc.) use a generic path that relies solely on the bricks/query/loop_object filter:

// Custom loop — no WP global context, only filter-based

$this->loop_object = apply_filters('bricks/query/loop_object', $loop_object, ...);

For the first iteration of custom providers, parse_dynamic_data() appears to resolve CSS background-image properties before the loop object context is fully available. This doesn’t affect <img> elements (Image Element) because they resolve their src attribute through a different code path.

This does not affect:

  • Core query types (post, term, user) — they use the_post() / direct assignment with full WP context

  • Image Elements (<img> tags)

  • Frontend rendering

Workaround:

Use an Image Element instead of background-image for dynamic images inside custom query loops. Builder/Frontend rendering is unaffected regardless of approach.

Thanks

:bear:

Hey @datgausaigon,

that’s quite a setup. But the first question is, can you replicate the same with a native query loop or/and with native dynamic tags?

Matej

Hi @Matej ,

Thank you for your reply and your patience. Over the past while, alongside my work, I’ve dug into the root cause of this issue and finally found it. You can use the attached plugin to reproduce the bug.

About this plugin:

  • It simulates an Image Repeater Loop by fetching the first 3 images from wp/media (uploads) → Please upload at least 3 random images to test.

  • The plugin creates provider-repro.php, which acts as the data provider and registers Bricks-standard Dynamic Tags.

Source code Plugin:

plugin-code-name: gau-bricks-bug-report-002

gau-bricks-bug-report-002.php

<?php
/**
 * Plugin Name: Bricks Bug Report 002 - Nested Loop Background Dynamic Data (Gấu)
 * Description: Minimal reproduction for Bricks Forum — a dynamic data tag used as Background image inside a NESTED query loop shows NO background in the builder preview (single top-level loop and frontend are both fine). Root cause: the generated "background-image: none" fallback rule gains the parent-loop class prefix, ties the correct per-item [data-query-loop-index] rules at specificity (0,3,0), and wins by source order. Zero dependencies. STR below.
 * Version: 1.4.1 (20260711)
 * Author: 🐻
 */

namespace Gau\Bricks_Bug_Report_002;

defined( 'ABSPATH' ) || exit;

const QUERY_TYPE      = 'repro_attachments';
const QUERY_TYPE_ROWS = 'repro_attachment_rows';

/** Latest N image attachment IDs (the loop data source — no custom fields involved). */
function latest_attachment_ids( int $count = 3 ): array {
	$ids = get_posts(
		array(
			'post_type'      => 'attachment',
			'post_mime_type' => 'image',
			'post_status'    => 'inherit',
			'numberposts'    => $count,
			'fields'         => 'ids',
		)
	);
	return array_map( 'intval', $ids );
}

add_action(
	'plugins_loaded',
	function () {
		if ( get_template() !== 'bricks' ) {
			return;
		}

		$providers_dir = get_template_directory() . '/includes/integrations/dynamic-data/providers/';
		if ( ! is_file( $providers_dir . 'base.php' ) ) {
			return;
		}
		require_once $providers_dir . 'provider-interface.php';
		require_once $providers_dir . 'base.php';

		/* 1. Custom query types: A = scalar attachment IDs · B = associative-array rows. */
		add_filter(
			'bricks/setup/control_options',
			function ( $control_options ) {
				$control_options['queryTypes'][ QUERY_TYPE ]      = 'Repro A: Attachments Loop (scalar IDs)';
				$control_options['queryTypes'][ QUERY_TYPE_ROWS ] = 'Repro B: Attachment Rows Loop (array rows)';
				return $control_options;
			}
		);

		add_filter(
			'bricks/query/run',
			function ( $results, $query_obj ) {
				if ( $query_obj->object_type === QUERY_TYPE ) {
					return latest_attachment_ids( 3 );
				}
				if ( $query_obj->object_type === QUERY_TYPE_ROWS ) {
					// Same 3 attachments, but each loop item is an ARRAY row — the exact
					// shape repeater-style integrations use.
					$rows = array();
					foreach ( latest_attachment_ids( 3 ) as $index => $attachment_id ) {
						$rows[] = array(
							'image'   => $attachment_id,
							'caption' => 'Row ' . ( $index + 1 ),
						);
					}
					return $rows;
				}
				return $results;
			},
			10,
			2
		);

		/* 2. Provider with 4 tags — resolved via Bricks' OWN query state (\Bricks\Query),
			so no custom loop-context bookkeeping can be blamed. */
		require_once __DIR__ . '/provider-repro.php';

		add_filter(
			'bricks/dynamic_data/register_providers',
			function ( $providers ) {
				$providers[] = 'Repro';
				return $providers;
			}
		);
	}
);

provider-repro.php

<?php
/**
 * Repro provider — 4 tags, all returning a media attachment ID through
 * format_value_for_context() (the exact pipeline every real integration uses).
 * Loop-dependent tags read the CURRENT loop item via \Bricks\Query — the
 * theme's own state — so no custom loop bookkeeping can be blamed:
 *
 *   {repro_loop_image}    current item of loop A (scalar attachment ID)
 *   {repro_row_image}     ['image'] of the current loop-B row (array loop object)
 *   {repro__rows__image}  same data/logic, tag name carrying double underscores
 *   {repro_static_image}  latest attachment, NO loop dependency — control tag
 *                         proving the provider + background pipeline work.
 */

namespace Bricks\Integrations\Dynamic_Data\Providers;

use Bricks\Query;

defined( 'ABSPATH' ) || exit;

class Provider_Repro extends Base {

	protected $name = 'provider_repro';

	public function register_tags(): void {
		parent::register_tags();

		$this->tags['repro_loop_image'] = array(
			'name'     => '{repro_loop_image}',
			'label'    => 'Repro Loop Image (current loop attachment)',
			'group'    => 'Repro',
			'provider' => $this->name,
			'render'   => 'get_tag_value',
			'fields'   => array(),
		);

		$this->tags['repro_row_image'] = array(
			'name'     => '{repro_row_image}',
			'label'    => 'Repro Row Image (array loop object)',
			'group'    => 'Repro',
			'provider' => $this->name,
			'render'   => 'get_tag_value',
			'fields'   => array(),
		);

		// v1.2.0 — SAME data/logic as repro_row_image, but the tag NAME contains
		// DOUBLE UNDERSCORES (like real repeater sub-field tags: {gf_gfb__slides__image}).
		// Isolates whether "__" in a tag name breaks the BACKGROUND path specifically
		// (it is proven fine in the content path).
		$this->tags['repro__rows__image'] = array(
			'name'     => '{repro__rows__image}',
			'label'    => 'Repro Row Image DOUBLE UNDERSCORE variant',
			'group'    => 'Repro',
			'provider' => $this->name,
			'render'   => 'get_tag_value',
			'fields'   => array(),
		);

		$this->tags['repro_static_image'] = array(
			'name'     => '{repro_static_image}',
			'label'    => 'Repro Static Image (control, no loop)',
			'group'    => 'Repro',
			'provider' => $this->name,
			'render'   => 'get_tag_value',
			'fields'   => array(),
		);
	}

	/** Current attachment ID of loop A (scalar loop object) — via Bricks' own Query state. */
	protected function current_loop_attachment_id(): int {
		$looping_query_id = Query::is_any_looping();
		if ( ! $looping_query_id ) {
			return 0;
		}
		if ( Query::get_query_object_type( $looping_query_id ) !== \Gau\Bricks_Bug_Report_002\QUERY_TYPE ) {
			return 0;
		}
		return (int) Query::get_loop_object( $looping_query_id );
	}

	/** Current attachment ID of loop B (ARRAY loop object — repeater-row shape). */
	protected function current_row_attachment_id(): int {
		$looping_query_id = Query::is_any_looping();
		if ( ! $looping_query_id ) {
			return 0;
		}
		if ( Query::get_query_object_type( $looping_query_id ) !== \Gau\Bricks_Bug_Report_002\QUERY_TYPE_ROWS ) {
			return 0;
		}
		$row = Query::get_loop_object( $looping_query_id );
		return ( is_array( $row ) && isset( $row['image'] ) ) ? (int) $row['image'] : 0;
	}

	public function get_tag_value( $tag, $post, $args, $context ) {
		$tag_id  = trim( trim( (string) $tag ), '{}' );
		$post_id = $post->ID ?? get_the_ID();

		if ( $tag_id === 'repro_static_image' ) {
			$ids           = \Gau\Bricks_Bug_Report_002\latest_attachment_ids( 1 );
			$attachment_id = $ids ? $ids[0] : 0;
		} elseif ( $tag_id === 'repro_loop_image' ) {
			$attachment_id = $this->current_loop_attachment_id();
		} elseif ( $tag_id === 'repro_row_image' || $tag_id === 'repro__rows__image' ) {
			$attachment_id = $this->current_row_attachment_id();
		} else {
			return '';
		}

		if ( ! $attachment_id ) {
			return '';
		}

		$filters = array(
			'object_type' => 'media',
			'image'       => true,
		);
		return $this->format_value_for_context( $attachment_id, $tag, $post_id ?: 0, $filters, $context ?: 'image' );
	}
}

Here are the steps to see the issue:

Environment for reproduction:

  • PHP 8.5.5

  • WordPress 7.0.1

  • Bricks Builder 2.3.9

  1. Install and activate this plugin.

  2. Upload at least 3 images to the website via wp-admin/Media.

  3. Create 3 posts with any titles.

  4. Create a new Page → Edit with Bricks.

  5. Create the following Sections to see the issue:

4.1: Section 1 (as shown in the 2 images below)

  • Block <loop> by: Repro A: Attachments Loop (scalar IDs)

  • Inside the Block, there is an Image Element with Content set to: {repro_loop_image} → Works normally.

4.2: Section 2 (as shown in the 2 images below)

  • Block <loop> by: Repro A: Attachments Loop (scalar IDs)

  • Block settings:

    • Set <Height>

    • Set <Background image> to: {repro_loop_image} → Works normally.

4.3: Section 3 (as shown in the 2 images below)

  • Block <loop> by: Repro B: Attachment Rows Loop (array rows)

  • Block settings:

    • Set <Height>

    • Set <Background image> to: {repro_row_image} → Works normally.

4.4: Section 4 (as shown in the 2 images below)

  • Block <loop> by: Repro B: Attachment Rows Loop (array rows)

  • Block Wrapper <loop>: post → loops over the 3 created posts.

  • Block settings:

    • Set <Height>

    • Set <Background image> to: {repro__rows__image}

- → The issue starts here. When wrapped inside another <loop> block, it generates a background-image: none; CSS rule that overrides the correct per-item background (as highlighted in the 2 red boxes in the image). If you temporarily uncheck this rule in DevTools, the correct background image will appear.

In short, why this happens (The Root Cause): This issue is caused by a CSS specificity tie generated within the builder. When an element with a dynamic background sits inside a nested query loop, its fallback rule (background-image: none;) is prefixed with the parent loop’s class.

This prefix increases the fallback rule’s specificity, making it exactly tie with the correct per-item rule ([data-query-loop-index="..."]). Because they tie, the fallback rule wins simply due to its source order, suppressing all correct per-item backgrounds in the preview. Unchecking the none rule in DevTools instantly reveals that the correct URLs are actually being generated.

Thanks

:bear:

Hi @Matej @timmse

Can you reproduce the error? Do you need any further assistance?

Thanks,

:bear:

Hi @datgausaigon,

Thanks for the detailed report and reproduction and I’m sorry for delayed response.

I’ve now replicated the issue locally without using the plugin. The reproduction uses native nested query loops with a dynamic background image, and confirms that the issue is limited to the Builder preview, while the frontend renders the background images correctly.

Thanks again and sorry for the wait!
Matej

1 Like

We’ve addressed this in Bricks 2.3.11, now available as a one-click update in your WordPress Dashboard.

Please read the changelog entry before updating, and let us know if you experience any issues.

Hi @Matej ,

Thank you for the patch.

But there are still bugs. Specifically:

  • This only happens in the Builder; the Front End works normally.

  • Issue with Slider (Nestable) Element, First “Slide”

  • From the second Item Loop onwards, everything works normally. It has:
    background-image: url(…

  • However, the first Item is missing:
    background-image: url(…
    –> don’t have “background-image"

Please check and re-open it.

Thanks,

:bear:

Hi @Matej

This is a new bug found by this issue. I create new Bug for this

Thanks,

:bear:

Hi @datgausaigon,

Thanks. This is separate from the original nested-query-loop background issue, and we’re tracking it in your new Slider (Nestable) thread: Slider (Nestable): Background Image missing on cloned/next slides in Builder (Items to show > 1)

We’ll update that thread once it’s fixed, I was able to replicate it :wink:

Thank you!
Matej

1 Like