WooCommerce adds a product_cat-{slug} class to the product wrapper, but not to the <body> tag, so in WordPress you cannot style the header, breadcrumbs or sidebar of a single product page by its product category.
This snippet adds one class per assigned category to <body> on single product pages, for example product_cat_shoes. A rule like .product_cat_shoes .site-header { … } can then change the whole page, not only the product summary.
The snippet
<?php
add_filter( 'body_class', 'dpc_product_cat_body_class' );
/**
* Add a product_cat_{slug} class to <body> on single product pages.
*/
function dpc_product_cat_body_class( $classes ) {
if ( ! function_exists( 'is_product' ) || ! is_product() ) {
return $classes;
}
$terms = get_the_terms( get_queried_object_id(), 'product_cat' );
if ( ! is_array( $terms ) ) {
return $classes;
}
foreach ( $terms as $term ) {
$classes[] = 'product_cat_' . sanitize_html_class( $term->slug );
}
return $classes;
}
Add it with Scripts Organizer
Scripts Organizer ships this snippet in its One-Click Import library. To add it by hand:
- In wp-admin go to Scripts Organizer → Code Blocks → Add New and give the block a title.
- Script location: PHP.
- Trigger location: Everywhere. The
body_classfilter runs on the front end, so Conditions would also work, but Everywhere keeps the setup simple. - Action hook: leave it empty. The code only registers filters and actions, so it can run while plugins load.
- Paste the snippet into the PHP editor, enable the block and publish it.
The same code also works unchanged in a small plugin or an mu-plugin file in wp-content/mu-plugins/.
How it works
body_class is the filter WordPress runs when it builds the class list for <body>, in classic themes through body_class() in header.php and in block themes through the template canvas.
is_product()limits the snippet to single product pages. Thefunction_exists()check keeps the site running if WooCommerce is deactivated, because the filter would otherwise call an undefined function.get_queried_object_id()reads the product ID from the main query. It works before the loop starts and does not depend on the global$product. Older copies of this snippet calledwc_get_product()and then->get_id(), which is a fatal error whenever no product object is available.get_the_terms()returnsfalseor aWP_Errorwhen there is nothing to add; theis_array()check handles both.sanitize_html_class()strips characters that are not valid in a class name, which matters for non-Latin slugs.
Customise it
To add parent categories as well, so a product in Shoes → Running also gets product_cat_shoes, replace the foreach loop with this one and return array_unique( $classes ) at the end:
foreach ( $terms as $term ) {
$classes[] = 'product_cat_' . sanitize_html_class( $term->slug );
foreach ( get_ancestors( $term->term_id, 'product_cat', 'taxonomy' ) as $ancestor_id ) {
$ancestor = get_term( $ancestor_id, 'product_cat' );
if ( $ancestor instanceof WP_Term ) {
$classes[] = 'product_cat_' . sanitize_html_class( $ancestor->slug );
}
}
}
On product category archives WordPress already adds tax-product_cat and term-{slug} to <body>, so the snippet leaves those pages alone.
Gotchas
- The body class uses an underscore (
product_cat_shoes), while the class WooCommerce puts on the product wrapper uses a hyphen (product_cat-shoes). Use the one that matches the element you target. - Page caching stores the full HTML. After you rename a category slug, clear the cache or the old class stays in the markup.
- If the class does not appear, the theme or another plugin may replace the array later on
body_class. Find all functions hooked to a WordPress action / filter shows what else runs there.
In the Scripts Organizer library Import it with one click from Scripts Organizer.