B2B & wholesaleTutorial

Prijs exclusief btw op de productpagina: een block dat met de variant meebeweegt

Diek ThunnissenDiek Thunnissen17 jul 2025bijgewerkt 8 sep 20269 min leestijd

Een zakelijke koper rekent met netto prijzen, je shop toont bruto. Deze snippet zet de prijs exclusief btw onder de gewone prijs, met het percentage instelbaar in de theme editor, en rekent mee als de klant een andere variant kiest.

Het btw-block in vijf minuten, van snippet tot instellingen in de editor.

Verkoop je aan bedrijven én consumenten, dan ziet de zakelijke koper op je productpagina een prijs waar hij niets mee kan. Hij moet een budget verantwoorden of een offerte maken en wil het bedrag zonder btw zien. De vraag "hoeveel is het zonder btw?" komt dan per mail, en jij rekent het uit.

Dit block lost dat op aan de voorkant. Het toont de prijs exclusief btw direct onder de gewone prijs, met een instelbaar percentage, optioneel een label "Zakelijk" en de keuze om de prijs inclusief btw ernaast of eronder te laten staan. Kiest de klant een andere variant, dan rekent het block mee zonder pagina-refresh.

Het structurele antwoord voor een echte B2B-shop is native B2B met Companies en catalogs, waar prijzen per bedrijf exclusief btw kunnen zijn. Dit block is de lichte variant voor een gemengde shop op één storefront.

Stap 1: de snippet

Online Store > Themes > Actions > Edit code. Maak in de map Snippets een nieuwe snippet met de naam price-ex-btw en plak deze code:

279 regels codeToon code +
{% comment %}  ⚡️ Prijs Exclusief BTW Snippet by Diek Thunnissen

  - Werkt met echte product variants (variant-selects component)
  - Werkt met product variations (verschillende producten)
  - Detecteert beide systemen automatisch
{% endcomment %}
{%- liquid
  # Bereken prijzen excl. BTW
  assign btw_percentage = block.settings.btw_percentage | times: 1.0 | divided_by: 100.0
  assign btw_multiplier = 1.0 | plus: btw_percentage

  # Bereken de afrondfactor
  assign decimal_places = block.settings.decimal_places | default: 2
  assign rounding_factor = decimal_places | prepend: '1' | append: '0' | times: 1

  # Voor product variations gebruiken we altijd het huidige product
  assign target_product = product

  # Verwerk prijzen voor het huidige product
  if product.price_varies
    assign regular_price_min = product.price_min | divided_by: btw_multiplier | times: rounding_factor | round | divided_by: rounding_factor
    assign regular_price_max = product.price_max | divided_by: btw_multiplier | times: rounding_factor | round | divided_by: rounding_factor
  else
    assign regular_price = product.price | divided_by: btw_multiplier | times: rounding_factor | round | divided_by: rounding_factor
  endif

  # Als er een selected variant is, gebruik die prijs
  if product.selected_or_first_available_variant
    assign variant_price = product.selected_or_first_available_variant.price | divided_by: btw_multiplier | times: rounding_factor | round | divided_by: rounding_factor
  endif
-%}

<style>
  .zakelijk-btw-container {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-top: {{ block.settings.spacing | default: 8 }}px;
    margin-bottom: 12px;
  }

  .zakelijk-tag {
    display: inline-block;
    background-color: {{ block.settings.badge_bg_color | default: '#a18367' }};
    color: {{ block.settings.badge_text_color | default: '#ffffff' }};
    font-size: 12px;
    padding: 3px 10px;
    border-radius: {{ block.settings.badge_radius | default: 3 }}px;
  }

  .ex-btw-price {
    font-size: {{ block.settings.font_size | default: 14 }}px;
    color: {{ block.settings.price_color | default: '#000000' }};
    font-weight: {{ block.settings.price_weight | default: 'bold' }};
  }

  .ex-btw-label {
    color: {{ block.settings.label_color | default: '#707070' }};
    font-weight: {{ block.settings.label_weight | default: 'normal' }};
    font-size: {{ block.settings.font_size | default: 14 }}px;
    {% if block.settings.label_style == 'italic' %}font-style: italic;{% endif %}
  }

  {% if block.settings.custom_css != blank %}
    {{ block.settings.custom_css }}
  {% endif %}
