
🔧 How to make a WordPress theme WooCommerce ready: a complete guide for developers
Building your own theme and want to integrate a store? Technically WooCommerce works with any theme since it's just a plugin. But "works" doesn't mean "looks right": the product grid breaks, sidebars drift, and default styles clash with yours.
The problem runs deeper than a few CSS rules. Starting with WooCommerce 3.3, a theme without declared support gets simplified rendering via shortcodes and a warning in the admin panel. You lose full control over the layout.
Below are ready-to-use snippets covering everything from declaring support to a live cart in the menu. Each one has been tested on the current WooCommerce version and is organized by section, from foundations to fine-tuning.
💡 Quick overview:
- First, the theme must explicitly declare WooCommerce support; without this, template overrides and some features won't activate
- Place code in a separate file (for example
inc/woocommerce.php) that loads only when the plugin is active - Setting up the shop grid requires two filters: one changes the columns, the other adds a class to
body - Product gallery, zoom, lightbox, and slider are enabled through separate
add_theme_supportcalls - The cart in the menu must update via AJAX; use the
add_to_cart_fragmentsfilter
1. Foundation: declaring support and checking if the plugin is active
First and foremost, the theme must explicitly tell WooCommerce: "I support you." Without this, the plugin won't activate template overrides and displays a warning in the admin panel. Starting with WooCommerce 3.3, a theme without declared support gets simplified rendering via shortcodes. This works but limits your control over the layout.
1 add_action( 'after_setup_theme', function() { 2 add_theme_support( 'woocommerce' ); 3 } );
It's important to use the after_setup_theme hook rather than init, as recommended by the WooCommerce documentation. Since version 3.3+, you can pass grid settings directly:
1 add_action( 'after_setup_theme', function() { 2 add_theme_support( 'woocommerce', array( 3 'thumbnail_image_width' => 150, 4 'single_image_width' => 300, 5 'product_grid' => array( 6 'default_rows' => 3, 7 'min_rows' => 2, 8 'max_rows' => 8, 9 'default_columns' => 4, 10 'min_columns' => 2, 11 'max_columns' => 5, 12 ), 13 ) ); 14 } );
These parameters set default values for the customizer (WooCommerce → Product Images / Product Catalog) and save users from having to configure the grid manually.
Checking if the plugin is active
When you distribute a theme, you can't just dump WooCommerce snippets at the end of functions.php because they'll cause a fatal error if the plugin isn't active. The code should load only when WooCommerce is running:
1 define( 'MYTHEME_WOOCOMMERCE_ACTIVE', class_exists( 'WooCommerce' ) ); 2 3 if ( MYTHEME_WOOCOMMERCE_ACTIVE ) { 4 require_once get_template_directory() . '/inc/woocommerce.php'; 5 }
The constant is evaluated once, and then anywhere in your theme you can wrap calls in if ( MYTHEME_WOOCOMMERCE_ACTIVE ). This is cleaner than calling class_exists or is_plugin_active every time.
2. Managing WooCommerce styles
WooCommerce ships with three CSS files: woocommerce-general, woocommerce-layout, and woocommerce-smallscreen. Ideally, you don't remove them but override them in your theme. That way, third-party WooCommerce extensions that rely on these classes don't break. But if you're writing everything from scratch and want full control, here's how to remove them.
Remove all styles at once:
1 add_filter( 'woocommerce_enqueue_styles', '__return_empty_array' );
Or remove selectively:
1 function mytheme_remove_woo_styles( $styles ) { 2 unset( $styles['woocommerce-general'] ); 3 unset( $styles['woocommerce-layout'] ); 4 unset( $styles['woocommerce-smallscreen'] ); 5 return $styles; 6 } 7 add_filter( 'woocommerce_enqueue_styles', 'mytheme_remove_woo_styles' );
In practice, the second option is more common: you remove woocommerce-layout (the percentage-based grid) but keep woocommerce-general (buttons, notices, forms) and selectively override them with your own CSS using higher specificity.
3. Configuring shop pages
How many products to display
The loop_shop_per_page filter controls the number of products on the shop page and in archives (categories, tags):
1 function mytheme_woo_posts_per_page( $cols ) { 2 return 12; 3 } 4 add_filter( 'loop_shop_per_page', 'mytheme_woo_posts_per_page' );
Number of columns in the grid
Two filters are needed here. Using loop_shop_columns alone isn't enough: unlike WooCommerce shortcodes (which have a wrapper with a columns-N class), shop pages don't have this class. So we add it to body:
1 function mytheme_woo_shop_columns( $columns ) { 2 return 4; 3 } 4 add_filter( 'loop_shop_columns', 'mytheme_woo_shop_columns' ); 5 6 function mytheme_woo_shop_columns_body_class( $classes ) { 7 if ( is_shop() || is_product_category() || is_product_tag() ) { 8 $classes[] = 'columns-4'; 9 } 10 return $classes; 11 } 12 add_filter( 'body_class', 'mytheme_woo_shop_columns_body_class' );
Now in CSS you can write selectors like .columns-4 ul.products li.product, and they'll apply specifically to the shop grid.
Shop title
WooCommerce displays "Shop" as the title on the shop page by default. If your theme already handles archive titles, this duplicate gets in the way:
1 add_filter( 'woocommerce_show_page_title', '__return_false' );
If you use the_archive_title(), you can replace the title with the shop page name (set in the admin panel):
1 function mytheme_woo_archive_title( $title ) { 2 if ( is_shop() && $shop_id = wc_get_page_id( 'shop' ) ) { 3 $title = get_the_title( $shop_id ); 4 } 5 return $title; 6 } 7 add_filter( 'get_the_archive_title', 'mytheme_woo_archive_title' );
4. Product gallery: zoom, lightbox, and slider
Starting with WooCommerce 3.0, a new product gallery was introduced based on FlexSlider, PhotoSwipe, and jQuery Zoom. In versions 3.0-3.2, it's disabled by default and requires explicit enabling. Starting with 3.3, the gallery is enabled for non-WooCommerce themes and disabled for themes that declare support; you enable the components you need yourself:
1 add_theme_support( 'wc-product-gallery-slider' ); 2 add_theme_support( 'wc-product-gallery-zoom' ); 3 add_theme_support( 'wc-product-gallery-lightbox' );
You can enable only some features: for example, slider and zoom, yes, but replace the lightbox with your own Fancybox solution. Each directive loads its own scripts, so don't enable what you don't need.
Number of columns for gallery thumbnails (below the main product image):
1 function mytheme_woo_product_thumbnails_columns() { 2 return 4; 3 } 4 add_action( 'woocommerce_product_thumbnails_columns', 'mytheme_woo_product_thumbnails_columns' );
5. Related products and cross-sells/up-sells
On the product page, WooCommerce displays "Related products" and "You may also like" (up-sells) blocks. Their count and grid are configured separately.
How many related products to show:
1 function mytheme_woo_related_posts_per_page( $args ) { 2 $args['posts_per_page'] = 4; 3 return $args; 4 } 5 add_filter( 'woocommerce_output_related_products_args', 'mytheme_woo_related_posts_per_page' );
Columns for up-sells and related blocks (the same story as with the shop grid): we change both the columns and the class on body:
1 function mytheme_woo_single_loops_columns( $columns ) { 2 return 4; 3 } 4 add_filter( 'woocommerce_up_sells_columns', 'mytheme_woo_single_loops_columns' ); 5 6 function mytheme_woo_related_columns( $args ) { 7 $args['columns'] = 4; 8 return $args; 9 } 10 add_filter( 'woocommerce_output_related_products_args', 'mytheme_woo_related_columns', 10 ); 11 12 function mytheme_woo_single_loops_columns_body_class( $classes ) { 13 if ( is_singular( 'product' ) ) { 14 $classes[] = 'columns-4'; 15 } 16 return $classes; 17 } 18 add_filter( 'body_class', 'mytheme_woo_single_loops_columns_body_class' );
Note that woocommerce_output_related_products_args has two filters with different priorities: mytheme_woo_related_posts_per_page (default 10) changes posts_per_page, and mytheme_woo_related_columns (priority 10) changes columns. They don't conflict because they work with different keys in the same $args array.
6. Pagination and sale badge
You can replace the pagination arrows with your theme's icons:
1 function mytheme_woo_pagination_args( $args ) { 2 $args['prev_text'] = '<i class="fa fa-angle-left"></i>'; 3 $args['next_text'] = '<i class="fa fa-angle-right"></i>'; 4 return $args; 5 } 6 add_filter( 'woocommerce_pagination_args', 'mytheme_woo_pagination_args' );
Replace the Font Awesome classes with your own icon classes or direct SVGs.
The text on the sale badge (Sale!) is easy to override. This is useful for sites in other languages or simply to remove the exclamation mark:
1 function mytheme_woo_sale_flash() { 2 return '<span class="onsale">' . esc_html__( 'Sale', 'woocommerce' ) . '</span>'; 3 } 4 add_filter( 'woocommerce_sale_flash', 'mytheme_woo_sale_flash' );
For other languages, replace 'Sale' with your translation. The translation will be picked up automatically if you have WooCommerce MO files for that language.
7. Dynamic cart in the menu
This is probably the most requested element in any shop theme: a cart icon in the navigation with the current total. The implementation consists of three parts: adding the element to the menu, generating the cart HTML, and AJAX updating.
1 // Adding the link to the menu 2 function mytheme_add_menu_cart_item( $items, $args ) { 3 if ( $args->theme_location === 'primary' ) { 4 $css_class = 'menu-item menu-item-type-cart menu-item-type-woocommerce-cart'; 5 if ( is_cart() ) { 6 $css_class .= ' current-menu-item'; 7 } 8 $items .= '<li class="' . esc_attr( $css_class ) . '">'; 9 $items .= mytheme_menu_cart_item(); 10 $items .= '</li>'; 11 } 12 return $items; 13 } 14 add_filter( 'wp_nav_menu_items', 'mytheme_add_menu_cart_item', 10, 2 ); 15 16 // Cart HTML 17 function mytheme_menu_cart_item() { 18 $cart_count = WC()->cart->cart_contents_count; 19 $css_class = 'wpex-menu-cart-total wpex-cart-total-' . intval( $cart_count ); 20 $url = $cart_count ? WC()->cart->get_cart_url() : wc_get_page_permalink( 'shop' ); 21 $html = WC()->cart->get_cart_total(); 22 $html = str_replace( 'amount', '', $html ); 23 24 return '<a href="' . esc_url( $url ) . '" class="' . esc_attr( $css_class ) . '">' 25 . '<span class="cart-icon"></span>' 26 . wp_kses_post( $html ) 27 . '</a>'; 28 } 29 30 // AJAX fragment update 31 function mytheme_menu_cart_link_fragments( $fragments ) { 32 $fragments['.wpex-menu-cart-total'] = mytheme_menu_cart_item(); 33 return $fragments; 34 } 35 add_filter( 'add_to_cart_fragments', 'mytheme_menu_cart_link_fragments' );
Critical point: **do not wrap these functions in **is_admin(). Cart AJAX requests come through admin-ajax.php, and if the function isn't available in the admin context, the price update in the menu simply won't work. Replace theme_location with your menu identifier (primary in the example).
8. Alternative approach: the woocommerce.php template
The hooks described above are the most flexible and update-safe method. But if you need to radically overhaul the entire shop and product page wrapper, there's a path through a separate template.
Create a woocommerce.php file in your theme's root (a copy of page.php) and replace the main loop with a call to woocommerce_content():
1 <?php 2 get_header(); ?> 3 4 <div id="primary" class="content-area"> 5 <main id="main" class="site-main"> 6 <?php woocommerce_content(); ?> 7 </main> 8 </div> 9 10 <?php get_footer(); 11
This method is simpler, but it comes at a cost: one template for all WooCommerce pages (the shop, categories, and product detail). Fine-tuning (different grids, different sidebars on different pages) becomes harder. Choose this approach only if your current theme really doesn't get along with WooCommerce at the markup level, and you're prepared to accept the limitations.
⁉️🤔 Frequently asked questions
Is it mandatory to remove WooCommerce's default styles?
Not mandatory. A safer approach is to keep them and override selectors in your theme with higher specificity. This preserves compatibility with extensions that rely on standard WooCommerce classes. Complete removal (
__return_empty_array) is justified only if you're building a shop theme from scratch and know for certain that no third-party plugins will add elements styled for WooCommerce. In most cases, selectively disablingwoocommerce-layout(the percentage grid conflicts with flexbox and grids in modern themes) while keepingwoocommerce-generalandwoocommerce-smallscreenand carefully overriding them through the CSS cascade is enough. This gives you a clean grid without losing compatibility.
Why doesn't changing columns in the shop work?
Because the loop_shop_columns filter alone isn't enough. WooCommerce uses .columns-N classes on the wrapper to make the grid work, and they're automatically added only to shortcodes ([products columns="4"]). On shop pages and archives, this wrapper doesn't exist, so you need to add the class to body via body_class. The two filters from section 3 solve this problem completely.
The
loop_shop_columnsfilter changes the PHP variable that WooCommerce passes to the template, but the CSS grid depends on the wrapper class. Without thecolumns-4class onbody, yourul.products li.productstyles have no context selector and either don't apply or hit all product lists across the site.
Do I need to rebuild the theme after a WooCommerce update?
If you're using hooks (all snippets from this article), no. Hooks change extremely rarely. If you're overriding templates (files from woocommerce/templates/), yes, you need to compare your copies with the current versions after each major update. This is exactly why the official documentation recommends hooks as the preferred customization method.
Hooks are a contract. WooCommerce guarantees their stability between versions. Templates are an implementation that can change at any time: new actions get added, markup changes, classes get renamed. In the five years these hooks have existed (
loop_shop_columnsandwoocommerce_output_related_products_args), their signature has never changed, while thecontent-product.phpfile has been updated dozens of times during the same period.
Can I use is_admin() to protect cart functions?
No, and this is a common trap. WooCommerce AJAX requests (add to cart, update quantity) are processed through admin-ajax.php; in this context, is_admin() returns true. If you wrap cart functions in if ( ! is_admin() ), AJAX fragment updates (add_to_cart_fragments) will stop working, and the total in the cart icon won't change without a page reload.
Wrapping in
! is_admin()should only be used for things that shouldn't run in the admin (front-end HTML output), but AJAX handler functions must always be available. For the code in section 7, don't useis_admin()at all. WooCommerce itself calls the necessary callbacks through its AJAX handler, and the restriction will break the logic.
How do I add WooCommerce support to a block theme (FSE)?
For block themes, the support declaration hasn't changed: the same add_theme_support('woocommerce'). But instead of hooks and overriding PHP templates, you work in the Site Editor: WooCommerce provides blocks (Products, Product Search, Cart, Checkout) that you insert into templates via Appearance → Editor. Fine-tuning is done through theme.json and CSS variables.
Block themes fundamentally change the approach: you don't write PHP hooks for layout but visually assemble pages from ready-made WooCommerce blocks. But the foundation (
add_theme_supportinfunctions.php) remains the same. For fine adjustments that can't be done with blocks (for example, custom text on the "Add to Cart" button), the good old filters from this article work in FSE themes too.
Which strategy to choose for your project
If you're enhancing an existing theme, start with steps 1 and 2: declare support and configure the grid. In most cases, this is enough to make the shop look decent. Then add only the fixes that solve specific problems: a broken gallery, ugly pagination, missing cart in the menu.
If you're writing a theme from scratch for sale or distribution, go through the entire list. Each item adds something users expect from a shop theme "out of the box": a customizable grid, zoom on products, a live cart icon. Put the code in inc/woocommerce.php and load it based on the constant. This way the theme works equally well as both a blog and a shop.
The starting point is always the same:
1 add_action( 'after_setup_theme', function() { 2 add_theme_support( 'woocommerce' ); 3 } );
Eight lines. Everything else is details that distinguish a "compatible" theme from a "purpose-built" one.



