Skip to main content
Being Idea Innovations
Add a Live WooCommerce Cart Icon to Your Header (No Plugin)
Back to Blog

Add a Live WooCommerce Cart Icon to Your Header (No Plugin)

28 August 20185 min readWordPress
Share:

A cart icon showing item count and total is standard on any WooCommerce store. You do not need a plugin for it — about forty lines in your theme covers it.

The catch, and the reason most tutorials on this produce something subtly broken, is that the count must update when a customer adds something without reloading the page. That requires WooCommerce's fragment system, which the older guides omit entirely.

What the old code got wrong

The version widely copied — including the previous version of this article — uses APIs that have since been removed:

// All deprecated or removed
global $woocommerce;
$cart_url  = $woocommerce->cart->get_cart_url();          // removed in WC 3.0
$shop_page = get_permalink( woocommerce_get_page_id( 'shop' ) );  // deprecated
wp_enqueue_style( 'font-awesome', '//maxcdn.bootstrapcdn.com/...' ); // CDN is gone
OldCurrent
global $woocommerce;WC()
$woocommerce->cart->get_cart_url()wc_get_cart_url()
woocommerce_get_page_id('shop')wc_get_page_id('shop')
in_array('woocommerce/woocommerce.php', …)class_exists('WooCommerce')

The MaxCDN Font Awesome URL in that snippet stopped resolving when MaxCDN shut down, so the icon simply never appears. An inline SVG avoids the dependency entirely — one fewer request, no external host, and it inherits your text colour.

The cart icon

Put this in your child theme's functions.php, or better, a small site-specific plugin so it survives a theme change:

<?php
/**
 * Renders the cart link. Wrapped in a fragment container so WooCommerce can
 * swap it out over AJAX when the cart changes.
 */
function bi_cart_icon_html(): string {
    if ( ! class_exists( 'WooCommerce' ) || is_null( WC()->cart ) ) {
        return '';
    }

    $count = WC()->cart->get_cart_contents_count();
    $total = WC()->cart->get_cart_subtotal();

    $url   = $count > 0 ? wc_get_cart_url() : get_permalink( wc_get_page_id( 'shop' ) );
    $label = $count > 0
        ? sprintf( _n( 'View cart, %d item', 'View cart, %d items', $count, 'being-idea' ), $count )
        : __( 'Start shopping', 'being-idea' );

    ob_start();
    ?>
    <a class="bi-cart" href="<?php echo esc_url( $url ); ?>" aria-label="<?php echo esc_attr( $label ); ?>">
        <svg class="bi-cart__icon" width="22" height="22" viewBox="0 0 24 24" fill="none"
             stroke="currentColor" stroke-width="1.8" stroke-linecap="round"
             stroke-linejoin="round" aria-hidden="true" focusable="false">
            <circle cx="9" cy="21" r="1" />
            <circle cx="20" cy="21" r="1" />
            <path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6" />
        </svg>

        <?php if ( $count > 0 ) : ?>
            <span class="bi-cart__count"><?php echo esc_html( $count ); ?></span>
            <span class="bi-cart__total"><?php echo wp_kses_post( $total ); ?></span>
        <?php endif; ?>
    </a>
    <?php
    return ob_get_clean();
}

Note wp_kses_post() rather than esc_html() on the total — get_cart_subtotal() returns markup containing the currency symbol span, so escaping it as plain text would print the HTML tags.

The is_null( WC()->cart ) guard matters. The cart object does not exist during REST requests, in the admin, or on some cached early hooks, and calling a method on null is a fatal error that takes down the whole page.

Making it update over AJAX

This is the part everyone leaves out. When a customer clicks "Add to cart" on a shop archive, WooCommerce adds the item over AJAX with no page reload — so your header count stays stale until they navigate.

WooCommerce solves this with fragments: you register a chunk of HTML identified by a CSS selector, and after any cart change WooCommerce replaces that element with freshly rendered markup.