</style>

<div class="zakelijk-btw-container {{ block.settings.custom_class }}" id="btw-price-container">
  {% if block.settings.show_badge %}
    <span class="zakelijk-tag">{{ block.settings.badge_text | default: 'Zakelijk' }}</span>
  {% endif %}

  <span class="ex-btw-label">{{ block.settings.label_text | default: 'Excl. BTW:' }}</span>

  {%- if product.price_varies -%}
    <span class="ex-btw-price" id="btw-price-display">
      {%- if block.settings.price_format == 'from' -%}
        {{ block.settings.from_text | default: 'Vanaf' }} {{ regular_price_min | money }}
      {%- else -%}
        {{ regular_price_min | money }}{{ block.settings.range_separator | default: ' - ' }}{{ regular_price_max | money }}
      {%- endif -%}
    </span>
  {%- else -%}
    <span class="ex-btw-price" id="btw-price-display">
      {%- if variant_price -%}
        {{ variant_price | money }}
      {%- else -%}
        {{ regular_price | money }}
      {%- endif -%}
    </span>
  {%- endif -%}

  {% if block.settings.show_inc_btw %}
    {% if block.settings.inc_btw_placement == 'inline' %}
      <span class="ex-btw-label" style="margin-left: 8px;">{{ block.settings.inc_btw_label | default: 'Incl. BTW:' }}</span>
      <span class="ex-btw-price" id="inc-btw-price-display">
        {%- if product.price_varies -%}
          {%- if block.settings.price_format == 'from' -%}
            {{ block.settings.from_text | default: 'Vanaf' }} {{ product.price_min | money }}
          {%- else -%}
            {{ product.price_min | money }}{{ block.settings.range_separator | default: ' - ' }}{{ product.price_max | money }}
          {%- endif -%}
        {%- else -%}
          {%- if product.selected_or_first_available_variant -%}
            {{ product.selected_or_first_available_variant.price | money }}
          {%- else -%}
            {{ product.price | money }}
          {%- endif -%}
        {%- endif -%}
      </span>
    {% endif %}
  {% endif %}
</div>

{% if block.settings.show_inc_btw and block.settings.inc_btw_placement == 'under' %}
  <div class="zakelijk-btw-container {{ block.settings.custom_class }}" style="margin-top: 4px;">
    <span class="ex-btw-label">{{ block.settings.inc_btw_label | default: 'Incl. BTW:' }}</span>
    <span class="ex-btw-price" id="inc-btw-price-display-under">
      {%- if product.price_varies -%}
        {%- if block.settings.price_format == 'from' -%}
          {{ block.settings.from_text | default: 'Vanaf' }} {{ product.price_min | money }}
        {%- else -%}
          {{ product.price_min | money }}{{ block.settings.range_separator | default: ' - ' }}{{ product.price_max | money }}
        {%- endif -%}
      {%- else -%}
        {%- if product.selected_or_first_available_variant -%}
          {{ product.selected_or_first_available_variant.price | money }}
        {%- else -%}
          {{ product.price | money }}
        {%- endif -%}
      {%- endif -%}
    </span>
  </div>
{% endif %}

