Taxly: VAT Switcher App
Frontend Integration Guide for Developers
The Tax Switcher app allows Shopify store owners to display prices with or without VAT (Value Added Tax) based on customer type. The app automatically updates all prices across the storefront (product pages, collections, cart, checkout) when customers select their type.
Store owners configure VAT rates through the app's admin interface by selecting a country and assigning a VAT rate percentage. When a customer visits the store, the app:
This document provides technical documentation for external developers working with the Tax Switcher app's frontend components. It focuses on the publicly available interfaces, events, and how the app modifies price fields on the storefront.
customer-type-popup.jsThe main frontend script (extensions/customer-type-popup/assets/customer-type-popup.js) is loaded on every page if one or both of the app blocks (Customer Type Popup or VAT Switcher) are enabled in the theme. The script handles both components depending on which blocks are enabled. It provides:
The script is loaded via Liquid blocks in the theme:
<script src="{{ 'customer-type-popup.js' | asset_url }}" defer></script>
The script uses an IIFE (Immediately Invoked Function Expression) pattern and initializes automatically when the DOM is ready.
The script automatically detects price elements using a comprehensive list of CSS selectors. The complete list includes:
.price.money[data-money].price-item.price__regular.price__sale.f-price-item.f-price-item--regular.f-price-item--sale[data-product-price][data-price].cart-item__price, .cart__item-price, .cart-item-price.cart-price, .cart-item__total, .cart__item-total.cart-item-total, .cart-total.cart-item__original-price, .cart-item__sale-price, .cart-item__compare-priceThese selectors cover the most common Shopify theme patterns and ensure compatibility across different theme structures.
When a price element is first encountered:
data-ctp-original attributedata-ctp-processed="1"data-ctp-session-processed="1"// Example of how elements are marked
el.setAttribute('data-ctp-processed', '1');
el.setAttribute('data-ctp-original', el.textContent || '');
el.setAttribute('data-ctp-session-processed', '1');
The script calculates new prices based on:
data-ctp-original attributeFor Individual Customers:
newPrice = originalPrice × (1 + VATrate/100)For Business Customers:
The adjustPriceElement() function modifies price elements:
// Simplified example of price adjustment
function adjustPriceElement(el, mode, rate, cfg) {
const originalText = el.getAttribute('data-ctp-original') || '';
const parsed = parseFirstNumber(originalText);
if (mode === 'Individual') {
const increased = parsed.value * (1 + (rate / 100));
const formattedNum = formatNumberLike(parsed.raw, increased, ...);
el.textContent = originalText.replace(parsed.raw, formattedNum);
ensureSuffix(el, individualSuffix, cfg);
} else {
// Business: keep original price, add suffix
el.textContent = originalText;
ensureSuffix(el, businessSuffix, cfg);
}
}
The script adds VAT information suffixes to prices:
Suffixes are added as <span> elements with data-ctp-suffix="1" attribute:
<!-- Example result -->
<span class="price">
€120.00
<span data-ctp-suffix="1">VAT incl.</span>
</span>
The script uses MutationObserver to watch for new price elements added to the DOM:
const mo = new MutationObserver((mutations) => {
mutations.forEach(m => {
m.addedNodes.forEach(n => {
if (n instanceof HTMLElement) {
getPriceNodes(n).forEach(el =>
updatePriceWithAnimation(el, mode, rate, cfg)
);
}
});
});
});
mo.observe(document.documentElement, {childList: true, subtree: true});
Cart pages receive special treatment:
customerTypeChanged EventThe script dispatches a custom event when the customer type selection changes:
window.dispatchEvent(new CustomEvent('customerTypeChanged', {
detail: {
selection: 'Individual' | 'Business'
}
}));
window.addEventListener('customerTypeChanged', function(event) {
const selection = event.detail.selection; // 'Individual' or 'Business'
// Your custom logic here
});
The script uses several data attributes to track and identify elements:
| Attribute | Purpose | Example |
|---|---|---|
data-ctp-processed |
Marks price elements that have been processed | data-ctp-processed="1" |
data-ctp-original |
Stores the original price text before modification | data-ctp-original="€100.00" |
data-ctp-session-processed |
Prevents duplicate processing in the same session | data-ctp-session-processed="1" |
data-ctp-suffix |
Identifies VAT suffix elements | data-ctp-suffix="1" |
data-ctp-product-identifier |
Stores product ID for cart items | data-ctp-product-identifier="123456" |
The script reads configuration from anchor elements in the DOM:
Customer Type Popup Anchor:
<div id="customer-type-popup-anchor"
data-preview-mode="false"
data-show-once-per-session="false"
data-translations='[...]'
data-vat-rates='[...]'
data-price-display-translations='[...]'
data-price-display-design='{...}'
data-locale="en"
data-country-code="US"
data-country-name="United States"
data-popup-background-color="#ffffff"
<!-- ... more theme customization attributes ... -->
></div>
VAT Switcher Block Anchor:
<div id="vat-switcher-block-anchor"
data-preview-mode="false"
data-vat-rates='[...]'
data-price-display-translations='[...]'
data-vat-switcher-translations='[...]'
data-price-display-design='{...}'
data-locale="en"
data-country-code="US"
data-button-size="medium"
data-container-background="#ffffff"
<!-- ... more styling attributes ... -->
></div>
Both blocks expose settings through the Shopify theme editor:
The app uses Shopify metafields for configuration:
shop.metafields.popup_translations.translations: Popup text translationsshop.metafields.vat_rates.rates: VAT rate configurationshop.metafields.price_display_translations.translations: Price suffix translationsshop.metafields.vat_switcher_texts.texts: VAT switcher button text translationsshop.metafields.price_display_design.settings: Price suffix design settingsIf you need to detect when prices are updated, you can:
1. Watch for customerTypeChanged events:
window.addEventListener('customerTypeChanged', function(event) {
// Prices will be updated after this event
setTimeout(() => {
const prices = document.querySelectorAll('[data-ctp-processed]');
// Process updated prices
}, 100);
});
2. Monitor processed elements:
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1 && node.hasAttribute('data-ctp-processed')) {
// A price element was just processed
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
If your theme uses custom price selectors not covered by the default list, you can:
<span class="custom-price" data-ctp-processed="1" data-ctp-original="€100.00">
€100.00
</span>
The app sets a cart attribute when customer type is selected:
// Cart attribute is set via:
await fetch('/cart/update.js', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
attributes: {
customer_type: 'Individual' | 'Business'
}
})
});
fetch('/cart.js')
.then(res => res.json())
.then(cart => {
const customerType = cart.attributes.customer_type;
// 'Individual' or 'Business'
});
The script listens to various cart events for real-time updates:
cart:updatedcart:refreshcart:changecart:item:addedcart:item:removedcart:item:updatedcart:drawer:opencart:drawer:closecart:drawer:updatedmini-cart:openmini-cart:closemini-cart:updatedYou can trigger these events to force price updates:
document.dispatchEvent(new CustomEvent('cart:updated'));
The app stores customer type selection:
customer_type_selectionshow_once_per_session setting
true: Uses sessionStorage (cleared when browser closes)false: Uses localStorage (persists across sessions)const selection = localStorage.getItem('customer_type_selection') ||
sessionStorage.getItem('customer_type_selection');
// Returns: 'Individual', 'Business', or null
.ctp-overlay: Popup overlay container.ctp-modal: Popup modal container.ctp-header: Popup header section.ctp-title: Popup title.ctp-subtitle: Popup subtitle.ctp-body: Popup body section.ctp-buttons: Button container.ctp-button: Individual button.ctp-button.primary: Business button (primary style).ctp-button-helper: Helper text below buttons.vat-switcher-container: Main switcher container.vat-switcher-buttons: Button group container.vat-switcher-button: Individual button.vat-switcher-button.active: Active button state.vat-price-transitioning: Applied during price updates.vat-price-shimmer: Shimmer animation during calculation.vat-gray-overlay: Gray overlay for cart totals.vat-price-flash: Flash animation on price changeThe script sets CSS custom properties for theming:
--ctp-popup-bg: Popup background color--ctp-overlay-bg: Overlay background color--ctp-title-size: Title font size--ctp-title-color: Title text color--ctp-subtitle-size: Subtitle font size--ctp-subtitle-color: Subtitle text color--ctp-button-size: Button text size--ctp-button-radius: Button corner radius--ctp-individual-button-color: Individual button text color--ctp-individual-button-bg: Individual button background--vsb-container-background: VAT switcher container background--vsb-button-color: VAT switcher button text color--vsb-active-background: Active button background--vsb-active-color: Active button text colorThe script implements several caching mechanisms:
requestAnimationFrame for smooth transitionsThe script is compatible with:
const prices = document.querySelectorAll('[data-ctp-processed]');
console.log('Processed prices:', prices.length);
const selection = localStorage.getItem('customer_type_selection') ||
sessionStorage.getItem('customer_type_selection');
console.log('Customer type:', selection);
customer-type-popup.jsThe script attempts to hide prices initially to prevent flicker. If flickering occurs:
defer attributedata-ctp-processed attributesCart totals require special handling. The script:
If totals aren't updating:
| Event | Description | Detail |
|---|---|---|
customerTypeChanged |
Fired when customer type selection changes | { selection: 'Individual' | 'Business' } |
| Attribute | Type | Description |
|---|---|---|
data-ctp-processed |
string | Marks processed price elements |
data-ctp-original |
string | Original price text |
data-ctp-session-processed |
string | Session processing marker |
data-ctp-suffix |
string | VAT suffix marker |
data-ctp-product-identifier |
string | Product identifier for cart items |
| Key | Storage Type | Description |
|---|---|---|
customer_type_selection |
localStorage/sessionStorage | Current customer type selection |
ctp_cart_v2p |
sessionStorage | Cart variant-to-product mapping |
ctp_cart_h2p |
sessionStorage | Cart handle-to-product mapping |
data-ctp-processed before modifying price elementscustomerTypeChanged event for custom integrationscustomer_type_selection without understanding the app's logicFor issues or questions:
Email: contact@zonvi.io