/**
 * Register the cart icon as a refreshable fragment. The array key is the CSS
 * selector WooCommerce will replace — it must match the wrapper exactly.
 */
add_filter( 'woocommerce_add_to_cart_fragments', 'bi_cart_icon_fragment' );
function bi_cart_icon_fragment( array $fragments ): array {
    ob_start();
    echo '<div class="bi-cart-wrap">' . bi_cart_icon_html() . '</div>';
    $fragments['div.bi-cart-wrap'] = ob_get_clean();

    return $fragments;
}

Then output the wrapper in your header template:

<div class="bi-cart-wrap"><?php echo bi_cart_icon_html(); ?></div>

Two rules make or break this. The selector must match the wrapper element exactlydiv.bi-cart-wrap and a wrapper of <div class="bi-cart-wrap">. A mismatch fails silently. And the fragment must include the wrapper itself, not just its contents, because WooCommerce replaces the matched element outright; return only the inner HTML and the wrapper disappears after the first update.

Fragments depend on the wc-cart-fragments script, which WooCommerce enqueues by default. Some speed-optimisation plugins dequeue it to save a request — if your count stops updating, check that first.

Adding it to a nav menu

If you want the icon as the last item in a menu rather than free-standing:

add_filter( 'wp_nav_menu_items', 'bi_cart_menu_item', 10, 2 );
function bi_cart_menu_item( string $items, stdClass $args ): string {
    if ( 'primary' !== $args->theme_location ) {
        return $items;
    }

    return $items . '<li class="menu-item bi-cart-item"><div class="bi-cart-wrap">'
        . bi_cart_icon_html() . '</div></li>';
}

Check $args->theme_location, not the menu name — names are editable by the site owner and will not survive a rename.

Styling

.bi-cart {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  padding: .5rem .75rem;   /* keeps the tap target near 44px */
  color: inherit;
  text-decoration: none;
}

.bi-cart__count {
  position: absolute;
  top: 2px;
  inset-inline-start: 24px;
  min-width: 1.15rem;
  height: 1.15rem;
  padding: 0 .3rem;
  border-radius: 999px;
  background: #DC2626;
  color: #fff;
  font-size: .7rem;
  font-weight: 700;
  line-height: 1.15rem;
  text-align: center;
}

.bi-cart__total { font-size: .875rem; font-weight: 600; }

@media (max-width: 640px) {
  .bi-cart__total { display: none; }   /* icon and badge only on small screens */
}

Using inset-inline-start rather than left means the badge lands correctly in right-to-left languages without a separate stylesheet.

Page caching

If you run full-page caching, the cart icon is a problem: the first visitor's cart contents get baked into the cached HTML and served to everyone.

Fragments largely solve this, since the AJAX call refreshes the icon on load for each visitor. But there is a visible flash of the wrong count before it updates. To avoid that, render the wrapper empty server-side and let the fragment fill it, or exclude the header from caching. Most WooCommerce-aware caching plugins handle this already — verify rather than assume.

Summary

  • Use WC(), wc_get_cart_url() and wc_get_page_id() — the global $woocommerce forms are long gone.
  • Guard with class_exists('WooCommerce') and a null check on WC()->cart.
  • Register a fragment so the count updates without a reload — this is the step most guides miss.
  • Match the fragment selector exactly and include the wrapper in the returned HTML.
  • Use an inline SVG instead of an icon font.
  • Escape output: esc_url, esc_attr, esc_html, and wp_kses_post for the formatted total.
Share:

Written by

Amit Verma

Founder / Senior Software Engineer

Amit leads engineering at Being Idea. With 15+ years building scalable software products across global markets, he drives architecture decisions and engineering culture across every engagement.

More articles by Amit

Want to talk tech?

We ship software that scales. Let's work together.

No long-term contracts
Senior engineers only
US · AU · NZ timezone coverage
14-day trial on retainers