{% comment %} JavaScript voor variant price updates{% endcomment %}
{% if template == 'product' %}
<script>
document.addEventListener('DOMContentLoaded', function() {
  const BTW_MULTIPLIER = {{ btw_multiplier }};
  const btwPriceDisplay = document.getElementById('btw-price-display');
  const incBtwPriceDisplay = document.getElementById('inc-btw-price-display');
  const incBtwPriceDisplayUnder = document.getElementById('inc-btw-price-display-under');

  if (!btwPriceDisplay) return;

  // Functie om BTW prijzen te updaten
  function updateBTWPrices(variantId) {
    if (!variantId) return;

    // Zoek variant data uit het JSON script in variant-selects
    const variantScript = document.querySelector('variant-selects script[data-selected-variant]');
    let variantData = null;

    if (variantScript) {
      try {
        const selectedVariant = JSON.parse(variantScript.textContent);
        if (selectedVariant && selectedVariant.id.toString() === variantId.toString()) {
          variantData = selectedVariant;
        }
      } catch (e) {
        console.log('Error parsing selected variant:', e);
      }
    }

    // Als geen data van script, zoek alle variants
    if (!variantData) {
      // Zoek in window object
      if (window.productData && window.productData.variants) {
        variantData = window.productData.variants.find(v => v.id.toString() === variantId.toString());
      }

      // Zoek in andere mogelijke locaties
      if (!variantData && window.product && window.product.variants) {
        variantData = window.product.variants.find(v => v.id.toString() === variantId.toString());
      }
    }

    if (variantData && variantData.price) {
      const variantPrice = parseInt(variantData.price);
      const exBtwPrice = Math.round(variantPrice / BTW_MULTIPLIER);

      // Update BTW prijs
      if (typeof Shopify !== 'undefined' && Shopify.formatMoney) {
        btwPriceDisplay.textContent = Shopify.formatMoney(exBtwPrice, "{{ shop.money_format }}");
        if (incBtwPriceDisplay) {
          incBtwPriceDisplay.textContent = Shopify.formatMoney(variantPrice, "{{ shop.money_format }}");
        }
        if (incBtwPriceDisplayUnder) {
          incBtwPriceDisplayUnder.textContent = Shopify.formatMoney(variantPrice, "{{ shop.money_format }}");
        }
      } else {
        // Fallback zonder Shopify.formatMoney
        const formattedExBtw = '€' + (exBtwPrice / 100).toFixed(2).replace('.', ',');
        const formattedInc = '€' + (variantPrice / 100).toFixed(2).replace('.', ',');

        btwPriceDisplay.textContent = formattedExBtw;
        if (incBtwPriceDisplay) incBtwPriceDisplay.textContent = formattedInc;
        if (incBtwPriceDisplayUnder) incBtwPriceDisplayUnder.textContent = formattedInc;
      }
    }
  }

  // Luister naar variant-selects component
  const variantSelects = document.querySelector('variant-selects');
  if (variantSelects) {

    // Luister naar radio button changes (swatch picker)
    const radioInputs = variantSelects.querySelectorAll('input[type="radio"]');
    radioInputs.forEach(input => {
      input.addEventListener('change', function() {
        setTimeout(() => {
          // Check of er een hidden input is met variant ID
          const hiddenInput = document.querySelector('input[name="id"][type="hidden"], select[name="id"]');
          if (hiddenInput && hiddenInput.value) {
            updateBTWPrices(hiddenInput.value);
          }
        }, 100);
      });
    });

    // Luister naar select changes (dropdown picker)
    const selectInputs = variantSelects.querySelectorAll('select');
    selectInputs.forEach(select => {
      select.addEventListener('change', function() {
        setTimeout(() => {
          const hiddenInput = document.querySelector('input[name="id"][type="hidden"], select[name="id"]');
          if (hiddenInput && hiddenInput.value) {
            updateBTWPrices(hiddenInput.value);
          }
        }, 100);
      });
    });

    // Observeer DOM changes voor dynamische updates
    const observer = new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        if (mutation.type === 'childList' || mutation.type === 'attributes') {
          const hiddenInput = document.querySelector('input[name="id"][type="hidden"], select[name="id"]');
          if (hiddenInput && hiddenInput.value) {
            updateBTWPrices(hiddenInput.value);
          }
        }
      });
    });

    observer.observe(variantSelects, {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: ['data-selected-variant']
    });
  }

  // Luister naar custom events
  document.addEventListener('variant:change', function(event) {
    if (event.detail && event.detail.variant && event.detail.variant.id) {
      updateBTWPrices(event.detail.variant.id);
    }
  });

  // Fallback: observeer prijs wijzigingen
  const priceElement = document.querySelector('.price, .product__price, [data-price]');
  if (priceElement) {
    const priceObserver = new MutationObserver(() => {
      setTimeout(() => {
        const hiddenInput = document.querySelector('input[name="id"][type="hidden"], select[name="id"]');
        if (hiddenInput && hiddenInput.value) {
          updateBTWPrices(hiddenInput.value);
        }
      }, 50);
    });

    priceObserver.observe(priceElement, { childList: true, subtree: true });
  }
});
</script>
{% endif %}

