Benefits of Using Refillable Shampoo Bottles | Hairstory (2025)

Benefits of Using Refillable Shampoo Bottles| Hairstory (1)

Benefits of Using Refillable Shampoo Bottles

Published on October 14, 2024 — 8 min read

Share this article

Home / The Archive / Benefits of Using Refillable Shampoo Bottles

We go through approximately 16 bottles of shampoo & conditioner every year.

These plastic bottles are used once and then discarded, often thrown in the trash instead of the recycling can. Why? Well, many of the plastic bottles that hold shampoo and conditioner are only partially or totally unrecyclable. But when they are recyclable, we often throw them away in the most convenient place – the bathroom garbage can.

In fact, we only recycle 9% of our plastic! That’s right, nine percent. But we do put more than that in our recycling bins. What happens to those plastics? Shockingly, 70% of the plastic that’s collected for recycling is never actually recycled. So, out of the 91% of plastic that’s thrown away, about 12% is incinerated, and 79% is tossed into landfills or the natural environment.

Clearly, it’s time for a change. Instead of trying to figure out how we can make sure more plastic is recycled, we can begin to reduce or eliminate the need for it. There are easy ways to do just that, such as switching to reusable, refillable shampoo bottles.

WHY ARE PLASTIC SHAMPOO BOTTLES SO BAD?

After a plastic product has been used and tossed into a landfill, it’ll continue to do damage.

The plastic that’s sitting in our landfills can take up to 1,000 years to decompose, and in the process, it’s leaking toxic chemicals into the atmosphere, soil, waterways, and ocean.

In other words, our plastic shampoo and conditioner bottles are doing tremendous harm to us, other living beings on land and in the water, and to our environment.

In addition, the majority of the plastic that’s ever been made is still very slowly decaying. It’s estimated that about 6.3 billion metric tons of plastic have been thrown away since 1950. Of that amount, about 4.9 billion metric tons are still in landfills or throughout our environment. It’s estimated that if the trend continues, by 2050 there will be about 12 billion metric tons of plastic waste in landfills or in the natural environment, and there will be more plastic, by weight, in our oceans than fish. Additionally, most of this discarded waste has caused $2.2 trillion a year worth of environmental and social damage.

But, it’s not only the after-life of single-use plastic that’s concerning to environmentalists, it’s also the production. About 300 million tons of plastic are produced each year. The production alone causes damage to ourselves, wildlife, marine life, and the environment. Plastic is made from oil or natural gas, also called fossil fuels. Once it’s removed from the earth, it must be refined. Both the removal and the refining can release toxic chemicals, polluting the air, water, soil, etc.

Now you know the effects of plastic on the environment but have you ever wondered how shampoo impacts the environment? Unfortunately, some ingredients in shampoo can be unsafe, not only for you but also for the earth.

WHY SHOULD YOU REFILL SHAMPOO BOTTLES?

Made out of aluminum, reusable shampoo bottles do much less harm to ourselves, other living beings, and the environment. There are many benefits of using refillable shampoo bottles and we’ll break down a few of them for you here.

Admittedly, producing aluminum isn’t that much more eco-friendly than producing plastic. Aluminum is made by extracting refining mined bauxite ore – an expensive and energy-consuming practice. But, aluminum bottles can act as a dispenser that can be filled and refilled over and over again without losing their shape.

It’s not only that aluminum bottles can be used multiple times; aluminum’s after-life is exponentially better than a plastic bottle. When an aluminum product is finished, it can be recycled repeatedly without losing any volume or quality during the recycling process. So if 100% of the aluminum that was recycled is still there, then that shampoo or conditioner bottle can once again be turned into a reusable bottle without needing any new material. Aluminum is one of the world’s most recycled materials, and, even better, recycling it takes 90% less energy than it would to make it from scratch.

As an added bonus, it’s usually cheaper. Companies can use less plastic to make containers that hold the reusable shampoo or conditioner because these containers won’t need to be as hard and durable as a container that must withstand daily water pressure and incorporate an easy-to-pour design. The savings of using less plastic are then passed on to you, the consumer.

THE PROBLEM WITH REFILLABLE SHAMPOO BOTTLES

Sounds great, right? So why are there problems?

Currently, the two major problems with refillable bottles are price and where/how to refill them. The majority of refills are available at refill stations, which aren’t always convenient for people. In fact, refill stations aren’t even available in most towns – they’re typically found in big cities. And, the brands that offer a refill station are usually selling high-end products found in luxury department stores, something that isn’t affordable to everyone.

Between the lack of convenience and a high price point, using refillable bottles is simply out of reach for many people. Instead, many choose to buy in bulk, reducing the amount of waste as best they can.

HOW CAN YOU REFILL A SHAMPOO BOTTLE?

Currently, one of the best ways to refill shampoo and conditioner bottles, regardless of where you live, is to order refills online and have them delivered to your home. You can order refillable shampoo containers online through a variety of sites. We sell our 20 oz. aluminum canisters for $10 each.

When looking for refillable shampoo and conditioner containers, don’t forget to do the math. Add up the product and shipping costs as well as how frequently you’ll need to order to determine if the price is actually affordable. For example, with our Refill Club, in addition to receiving the refillable shampoo canister for free, you’ll save up to 15% on refill pouches. The club allows you to decide how often you’d like to receive a 20-oz or 32-oz refill pouch, and they’re automatically sent to your home with free shipping.

OTHER WAYS TO SAVE THE ENVIRONMENT WHILE CLEANING YOUR HAIR

