I sell products where some variations are physical versions requiring sales tax and some are digital versions requiring no sales tax.
Summary
WooCommerce supports per-variation tax_class but hardcodes tax_status to always inherit from the parent variable product. There is no way to mark one variation as taxable and another as non-taxable within the same product. The fix
requires the same pattern already used for tax_class.
—
Use Case
A store sells a product with both a physical and a digital variation (e.g. printed manual + downloadable PDF, physical event ticket + digital streaming pass, hardware part + software licence). Physical goods are taxable; digital goods are not. Because they share a parent variable product, both inherit the parent’s tax_status — there is no way to exempt the digital variation from tax without splitting them into entirely separate products.
—
Root Cause — Exact Code Location
File: includes/data-stores/class-wc-product-variation-data-store-cpt.php
The variation data store reads _tax_class from the variation’s own postmeta (it is in $meta_key_to_props, line ~392):
// Line ~392 — variation’s OWN postmeta is read:
‘_tax_class’ => ‘tax_class’,
But _tax_status is only in $parent_meta_key_to_props (~line 459) and then explicitly forced onto the variation from the parent at line ~481:
// Lines ~478-481 — comment is WooCommerce’s own:
// Pull data from the parent when there is no user-facing way to set props.
$product->set_sold_individually( $parent_data[‘sold_individually’] );
$product->set_tax_status( $parent_data[‘tax_status’] ); // ← always parent’s value
This call to set_tax_status() overwrites anything previously set on the variation object, making it impossible to
store or read a variation-level tax status.
File: includes/class-wc-product-variation.php
get_tax_class() (~line 299) has a full per-variation override with parent fallback:
// Line ~299 — tax_class correctly falls back to parent:
public function get_tax_class( $context = ‘view’ ) {
…
if ( array_key_exists( ‘tax_class’, $this->data ) ) {
$value = …
if ( ‘parent’ === $value ) {
$value = $this->parent_data[‘tax_class’]; // inherit when set to ‘parent’
}
}
}
There is no equivalent get_tax_status() override in this class. tax_status is not in $this->data at all (line ~44 —
only tax_class is declared), so the value is always whatever the data store last set via set_tax_status() — which is always the parent’s.
—
Requested Change
Apply the exact same pattern used for tax_class to tax_status:
1. class-wc-product-variation-data-store-cpt.php
Move _tax_status from $parent_meta_key_to_props into $meta_key_to_props so it is read from the variation’s own
postmeta. Add a sentinel fallback identical to the one for tax_class at line ~405:
// After reading variation meta:
$variation_data[‘tax_status’] = ! metadata_exists( ‘post’, $id, ‘_tax_status’ )
? ‘parent’ // new sentinel: inherit from parent when not explicitly set
: $variation_data[‘tax_status’];
Remove line ~481 ($product->set_tax_status( $parent_data[‘tax_status’] )) or guard it with the sentinel check.
2. class-wc-product-variation.php
Add tax_status to $this->data (line ~44) defaulting to ‘parent’ (matching tax_class’s default), and add a
get_tax_status() override mirroring the existing get_tax_class():
public function get_tax_status( $context = ‘view’ ) {
$value = array_key_exists( ‘tax_status’, $this->changes )
? $this->changes[‘tax_status’]
: $this->data[‘tax_status’];
if ( ‘parent’ === $value || ” === $value ) {
$value = $this->parent_data[‘tax_status’];
}
return apply_filters( $this->get_hook_prefix() . ‘tax_status’, $value, $this );
}
3. Admin UI
Add a Tax Status dropdown to the variation edit panel (the same panel that already shows the Tax Class dropdown),
defaulting to — Same as parent —. No variation-level UI currently exists for this field.
—
Why This Is Consistent With Existing Behaviour
tax_class and tax_status are conceptually paired — both control how tax is applied. WooCommerce already decided that tax_class should be overridable per variation. The only reason tax_status is not is the comment in the data store:
“when there is no user-facing way to set props” — which is a UI gap, not a design principle. The plumbing change is
minimal and follows an already-established pattern in the same file.
—
What Does Not Work As A Workaround
Hooking woocommerce_product_get_tax_status to return variation-level meta works at the PHP level but conflicts with tax calculation extensions that bypass the filter or cache the value, and is not a pattern WooCommerce documents or supports. It is a hack that should not be necessary.
Open
Last updated: May 8, 2026
0 comments
Log in to comment on this feature request.