Drie delen. Het Liquid-blok bovenin rekent de prijzen exclusief btw uit, ook voor producten met een prijsbereik. De CSS zet de opmaak uit de block-instellingen. Het JavaScript onderaan luistert naar de variant-picker van het thema en herrekent de prijs bij elke wissel. Het probeert meerdere plekken om de variantdata te vinden, omdat thema's die verschillend opslaan.

Stap 2: het block in main-product.liquid

Open main-product.liquid en zoek {%- case block.type -%}. Direct eronder:

{%- when 'price_ex_btw' -%}
    <div class="product-info__block product-info__block--sm product-price-ex-btw" {{ block.shopify_attributes }}>
      {% render 'price-ex-btw', product: product, block: block, use_variant: true, current_variant: current_variant %}
    </div>

Stap 3: de instellingen in het schema

Scroll naar {% schema %} en plak dit direct na "blocks": [ :

229 regels codeToon code +
{
    "type": "price_ex_btw",
    "name": "⚡️ Prijs Excl. BTW",
    "limit": 1,
    "settings": [
      {
        "type": "header",
        "content": "⚡️ Prijs Exclusief BTW by Diek Thunnissen",
        "info": "Voeg prijzen exclusief BTW toe aan je product pagina."
      },
      {
        "type": "range",
        "id": "btw_percentage",
        "min": 1,
        "max": 30,
        "step": 1,
        "unit": "%",
        "label": "BTW percentage",
        "default": 21
      },
      {
        "type": "text",
        "id": "label_text",
        "label": "Label tekst",
        "default": "Excl. BTW:"
      },
      {
        "type": "checkbox",
        "id": "show_inc_btw",
        "label": "Toon ook prijs incl. BTW",
        "default": true
      },
      {
        "type": "text",
        "id": "inc_btw_label",
        "label": "Incl. BTW label",
        "default": "Incl. BTW:"
      },
      {
        "type": "select",
        "id": "inc_btw_placement",
        "label": "Incl. BTW weergave",
        "options": [
          {
            "value": "under",
            "label": "Onder excl. BTW"
          },
          {
            "value": "inline",
            "label": "Naast excl. BTW"
          }
        ],
        "default": "under"
      },
      {
        "type": "select",
        "id": "price_format",
        "label": "Prijsweergave bij prijsbereik",
        "options": [
          {
            "value": "from",
            "label": "Vanaf"
          },
          {
            "value": "range",
            "label": "Bereik (min-max)"
          }
        ],
        "default": "from"
      },
      {
        "type": "text",
        "id": "from_text",
        "label": "Vanaf tekst",
        "default": "Vanaf"
      },
      {
        "type": "text",
        "id": "range_separator",
        "label": "Bereik separator",
        "default": " - "
      },
      {
        "type": "header",
        "content": "Badge"
      },
      {
        "type": "checkbox",
        "id": "show_badge",
        "label": "Toon badge",
        "default": false
      },
      {
        "type": "text",
        "id": "badge_text",
        "label": "Badge tekst",
        "default": "Zakelijk"
      },
      {
        "type": "color",
        "id": "badge_bg_color",
        "label": "Badge achtergrondkleur",
        "default": "#f2f2f2"
      },
      {
        "type": "color",
        "id": "badge_text_color",
        "label": "Badge tekstkleur",
        "default": "#555555"
      },
      {
        "type": "range",
        "id": "badge_radius",
        "min": 0,
        "max": 20,
        "step": 1,
        "unit": "px",
        "label": "Badge hoekradius",
        "default": 4
      },
      {
        "type": "header",
        "content": "Styling"
      },
      {
        "type": "range",
        "id": "spacing",
        "min": 0,
        "max": 40,
        "step": 2,
        "unit": "px",
        "label": "Verticale ruimte",
        "default": 8
      },
      {
        "type": "range",
        "id": "font_size",
        "min": 10,
        "max": 24,
        "step": 1,
        "unit": "px",
        "label": "Lettergrootte",
        "default": 14
      },
      {
        "type": "color",
        "id": "label_color",
        "label": "Label kleur",
        "default": "#707070"
      },
      {
        "type": "color",
        "id": "price_color",
        "label": "Prijs kleur",
        "default": "#000000"
      },
      {
        "type": "select",
        "id": "label_weight",
        "label": "Label gewicht",
        "options": [
          {
            "value": "normal",
            "label": "Normaal"
          },
          {
            "value": "bold",
            "label": "Vet"
          },
          {
            "value": "500",
            "label": "Medium"
          }
        ],
        "default": "normal"
      },
      {
        "type": "select",
        "id": "label_style",
        "label": "Label stijl",
        "options": [
          {
            "value": "normal",
            "label": "Normaal"
          },
          {
            "value": "italic",
            "label": "Cursief"
          }
        ],
        "default": "normal"
      },
      {
        "type": "select",
        "id": "price_weight",
        "label": "Prijs gewicht",
        "options": [
          {
            "value": "normal",
            "label": "Normaal"
          },
          {
            "value": "bold",
            "label": "Vet"
          },
          {
            "value": "500",
            "label": "Medium"
          }
        ],
        "default": "bold"
      },
      {
        "type": "header",
        "content": "Geavanceerd"
      },
      {
        "type": "text",
        "id": "custom_class",
        "label": "Custom CSS class"
      },
      {
        "type": "liquid",
        "id": "custom_css",
        "label": "Custom CSS",
        "info": "Gebruik de .zakelijk-btw-container class als selector"
      }
    ]
},

Het block gebruiken

Online Store > Themes > Customize, open een productpagina, Add block onder Product information en kies Prijs Excl. BTW. Direct onder de prijs is de logische plek. Rechts staan alle instellingen.

Drie configuraties

Standaard zakelijk: percentage op 21, "Toon ook prijs incl. BTW" aan, weergave "Onder excl. BTW". Beide prijzen netjes onder elkaar.

Subtiel: lettergrootte 12px, labelkleur lichtgrijs (#999999), prijskleur donkergrijs (#666666). De informatie staat er, maar trekt de aandacht niet weg van de hoofdprijs.

Met badge: zet de badge "Zakelijk" aan in een kleur uit je huisstijl en geef de container wat lucht via het Custom CSS-veld:

.zakelijk-btw-container {
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  padding: 12px;
  background: #f8f9fa;
  border-radius: 8px;
}

Verschillende btw-tarieven

Verkoop je producten met 21% en met 9% (voeding, boeken)? Maak per tarief een product template aan en stel in elk template het juiste percentage in op het block. Wijs de template toe per product.

Als het niet werkt

Meestal een typfout in de JSON van het schema: één ontbrekende komma en Shopify weigert het hele bestand. Werkt de variantprijs niet mee? Dan slaat je thema de variantdata ergens anders op dan de snippet zoekt. Kijk in de inspector welk element bij een variantwissel verandert en pas de selectors in het script daarop aan.

Bij een thema-update blijft de snippet staan, maar de block-code in main-product.liquid en het schema moet je opnieuw toevoegen. Bewaar die twee stukken ergens buiten het thema.

Veelgestelde vragen.

Kan de klant afrekenen exclusief btw?
Niet met dit block. Het toont alleen een berekende prijs. Btw-vrij afrekenen (bijvoorbeeld bij een geldig btw-nummer binnen de EU) regel je via Shopify Markets en belastinginstellingen, of via native B2B met Companies.
Werkt dit met Shopify Markets en meerdere valuta's?
De berekening deelt de getoonde prijs door de btw-factor, dus in elke valuta klopt de verhouding. Let wel op het btw-percentage: dat verschilt per land en staat hier vast per template.
Wat als je thema geen variant-selects-element heeft?
Dan luistert het script naar het variant:change-event en observeert het de prijs-elementen als fallback. Werkt dat ook niet, dan moet je de selector aanpassen naar het element dat jouw thema gebruikt voor de variant-id.
Diek ThunnissenDiek ThunnissenFounder & lead developer. Bouwt, verbetert en migreert de Shopify-laag voor DTC- en B2B-merken. LinkedInPlan een gesprek →

Technische Shopify-inzichten uit de praktijk.

Geen sales, wel techniek.