The Product Labels for WooCommerce plugin (EcoWebs) allows custom badges to be applied to product thumbnails in WooCommerce shop pages. Configuration is non-obvious in several areas, and client self-editing frequently breaks badge setups. This article documents key configuration patterns, known failure modes, and workarounds discovered during the [1] project.
The plugin supports two badge tiers:
Badges are not applied directly to products. Instead, you create a Product List that defines which products receive a badge, then assign a badge to that list.
Product lists can be scoped by:
- Category — any product in a given WooCommerce category
- Tag
- Manual product selection — type product names to add individually
Gotcha: The category search field in the Product List UI does not auto-populate. You must start typing the category name before results appear.
The "Sale" badge is triggered automatically by WooCommerce's on-sale flag (i.e., when a product has a reduced price set). It does not require a Product List. This means it will appear on any discounted product regardless of other badge configuration.
When two badges are assigned to the same position (e.g., both top-left), they overlap. The plugin's UI exposes a Z-Index field per badge, but this does not reliably control stacking order when the two badges are of different types (e.g., one standard, one advanced).
Root cause: Each badge type renders in its own absolutely-positioned container. CSS z-index only controls stacking within the same stacking context. Badges in separate containers are unaffected by each other's z-index values.
Give each badge a distinct position:
This avoids the overlap entirely and communicates both statuses clearly. The client (Flynn) confirmed this was acceptable.
If both badges are the same type, z-index can work:
10)5)Note: the Sale badge had a default Z-Index of 99 — this must be lowered explicitly if you want another badge to appear above it.
To apply a badge to all products in a WooCommerce category (e.g., "Open Box"):
CategoryBadges do not appear when a user first loads a filtered shop page (e.g., clicking the "Open Box" category filter). They only appear after a manual browser refresh.
Badges are injected into the DOM via JavaScript after the initial page load. When [2] serves a cached page, the badge injection script may run late or not at all on the first render. This is compounded by WooCommerce's AJAX-based product filtering, which re-renders the product grid without triggering a full page load.
Fetch the raw page HTML (e.g., via curl or browser dev tools → View Source). If badge markup is absent from the initial HTML, the badges are being injected client-side and are subject to this race condition.
A custom Must Use (MU) plugin is being developed to address this. The approach uses a MutationObserver to detect when the product grid is re-rendered by AJAX filtering and re-trigger badge injection at that point.
Status as of 2026-04-05: MU plugin in development. See [1] action items.
Key settings under Badges → Settings → General:
| Setting | Purpose |
|---|---|
| Disable Default Badge | Suppresses WooCommerce's built-in "Sale" / "Out of Stock" labels. Enable this when using custom badges to avoid duplication. |
| Use WooCommerce Shop Hook | Default rendering method; appends badges to product thumbnails |
| Enable Product Loop Hook | Fallback if badges aren't displaying; some themes override the default hook. Warning: enabling this when badges are already showing can cause duplicates. |
| Enable Product Title Hook | Secondary fallback; generally not needed |
A recurring issue on the Flynn account is the client modifying badge settings directly in WordPress. Because the plugin's configuration is stateful (badges, product lists, and hook settings interact), a single inadvertent change can break badge display entirely and leave no obvious audit trail.
Recommended approach: Communicate clearly that badge configuration changes should be submitted as requests rather than made directly. Estimated time to diagnose a client-introduced regression: 1–2 hours.