If you’re looking to refill shampoo bottles because of the environmental impact, odds are you’ll find the brands that sell these products are eco-conscious in other ways. Some may contribute to environmental organizations like 1% for the Planet, where, like us, they donate a meaningful share of their sales to water-focused sustainability non-profits.

Other companies may choose to reduce their global footprint by sourcing the most environmentally-friendly packaging available for their refill containers. Currently, our refills come in pouches that use 69% less plastic, energy, and water to produce than bottles. Members of our Refill Club use 91% less plastic and 82% less CO2 to wash and condition their hair.

As an additional benefit to you and the environment, you’ll also find companies who exclude unpronounceable, harmful chemicals. If you’re washing and rinsing your hair with parabens, fragrances, synthetic colors, sulfates, and other dangerous chemicals, they’re sent down your drain and out into the water supply. They’ll end up in a variety of environments, including wastewater, surface water, sediment, groundwater, and drinking water. But many companies that sell shampoo and conditioner in reusable bottles use all-natural ingredients in their products. Look for those that are made with essential oils and natural minerals, as well as ones that are biodegradable, like our New Wash.

Look for other ways to ditch the plastic and use refillable aluminium bottles for things like a soap dispenser, cleaning product, or body wash.

Whatever additional benefits you may find, we applaud all individuals and companies who share our passion for the environment and take meaningful steps to improve their carbon footprint.

shop the collection

View More Information Suited For Flat Hair, Oily Hair, Gray Hair, Dry Hair, Damaged Hair, Color-Treated Hair What It Is PRE-WASH AND NEW WASH ORIGINAL Benefits Clarify , cleanse and condition FOR ALL HAIR TYPES Learn More

New Wash Method for All Hair Types

PRE-WASH AND NEW WASH ORIGINAL

