Skip to content
ThemesIonic — home
WooCommerce

Adding Content Above the WooCommerce Product Grid

The hook fires on archive templates and nowhere else, which is why a banner that works on the shop page vanishes inside a shortcode grid. Priority decides whether it lands above or below the sorting dropdown.

4 min read intermediate

The hook belongs to the archive template, not to the concept of a product grid. woocommerce_before_shop_loop fires on the shop page and on product category and tag archives. It does not fire inside a shortcode grid, and it does not fire inside a block-rendered collection. Most "my banner disappeared" reports are the same banner working correctly on a page that was never an archive.

What already runs there

The hook is not empty when you arrive. WooCommerce attaches three callbacks to it, and their priorities are the reason your own output lands where it does:

Priority Callback What it prints
10 woocommerce_output_all_notices Cart and error notices
20 woocommerce_result_count "Showing 1–12 of 40 results"
30 woocommerce_catalog_ordering The sorting dropdown

Hooking at the default priority of 10 puts you alongside the notices, which is almost never what a category banner wants. Choose the priority deliberately:

add_action( 'woocommerce_before_shop_loop', 'ti_category_intro', 5 );

function ti_category_intro() {
	if ( ! is_product_category() ) {
		return;
	}

	$term = get_queried_object();

	if ( ! $term instanceof WP_Term ) {
		return;
	}

	printf(
		'<div class="ti-category-intro">%s</div>',
		wp_kses_post( term_description( $term ) )
	);
}

Priority 5 places the block above the notices. Priority 25 drops it between the result count and the sorting dropdown. There is no priority that places content below the grid — that is a different hook, woocommerce_after_shop_loop.

The conditional tag is not optional

The hook fires on every archive that renders the product loop, which is more templates than people expect: the main shop page, every product category, every product tag, and the search results page for products. A callback without a conditional prints its banner on all of them.

is_product_category() narrows it to category archives. is_shop() matches the main shop page only — and note that on a site where the shop page is also the front page, is_shop() is true while is_product_category() is false. If the banner belongs to one specific category, compare the queried object's slug rather than hard-coding a term ID that changes when the catalogue is rebuilt or imported, as it will be if you import products from a CSV.

Where the hook genuinely does not fire

This is the part that costs the most time, so work through it before rewriting the callback:

  • Shortcode grids. A grid produced by the products shortcode runs woocommerce_shortcode_before_products_loop instead. The distinction matters on any page assembled with product shortcodes rather than on the archive template.
  • Block-rendered collections. A product block renders from its own markup and does not call the classic archive template at all, so no archive hook fires. This is the same boundary that governs product blocks generally.
  • A theme with its own archive template. A theme shipping its own archive-product.php can print the loop without calling the hook. Switch to a default theme for one page load to confirm before blaming your code.
  • A filtered view rendered over AJAX. A product filter that replaces the grid without a full page load re-renders only the products, so anything printed by an archive hook stays as it was on first paint.
  • Pages that are not archives. The cart, checkout and single product templates do not run it, by design.

Removing what WooCommerce prints

Removing a default callback works, but remove_action matches on hook name, callback name and priority. All three must match:

add_action( 'init', 'ti_drop_result_count' );

function ti_drop_result_count() {
	remove_action( 'woocommerce_before_shop_loop', 'woocommerce_result_count', 20 );
}

Two failures are common here. The first is omitting the 20, which makes the call look correct and do nothing. The second is running remove_action too early — before WooCommerce has registered the callback, there is nothing to remove. Wrapping it in an init callback is enough; a plugin file that calls it at load time may run first.

Removing the sorting dropdown deserves a second thought rather than a snippet. It is the only control a shopper has over a long catalogue, and taking it away raises the cost of every other browsing decision, in the same way a crowded product card does.

Verify it on the right template

Test on three URLs before calling it done: the main shop page, a category archive, and a tag archive. A callback guarded by is_shop() will pass the first and fail the other two, and the failure is silent — nothing prints, no error appears.

If nothing renders anywhere, confirm the page is actually running the archive template. The quickest check is whether the default result count is visible. If WooCommerce's own output is missing too, the template is not the one you think it is, and no hook priority will fix that — start from products not showing instead.

Frequently asked

Because the default callbacks already occupy priorities 10, 20 and 30. Anything you hook at the default priority of 10 competes with the notices block and lands after it. Hook at 5 to sit above everything WooCommerce prints.
The shortcode runs its own hook, woocommerce_shortcode_before_products_loop, not the archive hook. A shortcode grid is not an archive template, so the archive hooks never fire.
Yes, with remove_action, but the priority in your removal call must match the priority used to add it exactly. woocommerce_result_count is added at 20, so removing it at the default 10 silently does nothing.

Related guides