`; } await waitForRechargeWidgetToBeMounted(); await waitForRechargeSelect(); await relocateSubscriptionPriceAndCleanup(); if (shouldRenderBulletPoints) { assignProductBullets(); createBulletPoints(); } handleRechargeEventListeners('add'); configureRechargeForProductSelection(productId); const spinnerElement = document.getElementById('loading-spinner'); if (spinnerElement) { spinnerElement.remove(); } if (rechargeWidgetContainer) { rechargeWidgetContainer.style.opacity = '100'; rechargeWidgetContainer.classList.add('opacity-100', 'transition-opacity', 'duration-300'); } } /** * @description Function that is used to initially mount the recharge widget * @returns { boolean } based on if the product is found in recharge or not. If false, then the widget will not mount. * In addition if it cannot find the product, it short circuits and returns early. */ function shouldRechargeMount() { const checkReChargeWidget = setInterval(() => { if (window.ReChargeWidget && window.ReChargeWidget.api) { clearInterval(checkReChargeWidget); window.ReChargeWidget.api.fetchProduct(productId) .then(rechargeProduct => { if (!rechargeProduct.in_recharge) { return false; } const config = { productId: productId, injectionParent: `#subscription-selector-${productId}` }; window.ReChargeWidget.createWidget(config); return true; }) } }, 100); } async function waitForRechargeWidgetToBeMounted() { return new Promise(resolve => { const checkForRechargeContainer = setInterval(() => { if (document.querySelector(`#subscription-selector-${productId} > .rc-container-wrapper`)) { rechargeWidgetContainer = document.querySelector(`#subscription-selector-${productId} > .rc-container-wrapper`) clearInterval(checkForRechargeContainer) resolve() } }, 100) }) } /** * @description Function that is used to determine the initial price of the product, and then properly assign * the otpPriceText and subscriptionPriceText * @param {HTMLElement} originalPriceElement HTML Element that is used to determine the base price, and then calculate the subscription discount */ function derivePriceOptions(originalPriceElements) { let priceElementTextContent = originalPriceElements[0].innerText; let priceRegex = /([£$€¥])(\d+(?:\.\d{1,2})?)/; let match = priceElementTextContent.match(priceRegex); if (match) { let currencySymbol = match[1]; let priceValue = match[2]; otpPriceText = currencySymbol + priceValue; let subscriptionPrice if (productsWithTenPercentOff.includes(productId)) { subscriptionPrice = Number(priceValue) * 0.9; } else { subscriptionPrice = Number(priceValue) * 0.95; } subscriptionPriceText = `${currencySymbol}${subscriptionPrice.toFixed(2)}`; } } /** * @description Function to add or remove Recharge event listeners * @param {string} action - 'add' to add listeners, 'remove' to remove listeners */ function handleRechargeEventListeners(action) { function manageListeners() { const oneTimePurchaseInput = document.querySelector(`#subscription-selector-${productId} [data-radio-onetime]`); const subscriptionPurchaseInput = document.querySelector(`#subscription-selector-${productId} [data-radio-subsave]`); if (oneTimePurchaseInput && subscriptionPurchaseInput) { if (originalPriceElements.length > 0 && action === 'add') { derivePriceOptions(originalPriceElements) } if (currentSubscriptionSelector) { // Because recharge is responsbile for adding the .rc-option--active class when a user clicks a radio, we must set it manually for the accent-color black // to be present if we switch the radio manually, after the recharge widget remounts if (currentSubscriptionSelector === 'subscription') { subscriptionPurchaseInput.classList.add('rc-option--active') subscriptionPurchaseInput.checked = true // In products.js, the onSubmitHandler checks for a select element named 'selling_plan'. // When remounting and setting the checked value programmatically, it defaults to 'One time purchase', adding name=''. // If switching products and subscribe and save is selected, the select with name='' will still mount, causing the cart item to show a OTP rechargeSelectElement.setAttribute('name', 'selling_plan') let rechargeSellingPlans = deliveryOptionsContainer.querySelector('.rc-selling-plans') // By default our config for recharge is set to OTP. If we remount with a subscription option selected via our event handlers // The select with have a display: none; hardcoded onto the style, so we must remove it in order for it to be visible. if (rechargeSellingPlans) { if (rechargeSellingPlans.style.display === 'none') { rechargeSellingPlans.style.removeProperty('display'); } } } else { deliveryOptionsContainer.classList.add('hidden') oneTimePurchaseInput.classList.add('rc-option--active') oneTimePurchaseInput.checked = true // In products.js, the onSubmitHandler checks for a select element named 'selling_plan'. // When remounting and setting the checked value programmatically, it defaults to 'subscribe and save', adding name='selling_plan'. // If switching products and OTP is selected, the input with name='selling_plan' will still mount, causing the cart item to show as a subscription, even if it's OTP. rechargeSelectElement.setAttribute('name', '') } } // Check to see which input is checked on dom mount, then properly set the price based on the product and variant selected. if (subscriptionPurchaseInput.checked === true) { // reassign the perks container to the new product id on being switched, or set the intitial element if it doesn't exist on initial mount if ((!subscriptionPerksContainer && action === 'add') || (action === 'add' && subscriptionPerksContainer.id !== document.querySelector(`[id="${productId}-subscription-perks"]`).id)) { subscriptionPerksContainer = document.querySelector(`[id="${productId}-subscription-perks"]`); } if (originalPriceElements.length > 0) { originalPriceElements.forEach(element => { element.textContent = subscriptionPriceText; }); } if (shouldRenderBulletPoints) { subscriptionPerksContainer.classList.remove('hidden'); } } else { if (originalPriceElements.length > 0) { originalPriceElements.forEach(element => { element.textContent = otpPriceText; }); } } if (action === 'add') { oneTimePurchaseInput.addEventListener('click', onOneTimePurchaseClick); subscriptionPurchaseInput.addEventListener('click', onSubscriptionPurchaseClick); // At the time of this writing. Recharge after mounting sets the selling plan to 0. so we need to set the name manually // otherwise on PLP's radio options will only allow one option to be set oneTimePurchaseInput.name = `${productId}_selling_option` subscriptionPurchaseInput.name = `${productId}_selling_option` } else if (action === 'remove') { oneTimePurchaseInput.removeEventListener('click', onOneTimePurchaseClick); subscriptionPurchaseInput.removeEventListener('click', onSubscriptionPurchaseClick); } handleSellingPlanSelect(action) return true; } return false; } if (manageListeners()) return; const waitForElements = setInterval(() => { if (manageListeners()) { clearInterval(waitForElements); } }, 100); } /** * @description binds the proper event listerns to the proper selling plan select, in addition to pre-selecting the existing plan if * the previously selected option is present in the newly mounted select. */ function handleSellingPlanSelect(action) { const checkForSellingPlanSelect = setInterval(() => { const sellingPlanSelect = document.querySelector(`#selling_plan_${productId}`); if (sellingPlanSelect) { clearInterval(checkForSellingPlanSelect); if (action === 'add') { sellingPlanSelect.addEventListener('change', onSellingPlanChange); } else if (action === 'remove') { sellingPlanSelect.removeEventListener('change', onSellingPlanChange); } if (lastSelectedSellingPlan) { // Convert array like object into a proper array const matchingSellingPlanOption = [...sellingPlanSelect.options].find(option => option.textContent.trim() === lastSelectedSellingPlan ); if (matchingSellingPlanOption) { sellingPlanSelect.value = matchingSellingPlanOption.value; } } } }, 100); } /** * @description Function that is called when the OTP input is clicked */ function onOneTimePurchaseClick() { onRechargeOptionClick('otp'); } /** * @description Function that is called when the subscription input is clicked */ function onSubscriptionPurchaseClick() { onRechargeOptionClick('subscription'); } /** * @description Function that is called whenever the selling plan selected option has changed */ function onSellingPlanChange(event) { lastSelectedSellingPlan = event.target.options[event.target.selectedIndex].textContent.trim(); } /** * @description Function that is called when a recharge option input is clicked. Primarly used to add bullet points, and make sure the select has an option mounted * @param {string} optionType The type of option that was clicked, either OTP, or subscription */ function onRechargeOptionClick(optionType) { const selectedPriceText = optionType === 'subscription' ? subscriptionPriceText : otpPriceText; // Save the current selected selector, so when we remount, we can default to that option being checked currentSubscriptionSelector = optionType if (originalPriceElements[0].getAttribute('data-price-id') !== `price-${productId}`) { let originalPriceElementQuerySelector = document.querySelectorAll(`[data-price-id="price-${productId}"]`); // Assign all matching elements to the array originalPriceElements = Array.from(originalPriceElementQuerySelector); } if (originalPriceElements.length > 0) { originalPriceElements.forEach(element => { element.textContent = selectedPriceText; }); } if (optionType === 'otp') { deliveryOptionsContainer.classList.add('hidden') } // If the widget has been remounted, and OTP was selected, we need to remove hidden from the container so a user can see the selling plans if (deliveryOptionsContainer && optionType === 'subscription' && deliveryOptionsContainer.classList.contains('hidden')) { deliveryOptionsContainer.classList.remove('hidden') } if (subscriptionPerksContainer) { if (optionType === 'subscription' && shouldRenderBulletPoints) { subscriptionPerksContainer.classList.remove('hidden'); } else { subscriptionPerksContainer.classList.add('hidden'); } } if (optionType === 'subscription') { if (rechargeSelectElement) { // Make sure the select element as the first child element selected after mount // otherwise the select shows as empty rechargeSelectElement.children[0].selected = 'selected'; // on remount, if OTP is selected, we remove the name from the selling plan, this messes up recharge's internal logic, and then the select name always remains empty // to stop this from happening, we just manually set it everytime subscription is clicked rechargeSelectElement.setAttribute('name', 'selling_plan') } } if (optionType === 'otp') { if (rechargeSelectElement) { // When we select the OTP, but subscription was selected previously, it will still have the selling plan, name, we need to remove this or it falsely gets set as a subscription product rechargeSelectElement.setAttribute('name', '') } } } /** * @description Waits for the data price subsave attribute to be loaded, and then moves it down to the add cart button, in additio to cleaning up any remnants from recharge */ async function relocateSubscriptionPriceAndCleanup() { return new Promise((resolve) => { const waitForPriceElements = setInterval(() => { const subscriptionPriceElement = document.querySelector(`#subscription-selector-${productId} .rc_widget__price--subsave[data-price-subsave]`); const otpPriceElement = document.querySelector(`#subscription-selector-${productId} .rc_widget__price--onetime[data-price-onetime]`); const discountSpan = document.querySelector(`#subscription-selector-${productId} .rc_widget__option__discount[data-label-discount]`) let originalPriceElementQuerySelector = document.querySelectorAll(`[data-price-id="price-${productId}"]`); // Assign all matching elements to the array originalPriceElements = Array.from(originalPriceElementQuerySelector); if (subscriptionPriceElement && otpPriceElement && originalPriceElements.length > 0 && discountSpan) { // Left over elements from recharge that need to be hidden // I.E Subscription price vs OTP price, and percentage discounted const unusedRechargeElementSelectors = [ `#subscription-selector-${productId} .rc_widget__price--subsave[data-price-subsave]`, `#subscription-selector-${productId} .rc_widget__price--onetime[data-price-onetime]` ]; unusedRechargeElementSelectors.forEach(selector => { const element = document.querySelector(selector); if (element) { element.classList.add('hidden'); } }); clearInterval(waitForPriceElements); resolve(); } }, 100); }) } /** * @description Waits for the recharge select element to be loaded into the dom, and then moves it down to below the variant selector */ function waitForRechargeSelect() { return new Promise((resolve) => { const checkForRechargeSelect = setInterval(() => { if (document.querySelector(`#selling_plan_${productId}`)) { rechargeSelectElement = document.querySelector(`#selling_plan_${productId}`); const checkForNewDeliveryOptionsContainer = setInterval(() => { if (document.querySelector(`#delivery-options-${productId}`)) { deliveryOptionsContainer = document.querySelector(`#delivery-options-${productId}`); deliveryOptionsContainer.append(rechargeSelectElement.parentElement); clearInterval(checkForNewDeliveryOptionsContainer); // We only want to add the additional styles on the PDP if its the refill collection if (collection?.id === 449976238310 || pageTemplate === 'page.refill-club' || isStyledForCarousel) { conditionallyStyleRechargeOnPLP(); } // Remove the strong tag between the span and the subscribe and save text const subSaveSpan = document.querySelector(`#subscription-selector-${productId} .rc-option__text[data-label-text-subsave]`); if (subSaveSpan.querySelector('strong')) { const strongElement = subSaveSpan.querySelector('strong'); const strongText = strongElement.textContent.trim(); const textNode = document.createTextNode(strongText); subSaveSpan.replaceChild(textNode, strongElement); } clearInterval(checkForRechargeSelect); resolve(); } }, 100); } }, 100); }); } /** * @description Checks to see if the recharge widget is on the PDP, if so, it conditionally applies CSS styles, since there will be some products on the PDP that don't have * a

element, but all selects on the PDP will need to have a transparent background. */ function conditionallyStyleRechargeOnPLP() { // This is the id associated with the refill collection. We need to conditionally rechargeSelectElement.classList.add('!bg-transparent') const parentElement = deliveryOptionsContainer.parentElement; const variantSelectCard = parentElement.querySelector(':scope > variant-select-card'); if (!variantSelectCard) { deliveryOptionsContainer.classList.add('border-t', 'border-t-brand-secondary-200') // Because of the lack of a variant card, we need to remove the bottom border, so the line between the delivery options, and the add to bag doesn't have an extra thick border deliveryOptionsContainer.firstChild.classList.add('border-b-0') } } /** * @description Dynamically creates a list of bullet points whenever the shouldRenderBulletPoints flag is true. Also creates a title for the bullet points. */ function createBulletPoints() { if (shouldRenderBulletPoints && subscriptionBullets.length > 0) { const subscriptionSelector = document.querySelector(`#subscription-selector-${productId}`); subscriptionPerksContainer = document.createElement('div'); subscriptionPerksContainer.id = `${productId}-subscription-perks`; subscriptionPerksContainer.className = 'hidden space-y-2 pb-4 pt-3'; const subscriptionPerksTitle = document.createElement('h4'); subscriptionPerksTitle.className = 'space-y-2 uppercase'; subscriptionPerksTitle.innerHTML = 'Subscription Benefits:'; subscriptionPerksContainer.appendChild(subscriptionPerksTitle); subscriptionBullets.forEach((bullet, index) => { const bulletPoint = document.createElement('p'); bulletPoint.className = 'flex items-center w-full text-brand-secondary-200'; bulletPoint.innerHTML = ` ${renderIconCircleCheckmark()} ${bullet} `; if (shouldApplyBottomMargin && index === subscriptionBullets.length - 1) { bulletPoint.style.marginBottom = '4px'; } subscriptionPerksContainer.appendChild(bulletPoint); }); subscriptionSelector.appendChild(subscriptionPerksContainer); } } function renderIconCircleCheckmark() { return ``; } function assignProductBullets() { if (window.subscriptonBulletsDict && window.subscriptonBulletsDict[productId]) { let newSubscriptionBulletList = [...window.subscriptonBulletsDict[productId]] subscriptionBullets = newSubscriptionBulletList } else { subscriptionBullets = defaultSubscriptionBullets; } } /** * @param {string} newProductId the new product id that will be used to update all exising HTML elements, and rebind the function on the window * @description Whenever the recharge widget is remounted, we need to update the initial bullet with the new product description */ async function onNewProductSelect(newProductId) { if (productId !== newProductId) { let rechargeContainer = document.querySelector(`#subscription-selector-${productId}`); let cardAtcContainer = document.querySelector(`#card-atc-${productId}`) // Show loading spinner rechargeContainer.innerHTML = ` `; // cleanup all event listeners and old elements from the old widget handleRechargeEventListeners('remove'); if (window.ReChargeWidget.getWidgetsByProductId(productId)[0]) { window.ReChargeWidget.getWidgetsByProductId(productId)[0].widgetInstance.unmount(); } if (subscriptionPerksContainer) { subscriptionPerksContainer.remove(); } // After cleaning up the previous widget, reassign the id to properly mount the new widget. rechargeContainer.id = `subscription-selector-${newProductId}`; productId = Number(newProductId); const config = { productId: newProductId, injectionParent: `#subscription-selector-${newProductId}` }; // Re-establish and mount new widget window.ReChargeWidget.createWidget(config); // Wait for all elements to mount await waitForRechargeWidgetToBeMounted(); await waitForRechargeSelect(); await relocateSubscriptionPriceAndCleanup(); if (shouldRenderBulletPoints) { assignProductBullets() createBulletPoints(); } handleRechargeEventListeners('add'); configureRechargeForProductSelection(newProductId); // Hide spinner const spinnerElement = document.getElementById('loading-spinner'); if (spinnerElement) { spinnerElement.remove(); } if (rechargeWidgetContainer) { rechargeWidgetContainer.style.opacity = '100'; rechargeWidgetContainer.classList.add('opacity-100', 'transition-opacity', 'duration-300'); } } } /** * @param {string} productId productId responsible for the unique key to be set on the window, so all instances of the widget can be accessed. * @description Function responsible for binding the onNewProduct select to the window. */ function configureRechargeForProductSelection(productId) { window.HairstoryRechargeWidget = window.HairstoryRechargeWidget || {}; window.HairstoryRechargeWidget[productId] = { onNewProductSelect: onNewProductSelect }; } // Initial mount of recharge widget initializeRechargeWidget().catch(error => { console.error("Error initializing ReCharge widget:", error); }); }); View More Information Suited For Flat Hair, Oily Hair, Gray Hair, Dry Hair, Damaged Hair, Color-Treated Hair What It Is 8OZ NEW WASH ORIGINAL, BOND BOOST AND BOND SERUM Benefits NEW Learn More

Damage Repair Method

8OZ NEW WASH ORIGINAL, BOND BOOST AND BOND SERUM

`; } await waitForRechargeWidgetToBeMounted(); await waitForRechargeSelect(); await relocateSubscriptionPriceAndCleanup(); if (shouldRenderBulletPoints) { assignProductBullets(); createBulletPoints(); } handleRechargeEventListeners('add'); configureRechargeForProductSelection(productId); const spinnerElement = document.getElementById('loading-spinner'); if (spinnerElement) { spinnerElement.remove(); } if (rechargeWidgetContainer) { rechargeWidgetContainer.style.opacity = '100'; rechargeWidgetContainer.classList.add('opacity-100', 'transition-opacity', 'duration-300'); } } /** * @description Function that is used to initially mount the recharge widget * @returns { boolean } based on if the product is found in recharge or not. If false, then the widget will not mount. * In addition if it cannot find the product, it short circuits and returns early. */ function shouldRechargeMount() { const checkReChargeWidget = setInterval(() => { if (window.ReChargeWidget && window.ReChargeWidget.api) { clearInterval(checkReChargeWidget); window.ReChargeWidget.api.fetchProduct(productId) .then(rechargeProduct => { if (!rechargeProduct.in_recharge) { return false; } const config = { productId: productId, injectionParent: `#subscription-selector-${productId}` }; window.ReChargeWidget.createWidget(config); return true; }) } }, 100); } async function waitForRechargeWidgetToBeMounted() { return new Promise(resolve => { const checkForRechargeContainer = setInterval(() => { if (document.querySelector(`#subscription-selector-${productId} > .rc-container-wrapper`)) { rechargeWidgetContainer = document.querySelector(`#subscription-selector-${productId} > .rc-container-wrapper`) clearInterval(checkForRechargeContainer) resolve() } }, 100) }) } /** * @description Function that is used to determine the initial price of the product, and then properly assign * the otpPriceText and subscriptionPriceText * @param {HTMLElement} originalPriceElement HTML Element that is used to determine the base price, and then calculate the subscription discount */ function derivePriceOptions(originalPriceElements) { let priceElementTextContent = originalPriceElements[0].innerText; let priceRegex = /([£$€¥])(\d+(?:\.\d{1,2})?)/; let match = priceElementTextContent.match(priceRegex); if (match) { let currencySymbol = match[1]; let priceValue = match[2]; otpPriceText = currencySymbol + priceValue; let subscriptionPrice if (productsWithTenPercentOff.includes(productId)) { subscriptionPrice = Number(priceValue) * 0.9; } else { subscriptionPrice = Number(priceValue) * 0.95; } subscriptionPriceText = `${currencySymbol}${subscriptionPrice.toFixed(2)}`; } } /** * @description Function to add or remove Recharge event listeners * @param {string} action - 'add' to add listeners, 'remove' to remove listeners */ function handleRechargeEventListeners(action) { function manageListeners() { const oneTimePurchaseInput = document.querySelector(`#subscription-selector-${productId} [data-radio-onetime]`); const subscriptionPurchaseInput = document.querySelector(`#subscription-selector-${productId} [data-radio-subsave]`); if (oneTimePurchaseInput && subscriptionPurchaseInput) { if (originalPriceElements.length > 0 && action === 'add') { derivePriceOptions(originalPriceElements) } if (currentSubscriptionSelector) { // Because recharge is responsbile for adding the .rc-option--active class when a user clicks a radio, we must set it manually for the accent-color black // to be present if we switch the radio manually, after the recharge widget remounts if (currentSubscriptionSelector === 'subscription') { subscriptionPurchaseInput.classList.add('rc-option--active') subscriptionPurchaseInput.checked = true // In products.js, the onSubmitHandler checks for a select element named 'selling_plan'. // When remounting and setting the checked value programmatically, it defaults to 'One time purchase', adding name=''. // If switching products and subscribe and save is selected, the select with name='' will still mount, causing the cart item to show a OTP rechargeSelectElement.setAttribute('name', 'selling_plan') let rechargeSellingPlans = deliveryOptionsContainer.querySelector('.rc-selling-plans') // By default our config for recharge is set to OTP. If we remount with a subscription option selected via our event handlers // The select with have a display: none; hardcoded onto the style, so we must remove it in order for it to be visible. if (rechargeSellingPlans) { if (rechargeSellingPlans.style.display === 'none') { rechargeSellingPlans.style.removeProperty('display'); } } } else { deliveryOptionsContainer.classList.add('hidden') oneTimePurchaseInput.classList.add('rc-option--active') oneTimePurchaseInput.checked = true // In products.js, the onSubmitHandler checks for a select element named 'selling_plan'. // When remounting and setting the checked value programmatically, it defaults to 'subscribe and save', adding name='selling_plan'. // If switching products and OTP is selected, the input with name='selling_plan' will still mount, causing the cart item to show as a subscription, even if it's OTP. rechargeSelectElement.setAttribute('name', '') } } // Check to see which input is checked on dom mount, then properly set the price based on the product and variant selected. if (subscriptionPurchaseInput.checked === true) { // reassign the perks container to the new product id on being switched, or set the intitial element if it doesn't exist on initial mount if ((!subscriptionPerksContainer && action === 'add') || (action === 'add' && subscriptionPerksContainer.id !== document.querySelector(`[id="${productId}-subscription-perks"]`).id)) { subscriptionPerksContainer = document.querySelector(`[id="${productId}-subscription-perks"]`); } if (originalPriceElements.length > 0) { originalPriceElements.forEach(element => { element.textContent = subscriptionPriceText; }); } if (shouldRenderBulletPoints) { subscriptionPerksContainer.classList.remove('hidden'); } } else { if (originalPriceElements.length > 0) { originalPriceElements.forEach(element => { element.textContent = otpPriceText; }); } } if (action === 'add') { oneTimePurchaseInput.addEventListener('click', onOneTimePurchaseClick); subscriptionPurchaseInput.addEventListener('click', onSubscriptionPurchaseClick); // At the time of this writing. Recharge after mounting sets the selling plan to 0. so we need to set the name manually // otherwise on PLP's radio options will only allow one option to be set oneTimePurchaseInput.name = `${productId}_selling_option` subscriptionPurchaseInput.name = `${productId}_selling_option` } else if (action === 'remove') { oneTimePurchaseInput.removeEventListener('click', onOneTimePurchaseClick); subscriptionPurchaseInput.removeEventListener('click', onSubscriptionPurchaseClick); } handleSellingPlanSelect(action) return true; } return false; } if (manageListeners()) return; const waitForElements = setInterval(() => { if (manageListeners()) { clearInterval(waitForElements); } }, 100); } /** * @description binds the proper event listerns to the proper selling plan select, in addition to pre-selecting the existing plan if * the previously selected option is present in the newly mounted select. */ function handleSellingPlanSelect(action) { const checkForSellingPlanSelect = setInterval(() => { const sellingPlanSelect = document.querySelector(`#selling_plan_${productId}`); if (sellingPlanSelect) { clearInterval(checkForSellingPlanSelect); if (action === 'add') { sellingPlanSelect.addEventListener('change', onSellingPlanChange); } else if (action === 'remove') { sellingPlanSelect.removeEventListener('change', onSellingPlanChange); } if (lastSelectedSellingPlan) { // Convert array like object into a proper array const matchingSellingPlanOption = [...sellingPlanSelect.options].find(option => option.textContent.trim() === lastSelectedSellingPlan ); if (matchingSellingPlanOption) { sellingPlanSelect.value = matchingSellingPlanOption.value; } } } }, 100); } /** * @description Function that is called when the OTP input is clicked */ function onOneTimePurchaseClick() { onRechargeOptionClick('otp'); } /** * @description Function that is called when the subscription input is clicked */ function onSubscriptionPurchaseClick() { onRechargeOptionClick('subscription'); } /** * @description Function that is called whenever the selling plan selected option has changed */ function onSellingPlanChange(event) { lastSelectedSellingPlan = event.target.options[event.target.selectedIndex].textContent.trim(); } /** * @description Function that is called when a recharge option input is clicked. Primarly used to add bullet points, and make sure the select has an option mounted * @param {string} optionType The type of option that was clicked, either OTP, or subscription */ function onRechargeOptionClick(optionType) { const selectedPriceText = optionType === 'subscription' ? subscriptionPriceText : otpPriceText; // Save the current selected selector, so when we remount, we can default to that option being checked currentSubscriptionSelector = optionType if (originalPriceElements[0].getAttribute('data-price-id') !== `price-${productId}`) { let originalPriceElementQuerySelector = document.querySelectorAll(`[data-price-id="price-${productId}"]`); // Assign all matching elements to the array originalPriceElements = Array.from(originalPriceElementQuerySelector); } if (originalPriceElements.length > 0) { originalPriceElements.forEach(element => { element.textContent = selectedPriceText; }); } if (optionType === 'otp') { deliveryOptionsContainer.classList.add('hidden') } // If the widget has been remounted, and OTP was selected, we need to remove hidden from the container so a user can see the selling plans if (deliveryOptionsContainer && optionType === 'subscription' && deliveryOptionsContainer.classList.contains('hidden')) { deliveryOptionsContainer.classList.remove('hidden') } if (subscriptionPerksContainer) { if (optionType === 'subscription' && shouldRenderBulletPoints) { subscriptionPerksContainer.classList.remove('hidden'); } else { subscriptionPerksContainer.classList.add('hidden'); } } if (optionType === 'subscription') { if (rechargeSelectElement) { // Make sure the select element as the first child element selected after mount // otherwise the select shows as empty rechargeSelectElement.children[0].selected = 'selected'; // on remount, if OTP is selected, we remove the name from the selling plan, this messes up recharge's internal logic, and then the select name always remains empty // to stop this from happening, we just manually set it everytime subscription is clicked rechargeSelectElement.setAttribute('name', 'selling_plan') } } if (optionType === 'otp') { if (rechargeSelectElement) { // When we select the OTP, but subscription was selected previously, it will still have the selling plan, name, we need to remove this or it falsely gets set as a subscription product rechargeSelectElement.setAttribute('name', '') } } } /** * @description Waits for the data price subsave attribute to be loaded, and then moves it down to the add cart button, in additio to cleaning up any remnants from recharge */ async function relocateSubscriptionPriceAndCleanup() { return new Promise((resolve) => { const waitForPriceElements = setInterval(() => { const subscriptionPriceElement = document.querySelector(`#subscription-selector-${productId} .rc_widget__price--subsave[data-price-subsave]`); const otpPriceElement = document.querySelector(`#subscription-selector-${productId} .rc_widget__price--onetime[data-price-onetime]`); const discountSpan = document.querySelector(`#subscription-selector-${productId} .rc_widget__option__discount[data-label-discount]`) let originalPriceElementQuerySelector = document.querySelectorAll(`[data-price-id="price-${productId}"]`); // Assign all matching elements to the array originalPriceElements = Array.from(originalPriceElementQuerySelector); if (subscriptionPriceElement && otpPriceElement && originalPriceElements.length > 0 && discountSpan) { // Left over elements from recharge that need to be hidden // I.E Subscription price vs OTP price, and percentage discounted const unusedRechargeElementSelectors = [ `#subscription-selector-${productId} .rc_widget__price--subsave[data-price-subsave]`, `#subscription-selector-${productId} .rc_widget__price--onetime[data-price-onetime]` ]; unusedRechargeElementSelectors.forEach(selector => { const element = document.querySelector(selector); if (element) { element.classList.add('hidden'); } }); clearInterval(waitForPriceElements); resolve(); } }, 100); }) } /** * @description Waits for the recharge select element to be loaded into the dom, and then moves it down to below the variant selector */ function waitForRechargeSelect() { return new Promise((resolve) => { const checkForRechargeSelect = setInterval(() => { if (document.querySelector(`#selling_plan_${productId}`)) { rechargeSelectElement = document.querySelector(`#selling_plan_${productId}`); const checkForNewDeliveryOptionsContainer = setInterval(() => { if (document.querySelector(`#delivery-options-${productId}`)) { deliveryOptionsContainer = document.querySelector(`#delivery-options-${productId}`); deliveryOptionsContainer.append(rechargeSelectElement.parentElement); clearInterval(checkForNewDeliveryOptionsContainer); // We only want to add the additional styles on the PDP if its the refill collection if (collection?.id === 449976238310 || pageTemplate === 'page.refill-club' || isStyledForCarousel) { conditionallyStyleRechargeOnPLP(); } // Remove the strong tag between the span and the subscribe and save text const subSaveSpan = document.querySelector(`#subscription-selector-${productId} .rc-option__text[data-label-text-subsave]`); if (subSaveSpan.querySelector('strong')) { const strongElement = subSaveSpan.querySelector('strong'); const strongText = strongElement.textContent.trim(); const textNode = document.createTextNode(strongText); subSaveSpan.replaceChild(textNode, strongElement); } clearInterval(checkForRechargeSelect); resolve(); } }, 100); } }, 100); }); } /** * @description Checks to see if the recharge widget is on the PDP, if so, it conditionally applies CSS styles, since there will be some products on the PDP that don't have * a element, but all selects on the PDP will need to have a transparent background. */ function conditionallyStyleRechargeOnPLP() { // This is the id associated with the refill collection. We need to conditionally rechargeSelectElement.classList.add('!bg-transparent') const parentElement = deliveryOptionsContainer.parentElement; const variantSelectCard = parentElement.querySelector(':scope > variant-select-card'); if (!variantSelectCard) { deliveryOptionsContainer.classList.add('border-t', 'border-t-brand-secondary-200') // Because of the lack of a variant card, we need to remove the bottom border, so the line between the delivery options, and the add to bag doesn't have an extra thick border deliveryOptionsContainer.firstChild.classList.add('border-b-0') } } /** * @description Dynamically creates a list of bullet points whenever the shouldRenderBulletPoints flag is true. Also creates a title for the bullet points. */ function createBulletPoints() { if (shouldRenderBulletPoints && subscriptionBullets.length > 0) { const subscriptionSelector = document.querySelector(`#subscription-selector-${productId}`); subscriptionPerksContainer = document.createElement('div'); subscriptionPerksContainer.id = `${productId}-subscription-perks`; subscriptionPerksContainer.className = 'hidden space-y-2 pb-4 pt-3'; const subscriptionPerksTitle = document.createElement('h4'); subscriptionPerksTitle.className = 'space-y-2 uppercase'; subscriptionPerksTitle.innerHTML = 'Subscription Benefits:'; subscriptionPerksContainer.appendChild(subscriptionPerksTitle); subscriptionBullets.forEach((bullet, index) => { const bulletPoint = document.createElement('p'); bulletPoint.className = 'flex items-center w-full text-brand-secondary-200'; bulletPoint.innerHTML = ` ${renderIconCircleCheckmark()} ${bullet} `; if (shouldApplyBottomMargin && index === subscriptionBullets.length - 1) { bulletPoint.style.marginBottom = '4px'; } subscriptionPerksContainer.appendChild(bulletPoint); }); subscriptionSelector.appendChild(subscriptionPerksContainer); } } function renderIconCircleCheckmark() { return ``; } function assignProductBullets() { if (window.subscriptonBulletsDict && window.subscriptonBulletsDict[productId]) { let newSubscriptionBulletList = [...window.subscriptonBulletsDict[productId]] subscriptionBullets = newSubscriptionBulletList } else { subscriptionBullets = defaultSubscriptionBullets; } } /** * @param {string} newProductId the new product id that will be used to update all exising HTML elements, and rebind the function on the window * @description Whenever the recharge widget is remounted, we need to update the initial bullet with the new product description */ async function onNewProductSelect(newProductId) { if (productId !== newProductId) { let rechargeContainer = document.querySelector(`#subscription-selector-${productId}`); let cardAtcContainer = document.querySelector(`#card-atc-${productId}`) // Show loading spinner rechargeContainer.innerHTML = ` `; // cleanup all event listeners and old elements from the old widget handleRechargeEventListeners('remove'); if (window.ReChargeWidget.getWidgetsByProductId(productId)[0]) { window.ReChargeWidget.getWidgetsByProductId(productId)[0].widgetInstance.unmount(); } if (subscriptionPerksContainer) { subscriptionPerksContainer.remove(); } // After cleaning up the previous widget, reassign the id to properly mount the new widget. rechargeContainer.id = `subscription-selector-${newProductId}`; productId = Number(newProductId); const config = { productId: newProductId, injectionParent: `#subscription-selector-${newProductId}` }; // Re-establish and mount new widget window.ReChargeWidget.createWidget(config); // Wait for all elements to mount await waitForRechargeWidgetToBeMounted(); await waitForRechargeSelect(); await relocateSubscriptionPriceAndCleanup(); if (shouldRenderBulletPoints) { assignProductBullets() createBulletPoints(); } handleRechargeEventListeners('add'); configureRechargeForProductSelection(newProductId); // Hide spinner const spinnerElement = document.getElementById('loading-spinner'); if (spinnerElement) { spinnerElement.remove(); } if (rechargeWidgetContainer) { rechargeWidgetContainer.style.opacity = '100'; rechargeWidgetContainer.classList.add('opacity-100', 'transition-opacity', 'duration-300'); } } } /** * @param {string} productId productId responsible for the unique key to be set on the window, so all instances of the widget can be accessed. * @description Function responsible for binding the onNewProduct select to the window. */ function configureRechargeForProductSelection(productId) { window.HairstoryRechargeWidget = window.HairstoryRechargeWidget || {}; window.HairstoryRechargeWidget[productId] = { onNewProductSelect: onNewProductSelect }; } // Initial mount of recharge widget initializeRechargeWidget().catch(error => { console.error("Error initializing ReCharge widget:", error); }); }); View More Information Suited For Gray Hair, Dry Hair, Damaged Hair, Color-Treated Hair What It Is 8OZ NEW WASH RICH, BOND BOOST AND BOND SERUM Benefits NEW Learn More

Richest Damage Repair Method

8OZ NEW WASH RICH, BOND BOOST AND BOND SERUM

Benefits of Using Refillable Shampoo Bottles
| Hairstory (2025)

References

Top Articles
Latest Posts
Recommended Articles
Article information

Author: Jerrold Considine

Last Updated:

Views: 6419

Rating: 4.8 / 5 (58 voted)

Reviews: 81% of readers found this page helpful

Author information

Name: Jerrold Considine

Birthday: 1993-11-03

Address: Suite 447 3463 Marybelle Circles, New Marlin, AL 20765

Phone: +5816749283868

Job: Sales Executive

Hobby: Air sports, Sand art, Electronics, LARPing, Baseball, Book restoration, Puzzles

Introduction: My name is Jerrold Considine, I am a combative, cheerful, encouraging, happy, enthusiastic, funny, kind person who loves writing and wants to share my knowledge and understanding with you.