if (!customElements.get('media-gallery')) {
customElements.define(
'media-gallery',
class MediaGallery extends HTMLElement {
constructor() {
super();
this.elements = {
liveRegion: this.querySelector('[id^="GalleryStatus"]'),
viewer: this.querySelector('[id^="GalleryViewer"]'),
thumbnails: this.querySelector('[id^="GalleryThumbnails"]'),
};
this.mql = window.matchMedia('(min-width: 750px)');
if (!this.elements.thumbnails) return;
this.elements.viewer.addEventListener('slideChanged', debounce(this.onSlideChanged.bind(this), 500));
this.elements.thumbnails.querySelectorAll('[data-target]').forEach((mediaToSwitch) => {
mediaToSwitch
.querySelector('button')
.addEventListener('click', this.setActiveMedia.bind(this, mediaToSwitch.dataset.target, false));
});
if (this.dataset.desktopLayout.includes('thumbnail') && this.mql.matches) this.removeListSemantic();
}
connectedCallback() {
// Listen for variant changes to update gallery
this.variantChangeHandler = this.handleVariantChange.bind(this);
document.addEventListener('variant:change', this.variantChangeHandler);
}
disconnectedCallback() {
// Clean up event listener
if (this.variantChangeHandler) {
document.removeEventListener('variant:change', this.variantChangeHandler);
}
}
handleVariantChange(event) {
const variant = event.detail?.variant;
if (!variant || !variant.featured_media) return;
const featuredMediaId = variant.featured_media.id;
if (!featuredMediaId) return;
// Get section ID from gallery viewer
const galleryViewer = this.elements.viewer;
if (!galleryViewer) return;
const sectionIdMatch = galleryViewer.id.match(/GalleryViewer-(.+)/);
if (!sectionIdMatch) return;
const sectionId = sectionIdMatch[1];
const mediaId = `${sectionId}-${featuredMediaId}`;
// Find the media element
const mediaElement = galleryViewer.querySelector(`[data-media-id="${mediaId}"]`);
if (!mediaElement) {
console.log('Variant media not found in gallery:', mediaId);
return;
}
// Use setActiveMedia with prepend: true to move variant image to first position
this.setActiveMedia(mediaId, true);
// Additional scroll to ensure image is visible after DOM update
requestAnimationFrame(() => {
setTimeout(() => {
const activeElement = galleryViewer.querySelector(`[data-media-id="${mediaId}"]`);
if (activeElement) {
// Scroll gallery container
const sliderList = galleryViewer.querySelector('.slider, .product__media-list, ul');
if (sliderList && sliderList.scrollTo) {
sliderList.scrollTo({
left: activeElement.offsetLeft,
behavior: 'smooth'
});
}
// Scroll page to show the gallery with image in view
const elementRect = activeElement.getBoundingClientRect();
const offset = 120;
const scrollPosition = elementRect.top + window.scrollY - (window.innerHeight / 2) + (elementRect.height / 2);
window.scrollTo({
top: Math.max(0, scrollPosition - offset),
behavior: 'smooth'
});
}
}, 300);
});
}
onSlideChanged(event) {
const thumbnail = this.elements.thumbnails.querySelector(
`[data-target="${event.detail.currentElement.dataset.mediaId}"]`
);
this.setActiveThumbnail(thumbnail);
}
setActiveMedia(mediaId, prepend) {
const activeMedia =
this.elements.viewer.querySelector(`[data-media-id="${mediaId}"]`) ||
this.elements.viewer.querySelector('[data-media-id]');
if (!activeMedia) {
return;
}
this.elements.viewer.querySelectorAll('[data-media-id]').forEach((element) => {
element.classList.remove('is-active');
});
activeMedia?.classList?.add('is-active');
if (prepend) {
activeMedia.parentElement.firstChild !== activeMedia && activeMedia.parentElement.prepend(activeMedia);
if (this.elements.thumbnails) {
const activeThumbnail = this.elements.thumbnails.querySelector(`[data-target="${mediaId}"]`);
activeThumbnail.parentElement.firstChild !== activeThumbnail && activeThumbnail.parentElement.prepend(activeThumbnail);
}
if (this.elements.viewer.slider) this.elements.viewer.resetPages();
}
this.preventStickyHeader();
window.setTimeout(() => {
if (!this.mql.matches || this.elements.thumbnails) {
activeMedia.parentElement.scrollTo({ left: activeMedia.offsetLeft });
}
const activeMediaRect = activeMedia.getBoundingClientRect();
// Don't scroll if the image is already in view
if (activeMediaRect.top > -0.5) return;
const top = activeMediaRect.top + window.scrollY;
window.scrollTo({ top: top, behavior: 'smooth' });
});
this.playActiveMedia(activeMedia);
if (!this.elements.thumbnails) return;
const activeThumbnail = this.elements.thumbnails.querySelector(`[data-target="${mediaId}"]`);
this.setActiveThumbnail(activeThumbnail);
this.announceLiveRegion(activeMedia, activeThumbnail.dataset.mediaPosition);
}
setActiveThumbnail(thumbnail) {
if (!this.elements.thumbnails || !thumbnail) return;
this.elements.thumbnails
.querySelectorAll('button')
.forEach((element) => element.removeAttribute('aria-current'));
thumbnail.querySelector('button').setAttribute('aria-current', true);
if (this.elements.thumbnails.isSlideVisible(thumbnail, 10)) return;
this.elements.thumbnails.slider.scrollTo({ left: thumbnail.offsetLeft });
}
announceLiveRegion(activeItem, position) {
const image = activeItem.querySelector('.product__modal-opener--image img');
if (!image) return;
image.onload = () => {
this.elements.liveRegion.setAttribute('aria-hidden', false);
this.elements.liveRegion.innerHTML = window.accessibilityStrings.imageAvailable.replace('[index]', position);
setTimeout(() => {
this.elements.liveRegion.setAttribute('aria-hidden', true);
}, 2000);
};
image.src = image.src;
}
playActiveMedia(activeItem) {
window.pauseAllMedia();
const deferredMedia = activeItem.querySelector('.deferred-media');
if (deferredMedia) deferredMedia.loadContent(false);
}
preventStickyHeader() {
this.stickyHeader = this.stickyHeader || document.querySelector('sticky-header');
if (!this.stickyHeader) return;
this.stickyHeader.dispatchEvent(new Event('preventHeaderReveal'));
}
removeListSemantic() {
if (!this.elements.viewer.slider) return;
this.elements.viewer.slider.setAttribute('role', 'presentation');
this.elements.viewer.sliderItems.forEach((slide) => slide.setAttribute('role', 'presentation'));
}
}
);
}
if (!customElements.get('media-gallery')) {
customElements.define(
'media-gallery',
class MediaGallery extends HTMLElement {
constructor() {
super();
this.elements = {
liveRegion: this.querySelector('[id^="GalleryStatus"]'),
viewer: this.querySelector('[id^="GalleryViewer"]'),
thumbnails: this.querySelector('[id^="GalleryThumbnails"]'),
};
this.mql = window.matchMedia('(min-width: 750px)');
if (!this.elements.thumbnails) return;
this.elements.viewer.addEventListener('slideChanged', debounce(this.onSlideChanged.bind(this), 500));
this.elements.thumbnails.querySelectorAll('[data-target]').forEach((mediaToSwitch) => {
mediaToSwitch
.querySelector('button')
.addEventListener('click', this.setActiveMedia.bind(this, mediaToSwitch.dataset.target, false));
});
if (this.dataset.desktopLayout.includes('thumbnail') && this.mql.matches) this.removeListSemantic();
}
connectedCallback() {
// Listen for variant changes to update gallery
this.variantChangeHandler = this.handleVariantChange.bind(this);
document.addEventListener('variant:change', this.variantChangeHandler);
}
disconnectedCallback() {
// Clean up event listener
if (this.variantChangeHandler) {
document.removeEventListener('variant:change', this.variantChangeHandler);
}
}
handleVariantChange(event) {
const variant = event.detail?.variant;
if (!variant || !variant.featured_media) return;
const featuredMediaId = variant.featured_media.id;
if (!featuredMediaId) return;
// Get section ID from gallery viewer
const galleryViewer = this.elements.viewer;
if (!galleryViewer) return;
const sectionIdMatch = galleryViewer.id.match(/GalleryViewer-(.+)/);
if (!sectionIdMatch) return;
const sectionId = sectionIdMatch[1];
const mediaId = `${sectionId}-${featuredMediaId}`;
// Find the media element
const mediaElement = galleryViewer.querySelector(`[data-media-id="${mediaId}"]`);
if (!mediaElement) {
console.log('Variant media not found in gallery:', mediaId);
return;
}
// Use setActiveMedia with prepend: true to move variant image to first position
this.setActiveMedia(mediaId, true);
// Additional scroll to ensure image is visible after DOM update
requestAnimationFrame(() => {
setTimeout(() => {
const activeElement = galleryViewer.querySelector(`[data-media-id="${mediaId}"]`);
if (activeElement) {
// Scroll gallery container
const sliderList = galleryViewer.querySelector('.slider, .product__media-list, ul');
if (sliderList && sliderList.scrollTo) {
sliderList.scrollTo({
left: activeElement.offsetLeft,
behavior: 'smooth'
});
}
// Scroll page to show the gallery with image in view
const elementRect = activeElement.getBoundingClientRect();
const offset = 120;
const scrollPosition = elementRect.top + window.scrollY - (window.innerHeight / 2) + (elementRect.height / 2);
window.scrollTo({
top: Math.max(0, scrollPosition - offset),
behavior: 'smooth'
});
}
}, 300);
});
}
onSlideChanged(event) {
const thumbnail = this.elements.thumbnails.querySelector(
`[data-target="${event.detail.currentElement.dataset.mediaId}"]`
);
this.setActiveThumbnail(thumbnail);
}
setActiveMedia(mediaId, prepend) {
const activeMedia =
this.elements.viewer.querySelector(`[data-media-id="${mediaId}"]`) ||
this.elements.viewer.querySelector('[data-media-id]');
if (!activeMedia) {
return;
}
this.elements.viewer.querySelectorAll('[data-media-id]').forEach((element) => {
element.classList.remove('is-active');
});
activeMedia?.classList?.add('is-active');
if (prepend) {
activeMedia.parentElement.firstChild !== activeMedia && activeMedia.parentElement.prepend(activeMedia);
if (this.elements.thumbnails) {
const activeThumbnail = this.elements.thumbnails.querySelector(`[data-target="${mediaId}"]`);
activeThumbnail.parentElement.firstChild !== activeThumbnail && activeThumbnail.parentElement.prepend(activeThumbnail);
}
if (this.elements.viewer.slider) this.elements.viewer.resetPages();
}
this.preventStickyHeader();
window.setTimeout(() => {
if (!this.mql.matches || this.elements.thumbnails) {
activeMedia.parentElement.scrollTo({ left: activeMedia.offsetLeft });
}
const activeMediaRect = activeMedia.getBoundingClientRect();
// Don't scroll if the image is already in view
if (activeMediaRect.top > -0.5) return;
const top = activeMediaRect.top + window.scrollY;
window.scrollTo({ top: top, behavior: 'smooth' });
});
this.playActiveMedia(activeMedia);
if (!this.elements.thumbnails) return;
const activeThumbnail = this.elements.thumbnails.querySelector(`[data-target="${mediaId}"]`);
this.setActiveThumbnail(activeThumbnail);
this.announceLiveRegion(activeMedia, activeThumbnail.dataset.mediaPosition);
}
setActiveThumbnail(thumbnail) {
if (!this.elements.thumbnails || !thumbnail) return;
this.elements.thumbnails
.querySelectorAll('button')
.forEach((element) => element.removeAttribute('aria-current'));
thumbnail.querySelector('button').setAttribute('aria-current', true);
if (this.elements.thumbnails.isSlideVisible(thumbnail, 10)) return;
this.elements.thumbnails.slider.scrollTo({ left: thumbnail.offsetLeft });
}
announceLiveRegion(activeItem, position) {
const image = activeItem.querySelector('.product__modal-opener--image img');
if (!image) return;
image.onload = () => {
this.elements.liveRegion.setAttribute('aria-hidden', false);
this.elements.liveRegion.innerHTML = window.accessibilityStrings.imageAvailable.replace('[index]', position);
setTimeout(() => {
this.elements.liveRegion.setAttribute('aria-hidden', true);
}, 2000);
};
image.src = image.src;
}
playActiveMedia(activeItem) {
window.pauseAllMedia();
const deferredMedia = activeItem.querySelector('.deferred-media');
if (deferredMedia) deferredMedia.loadContent(false);
}
preventStickyHeader() {
this.stickyHeader = this.stickyHeader || document.querySelector('sticky-header');
if (!this.stickyHeader) return;
this.stickyHeader.dispatchEvent(new Event('preventHeaderReveal'));
}
removeListSemantic() {
if (!this.elements.viewer.slider) return;
this.elements.viewer.slider.setAttribute('role', 'presentation');
this.elements.viewer.sliderItems.forEach((slide) => slide.setAttribute('role', 'presentation'));
}
}
);
}
if (!customElements.get('media-gallery')) {
customElements.define(
'media-gallery',
class MediaGallery extends HTMLElement {
constructor() {
super();
this.elements = {
liveRegion: this.querySelector('[id^="GalleryStatus"]'),
viewer: this.querySelector('[id^="GalleryViewer"]'),
thumbnails: this.querySelector('[id^="GalleryThumbnails"]'),
};
this.mql = window.matchMedia('(min-width: 750px)');
if (!this.elements.thumbnails) return;
this.elements.viewer.addEventListener('slideChanged', debounce(this.onSlideChanged.bind(this), 500));
this.elements.thumbnails.querySelectorAll('[data-target]').forEach((mediaToSwitch) => {
mediaToSwitch
.querySelector('button')
.addEventListener('click', this.setActiveMedia.bind(this, mediaToSwitch.dataset.target, false));
});
if (this.dataset.desktopLayout.includes('thumbnail') && this.mql.matches) this.removeListSemantic();
}
connectedCallback() {
// Listen for variant changes to update gallery
this.variantChangeHandler = this.handleVariantChange.bind(this);
document.addEventListener('variant:change', this.variantChangeHandler);
}
disconnectedCallback() {
// Clean up event listener
if (this.variantChangeHandler) {
document.removeEventListener('variant:change', this.variantChangeHandler);
}
}
handleVariantChange(event) {
const variant = event.detail?.variant;
if (!variant || !variant.featured_media) return;
const featuredMediaId = variant.featured_media.id;
if (!featuredMediaId) return;
// Get section ID from gallery viewer
const galleryViewer = this.elements.viewer;
if (!galleryViewer) return;
const sectionIdMatch = galleryViewer.id.match(/GalleryViewer-(.+)/);
if (!sectionIdMatch) return;
const sectionId = sectionIdMatch[1];
const mediaId = `${sectionId}-${featuredMediaId}`;
// Find the media element
const mediaElement = galleryViewer.querySelector(`[data-media-id="${mediaId}"]`);
if (!mediaElement) {
console.log('Variant media not found in gallery:', mediaId);
return;
}
// Use setActiveMedia with prepend: true to move variant image to first position
this.setActiveMedia(mediaId, true);
// Additional scroll to ensure image is visible after DOM update
requestAnimationFrame(() => {
setTimeout(() => {
const activeElement = galleryViewer.querySelector(`[data-media-id="${mediaId}"]`);
if (activeElement) {
// Scroll gallery container
const sliderList = galleryViewer.querySelector('.slider, .product__media-list, ul');
if (sliderList && sliderList.scrollTo) {
sliderList.scrollTo({
left: activeElement.offsetLeft,
behavior: 'smooth'
});
}
// Scroll page to show the gallery with image in view
const elementRect = activeElement.getBoundingClientRect();
const offset = 120;
const scrollPosition = elementRect.top + window.scrollY - (window.innerHeight / 2) + (elementRect.height / 2);
window.scrollTo({
top: Math.max(0, scrollPosition - offset),
behavior: 'smooth'
});
}
}, 300);
});
}
onSlideChanged(event) {
const thumbnail = this.elements.thumbnails.querySelector(
`[data-target="${event.detail.currentElement.dataset.mediaId}"]`
);
this.setActiveThumbnail(thumbnail);
}
setActiveMedia(mediaId, prepend) {
const activeMedia =
this.elements.viewer.querySelector(`[data-media-id="${mediaId}"]`) ||
this.elements.viewer.querySelector('[data-media-id]');
if (!activeMedia) {
return;
}
this.elements.viewer.querySelectorAll('[data-media-id]').forEach((element) => {
element.classList.remove('is-active');
});
activeMedia?.classList?.add('is-active');
if (prepend) {
activeMedia.parentElement.firstChild !== activeMedia && activeMedia.parentElement.prepend(activeMedia);
if (this.elements.thumbnails) {
const activeThumbnail = this.elements.thumbnails.querySelector(`[data-target="${mediaId}"]`);
activeThumbnail.parentElement.firstChild !== activeThumbnail && activeThumbnail.parentElement.prepend(activeThumbnail);
}
if (this.elements.viewer.slider) this.elements.viewer.resetPages();
}
this.preventStickyHeader();
window.setTimeout(() => {
if (!this.mql.matches || this.elements.thumbnails) {
activeMedia.parentElement.scrollTo({ left: activeMedia.offsetLeft });
}
const activeMediaRect = activeMedia.getBoundingClientRect();
// Don't scroll if the image is already in view
if (activeMediaRect.top > -0.5) return;
const top = activeMediaRect.top + window.scrollY;
window.scrollTo({ top: top, behavior: 'smooth' });
});
this.playActiveMedia(activeMedia);
if (!this.elements.thumbnails) return;
const activeThumbnail = this.elements.thumbnails.querySelector(`[data-target="${mediaId}"]`);
this.setActiveThumbnail(activeThumbnail);
this.announceLiveRegion(activeMedia, activeThumbnail.dataset.mediaPosition);
}
setActiveThumbnail(thumbnail) {
if (!this.elements.thumbnails || !thumbnail) return;
this.elements.thumbnails
.querySelectorAll('button')
.forEach((element) => element.removeAttribute('aria-current'));
thumbnail.querySelector('button').setAttribute('aria-current', true);
if (this.elements.thumbnails.isSlideVisible(thumbnail, 10)) return;
this.elements.thumbnails.slider.scrollTo({ left: thumbnail.offsetLeft });
}
announceLiveRegion(activeItem, position) {
const image = activeItem.querySelector('.product__modal-opener--image img');
if (!image) return;
image.onload = () => {
this.elements.liveRegion.setAttribute('aria-hidden', false);
this.elements.liveRegion.innerHTML = window.accessibilityStrings.imageAvailable.replace('[index]', position);
setTimeout(() => {
this.elements.liveRegion.setAttribute('aria-hidden', true);
}, 2000);
};
image.src = image.src;
}
playActiveMedia(activeItem) {
window.pauseAllMedia();
const deferredMedia = activeItem.querySelector('.deferred-media');
if (deferredMedia) deferredMedia.loadContent(false);
}
preventStickyHeader() {
this.stickyHeader = this.stickyHeader || document.querySelector('sticky-header');
if (!this.stickyHeader) return;
this.stickyHeader.dispatchEvent(new Event('preventHeaderReveal'));
}
removeListSemantic() {
if (!this.elements.viewer.slider) return;
this.elements.viewer.slider.setAttribute('role', 'presentation');
this.elements.viewer.sliderItems.forEach((slide) => slide.setAttribute('role', 'presentation'));
}
}
);
}
if (!customElements.get('media-gallery')) {
customElements.define(
'media-gallery',
class MediaGallery extends HTMLElement {
constructor() {
super();
this.elements = {
liveRegion: this.querySelector('[id^="GalleryStatus"]'),
viewer: this.querySelector('[id^="GalleryViewer"]'),
thumbnails: this.querySelector('[id^="GalleryThumbnails"]'),
};
this.mql = window.matchMedia('(min-width: 750px)');
if (!this.elements.thumbnails) return;
this.elements.viewer.addEventListener('slideChanged', debounce(this.onSlideChanged.bind(this), 500));
this.elements.thumbnails.querySelectorAll('[data-target]').forEach((mediaToSwitch) => {
mediaToSwitch
.querySelector('button')
.addEventListener('click', this.setActiveMedia.bind(this, mediaToSwitch.dataset.target, false));
});
if (this.dataset.desktopLayout.includes('thumbnail') && this.mql.matches) this.removeListSemantic();
}
connectedCallback() {
// Listen for variant changes to update gallery
this.variantChangeHandler = this.handleVariantChange.bind(this);
document.addEventListener('variant:change', this.variantChangeHandler);
}
disconnectedCallback() {
// Clean up event listener
if (this.variantChangeHandler) {
document.removeEventListener('variant:change', this.variantChangeHandler);
}
}
handleVariantChange(event) {
const variant = event.detail?.variant;
if (!variant || !variant.featured_media) return;
const featuredMediaId = variant.featured_media.id;
if (!featuredMediaId) return;
// Get section ID from gallery viewer
const galleryViewer = this.elements.viewer;
if (!galleryViewer) return;
const sectionIdMatch = galleryViewer.id.match(/GalleryViewer-(.+)/);
if (!sectionIdMatch) return;
const sectionId = sectionIdMatch[1];
const mediaId = `${sectionId}-${featuredMediaId}`;
// Find the media element
const mediaElement = galleryViewer.querySelector(`[data-media-id="${mediaId}"]`);
if (!mediaElement) {
console.log('Variant media not found in gallery:', mediaId);
return;
}
// Use setActiveMedia with prepend: true to move variant image to first position
this.setActiveMedia(mediaId, true);
// Additional scroll to ensure image is visible after DOM update
requestAnimationFrame(() => {
setTimeout(() => {
const activeElement = galleryViewer.querySelector(`[data-media-id="${mediaId}"]`);
if (activeElement) {
// Scroll gallery container
const sliderList = galleryViewer.querySelector('.slider, .product__media-list, ul');
if (sliderList && sliderList.scrollTo) {
sliderList.scrollTo({
left: activeElement.offsetLeft,
behavior: 'smooth'
});
}
// Scroll page to show the gallery with image in view
const elementRect = activeElement.getBoundingClientRect();
const offset = 120;
const scrollPosition = elementRect.top + window.scrollY - (window.innerHeight / 2) + (elementRect.height / 2);
window.scrollTo({
top: Math.max(0, scrollPosition - offset),
behavior: 'smooth'
});
}
}, 300);
});
}
onSlideChanged(event) {
const thumbnail = this.elements.thumbnails.querySelector(
`[data-target="${event.detail.currentElement.dataset.mediaId}"]`
);
this.setActiveThumbnail(thumbnail);
}
setActiveMedia(mediaId, prepend) {
const activeMedia =
this.elements.viewer.querySelector(`[data-media-id="${mediaId}"]`) ||
this.elements.viewer.querySelector('[data-media-id]');
if (!activeMedia) {
return;
}
this.elements.viewer.querySelectorAll('[data-media-id]').forEach((element) => {
element.classList.remove('is-active');
});
activeMedia?.classList?.add('is-active');
if (prepend) {
activeMedia.parentElement.firstChild !== activeMedia && activeMedia.parentElement.prepend(activeMedia);
if (this.elements.thumbnails) {
const activeThumbnail = this.elements.thumbnails.querySelector(`[data-target="${mediaId}"]`);
activeThumbnail.parentElement.firstChild !== activeThumbnail && activeThumbnail.parentElement.prepend(activeThumbnail);
}
if (this.elements.viewer.slider) this.elements.viewer.resetPages();
}
this.preventStickyHeader();
window.setTimeout(() => {
if (!this.mql.matches || this.elements.thumbnails) {
activeMedia.parentElement.scrollTo({ left: activeMedia.offsetLeft });
}
const activeMediaRect = activeMedia.getBoundingClientRect();
// Don't scroll if the image is already in view
if (activeMediaRect.top > -0.5) return;
const top = activeMediaRect.top + window.scrollY;
window.scrollTo({ top: top, behavior: 'smooth' });
});
this.playActiveMedia(activeMedia);
if (!this.elements.thumbnails) return;
const activeThumbnail = this.elements.thumbnails.querySelector(`[data-target="${mediaId}"]`);
this.setActiveThumbnail(activeThumbnail);
this.announceLiveRegion(activeMedia, activeThumbnail.dataset.mediaPosition);
}
setActiveThumbnail(thumbnail) {
if (!this.elements.thumbnails || !thumbnail) return;
this.elements.thumbnails
.querySelectorAll('button')
.forEach((element) => element.removeAttribute('aria-current'));
thumbnail.querySelector('button').setAttribute('aria-current', true);
if (this.elements.thumbnails.isSlideVisible(thumbnail, 10)) return;
this.elements.thumbnails.slider.scrollTo({ left: thumbnail.offsetLeft });
}
announceLiveRegion(activeItem, position) {
const image = activeItem.querySelector('.product__modal-opener--image img');
if (!image) return;
image.onload = () => {
this.elements.liveRegion.setAttribute('aria-hidden', false);
this.elements.liveRegion.innerHTML = window.accessibilityStrings.imageAvailable.replace('[index]', position);
setTimeout(() => {
this.elements.liveRegion.setAttribute('aria-hidden', true);
}, 2000);
};
image.src = image.src;
}
playActiveMedia(activeItem) {
window.pauseAllMedia();
const deferredMedia = activeItem.querySelector('.deferred-media');
if (deferredMedia) deferredMedia.loadContent(false);
}
preventStickyHeader() {
this.stickyHeader = this.stickyHeader || document.querySelector('sticky-header');
if (!this.stickyHeader) return;
this.stickyHeader.dispatchEvent(new Event('preventHeaderReveal'));
}
removeListSemantic() {
if (!this.elements.viewer.slider) return;
this.elements.viewer.slider.setAttribute('role', 'presentation');
this.elements.viewer.sliderItems.forEach((slide) => slide.setAttribute('role', 'presentation'));
}
}
);
}
if (!customElements.get('media-gallery')) {
customElements.define(
'media-gallery',
class MediaGallery extends HTMLElement {
constructor() {
super();
this.elements = {
liveRegion: this.querySelector('[id^="GalleryStatus"]'),
viewer: this.querySelector('[id^="GalleryViewer"]'),
thumbnails: this.querySelector('[id^="GalleryThumbnails"]'),
};
this.mql = window.matchMedia('(min-width: 750px)');
if (!this.elements.thumbnails) return;
this.elements.viewer.addEventListener('slideChanged', debounce(this.onSlideChanged.bind(this), 500));
this.elements.thumbnails.querySelectorAll('[data-target]').forEach((mediaToSwitch) => {
mediaToSwitch
.querySelector('button')
.addEventListener('click', this.setActiveMedia.bind(this, mediaToSwitch.dataset.target, false));
});
if (this.dataset.desktopLayout.includes('thumbnail') && this.mql.matches) this.removeListSemantic();
}
connectedCallback() {
// Listen for variant changes to update gallery
this.variantChangeHandler = this.handleVariantChange.bind(this);
document.addEventListener('variant:change', this.variantChangeHandler);
}
disconnectedCallback() {
// Clean up event listener
if (this.variantChangeHandler) {
document.removeEventListener('variant:change', this.variantChangeHandler);
}
}
handleVariantChange(event) {
const variant = event.detail?.variant;
if (!variant || !variant.featured_media) return;
const featuredMediaId = variant.featured_media.id;
if (!featuredMediaId) return;
// Get section ID from gallery viewer
const galleryViewer = this.elements.viewer;
if (!galleryViewer) return;
const sectionIdMatch = galleryViewer.id.match(/GalleryViewer-(.+)/);
if (!sectionIdMatch) return;
const sectionId = sectionIdMatch[1];
const mediaId = `${sectionId}-${featuredMediaId}`;
// Find the media element
const mediaElement = galleryViewer.querySelector(`[data-media-id="${mediaId}"]`);
if (!mediaElement) {
console.log('Variant media not found in gallery:', mediaId);
return;
}
// Use setActiveMedia with prepend: true to move variant image to first position
this.setActiveMedia(mediaId, true);
// Additional scroll to ensure image is visible after DOM update
requestAnimationFrame(() => {
setTimeout(() => {
const activeElement = galleryViewer.querySelector(`[data-media-id="${mediaId}"]`);
if (activeElement) {
// Scroll gallery container
const sliderList = galleryViewer.querySelector('.slider, .product__media-list, ul');
if (sliderList && sliderList.scrollTo) {
sliderList.scrollTo({
left: activeElement.offsetLeft,
behavior: 'smooth'
});
}
// Scroll page to show the gallery with image in view
const elementRect = activeElement.getBoundingClientRect();
const offset = 120;
const scrollPosition = elementRect.top + window.scrollY - (window.innerHeight / 2) + (elementRect.height / 2);
window.scrollTo({
top: Math.max(0, scrollPosition - offset),
behavior: 'smooth'
});
}
}, 300);
});
}
onSlideChanged(event) {
const thumbnail = this.elements.thumbnails.querySelector(
`[data-target="${event.detail.currentElement.dataset.mediaId}"]`
);
this.setActiveThumbnail(thumbnail);
}
setActiveMedia(mediaId, prepend) {
const activeMedia =
this.elements.viewer.querySelector(`[data-media-id="${mediaId}"]`) ||
this.elements.viewer.querySelector('[data-media-id]');
if (!activeMedia) {
return;
}
this.elements.viewer.querySelectorAll('[data-media-id]').forEach((element) => {
element.classList.remove('is-active');
});
activeMedia?.classList?.add('is-active');
if (prepend) {
activeMedia.parentElement.firstChild !== activeMedia && activeMedia.parentElement.prepend(activeMedia);
if (this.elements.thumbnails) {
const activeThumbnail = this.elements.thumbnails.querySelector(`[data-target="${mediaId}"]`);
activeThumbnail.parentElement.firstChild !== activeThumbnail && activeThumbnail.parentElement.prepend(activeThumbnail);
}
if (this.elements.viewer.slider) this.elements.viewer.resetPages();
}
this.preventStickyHeader();
window.setTimeout(() => {
if (!this.mql.matches || this.elements.thumbnails) {
activeMedia.parentElement.scrollTo({ left: activeMedia.offsetLeft });
}
const activeMediaRect = activeMedia.getBoundingClientRect();
// Don't scroll if the image is already in view
if (activeMediaRect.top > -0.5) return;
const top = activeMediaRect.top + window.scrollY;
window.scrollTo({ top: top, behavior: 'smooth' });
});
this.playActiveMedia(activeMedia);
if (!this.elements.thumbnails) return;
const activeThumbnail = this.elements.thumbnails.querySelector(`[data-target="${mediaId}"]`);
this.setActiveThumbnail(activeThumbnail);
this.announceLiveRegion(activeMedia, activeThumbnail.dataset.mediaPosition);
}
setActiveThumbnail(thumbnail) {
if (!this.elements.thumbnails || !thumbnail) return;
this.elements.thumbnails
.querySelectorAll('button')
.forEach((element) => element.removeAttribute('aria-current'));
thumbnail.querySelector('button').setAttribute('aria-current', true);
if (this.elements.thumbnails.isSlideVisible(thumbnail, 10)) return;
this.elements.thumbnails.slider.scrollTo({ left: thumbnail.offsetLeft });
}
announceLiveRegion(activeItem, position) {
const image = activeItem.querySelector('.product__modal-opener--image img');
if (!image) return;
image.onload = () => {
this.elements.liveRegion.setAttribute('aria-hidden', false);
this.elements.liveRegion.innerHTML = window.accessibilityStrings.imageAvailable.replace('[index]', position);
setTimeout(() => {
this.elements.liveRegion.setAttribute('aria-hidden', true);
}, 2000);
};
image.src = image.src;
}
playActiveMedia(activeItem) {
window.pauseAllMedia();
const deferredMedia = activeItem.querySelector('.deferred-media');
if (deferredMedia) deferredMedia.loadContent(false);
}
preventStickyHeader() {
this.stickyHeader = this.stickyHeader || document.querySelector('sticky-header');
if (!this.stickyHeader) return;
this.stickyHeader.dispatchEvent(new Event('preventHeaderReveal'));
}
removeListSemantic() {
if (!this.elements.viewer.slider) return;
this.elements.viewer.slider.setAttribute('role', 'presentation');
this.elements.viewer.sliderItems.forEach((slide) => slide.setAttribute('role', 'presentation'));
}
}
);
}
100,000+ Happy Customers
Sculpting Facial Roller
£33.99
£25.99
LOW STOCK
close_small
Reduces Puffiness & Facial Tension
self_improvement
Supports Lymphatic Drainage
brightness_5
Promotes a calm, sculpted glow
Order by ... for guaranteed FREE GIFTS of 20,98£
local_shippingFree 3-5 Day Insured Shipping
workspace_premium30-Day Satisfaction Guarantee
lockSecured Checkout
Today’s Free Beauty Ritual
Collagen Eye Mask
A cooling collagen ritual that soothes tired eyes, reduces puffiness, and restores your natural glow. A gentle, effortles upgrade to tour self-care routine
Yours Free Today With Your Order
Real women. Real skin. Real results.
Product information
Reduced puffiness & fluid buildup
Relaxes Facial Muscles
Enhance Natural Glow
Supports Skincare Absorption
Encourages A Calming Daily Ritual
Nuvoria Sculpting Facial Roller
Refined Nuvoria Packaging
Nuvoria Ritual Card
Visibly Reduced Puffiness With Consistent Use
More Relaxed Facial Muscles
Healthier Looking Glow Over Time
Improved Absorption Of Skincare Products
Based on customer feedback & post-purchase surveys.
Featured in luxury self-care collections
Loved by over 100,000 women worldwide
Recognised for comfort & skin-friendly design
Chosen by wellness & beauty experts
Winner of European Beauty Award 2025
Use on clean, hydrated skin
Roll upward and outward with light pressure
Focus on Jawline, cheeks and under eye area
Use Daily for 1-5 minutes
Natural stone roller
Smooth, balanced metal frame
Designed for gentle, daily facial massage
Each stone has a unique natural pattern
Wipe clean with a soft, damp cloth
Dry thoroughly after use
Optional: store in refrigerator for cooling effect
Avoid dropping to protect the stone
Suitable for all skin types
Gentle enough for sensitive skin
No Pulling, tugging or irritation
Designed for daily rituals, not aggressive treatments
30-day satisfaction
2-3 day free insured delivery
No questions asked
A Moment to Release & Restore
A simple ritual that helps your face relax, reset, and glow again.
Evening Release
After a long day, your face holds tension you don’t even notice.Slow, cooling strokes help release tightness and invite calm turning skincare into a moment of quiet relief.
Deep Facial Relaxation
Gentle rolling helps stimulate circulation and support lymphatic flow.Puffiness softens. Skin feels lighter.Your features begin to look more relaxed and refreshed.
A Glow You Can Feel
With regular use, skin appears smoother, more balanced, and awake.Not from forcing results but from giving your skin the care it responds to best.
Results You Can Feel
94%
Noticed a more relaxed, less tense face after use.
91%
Felt reduced puffiness, especially around eyes and jawline.
89%
Skin looked smoother and more refreshed after the first week.
92%
Experienced a calmer, more balanced skin appearance.
90%
Said it became a calming daily ritual they looked forward to.
Based on 2-week user reviews
Real Results for Real Skin Concerns
See how women like you transformed their skin with a simple daily ritual.
★★★★★
Laura, 32
My face always felt tight by the end of the day. After using the roller for a week, my facial muscles finally feel relaxed again. It’s become my favorite evening ritual.
★★★★★
Sophie, 29
I didn’t realise how much tension I was holding in my face until I started rolling daily. My skin looks softer and my face feels lighter.
★★★★★
Emma, 35
I use it after long workdays and can literally feel my face unwind. Especially my forehead and cheeks feel less stiff.
★★★★★
Amey, 23
The roller helps me slow down. My face feels calm instead of tense, and my skin looks more rested.
★★★★★
Hannah, 38
I clench my face a lot without noticing. This tool helped release that tension gently, without irritation.
★★★★★
Mila, 31
My morning puffiness around the eyes went down noticeably. My face looks more sculpted and refreshed after use.
★★★★★
Iris, 34
I keep the roller in the fridge and use it every morning. The de-puffing effect is instant and feels amazing.
★★★★★
Charlotte, 28
My cheeks and jawline look less swollen, especially after stressful days or poor sleep.
★★★★★
Aisha, 36
I finally found something that helps with under-eye puffiness without harsh products.
★★★★★
Femke, 40
My face looks less ‘bloated’ in the morning. It gives me a fresh start to the day.
★★★★★
Olivia, 30
My skin looked tired and flat before. After a week of rolling, it looks brighter and more alive.
★★★★★
Lina, 26
I use it before applying serum and my skin absorbs products so much better now.
★★★★★
Roos, 33
My complexion looks smoother and healthier, even on no-makeup days.
★★★★★
Lisann, 30
It gives my skin that natural glow without making it shiny or irritated.
★★★★★
Julia, 37
I carry stress in my jaw and neck. The roller helps release that tension gently and naturally.
★★★★★
Nina, 29
I use it during my evening routine and it helps me mentally switch off too.
★★★★★
Hailey, 28
The jaw area feels less tight and my face looks more relaxed overall
★★★★★
Riham, 30
It’s not just skincare, it’s self-care. My face and mind both feel calmer.
Why Women Choose Nuvoria
Features
Nuvoria
Others
Natural stone roller
Smooth, silent rolling
No creases & pressure lines
Gentle on sensitive skin
Premium weight & balance
Complete Your Routine
Glow Moisturizer Creme
£54.99
£65.00
★★★★★
(65.876)
Collagen Eye Mask
£27.99
£37.99
★★★★★
(31.837)
Premium Cotton Bath Towel
£29.99
£39.99
★★★★★
(43.273)
What Experts Say
Trusted guidance from female doctors who understand skin, sleep, and calm routines.
Dr. Megan Burst
Dermatologist
“Facial rolling supports circulation and muscle relaxation. When used gently and consistently, it can visibly improve puffiness and skin tone.”
Dr. Ava Sinclair
Skin Medicine Specialist
“Cooling stone tools are especially beneficial for calming reactive skin and reducing morning swelling.”
Dr. Lina Carter
Regenerative Medicine
“Facial massage encourages relaxation of overactive facial muscles and supports a healthier skin appearance.”
You relax we’ll take care of the rest.
Because your self-care routine should come with total confidence.
favorite
Love It or Return It
30-day, no-questions-asked guarantee.
support_agent
Personal Customer Care
We’re here whenever you need us.
local_shipping
Fast & Tracked Shipping
Dispatched within 3-5 Days
Product Questions & Answers
How do I use the facial roller correctly?
Nuvoria
Use gentle, upward strokes on clean skin. Start from the center of your face and roll outward toward the ears and hairline.
Can I use it with skincare products?
Nuvoria
Yes. The roller works best after applying a serum or facial oil. It helps spread products evenly and improves absorption into the skin.
How long should one session take?
Nuvoria
A full routine takes about 3–5 minutes. You can focus on specific areas like the jaw, cheeks, or under-eyes if you’re short on time.
Is it suitable for beginners?
Nuvoria
Absolutely. The facial roller is designed to be intuitive and easy to use, even if you’ve never used facial tools before.
When will I start seeing results?
Nuvoria
Many users notice reduced puffiness and a calmer look after the first use. Visible improvements in glow and skin texture typically appear within 1–2 weeks of consistent use.
Does it really help with puffiness?
Nuvoria
Yes. The rolling motion supports lymphatic drainage, which helps reduce fluid buildup that causes puffiness especially around the eyes and cheeks.
Are the results permanent?
Nuvoria
Results are cumulative. Consistent daily or near-daily use maintains the benefits and improves overall skin appearance over time.
Is the facial roller suitable for all skin types?
Nuvoria
Yes. It’s safe for all skin types, including sensitive, dry, oily, and combination skin.
Yes. It’s safe for all skin types, including sensitive, dry, oily, and combination skin.
Nuvoria
Yes, as long as you use gentle pressure. The roller does not exfoliate or irritate the skin when used correctly.
Can I use it if I have acne or breakouts?
Nuvoria
You can use it on areas without active inflammation. Avoid rolling directly over painful or open breakouts.
What is the roller made of?
Nuvoria
The roller is crafted from natural stone with a smooth, skin-friendly finish and a sturdy metal frame for long-lasting use.
How do I clean the facial roller?How do I clean the facial roller?
Nuvoria
After each use, wipe it with a soft cloth. For deeper cleaning, use mild soap and lukewarm water, then dry thoroughly.
Can I store it in the fridge?
Nuvoria
Yes. Chilling the roller enhances the de-puffing and calming effect, especially in the morning.
Will the stone lose its effect over time?
Nuvoria
No. The roller maintains its properties with proper care and gentle handling.
How often should I use the facial roller?
Nuvoria
For best results, use it once daily. Morning for de-puffing, evening for relaxation or both.
Should I use it in the morning or evening?
Nuvoria
Both work well. Morning reduces puffiness and refreshes skin. Evening releases tension and supports relaxation
Can I combine it with other tools like gua sha?
Nuvoria
Yes. The roller is perfect for daily use, while gua sha can be added 2–3 times per week for deeper sculpting.
Is it okay to use it as part of a longer self-care routine?
Nuvoria
Is it okay to use it as part of a longer self-care routine?
Glow starts at Nuvoria
Join 200.000+ in the Nuvoria Glow Movement, become one of us
Members get exclusive discounts, early access to new products and more.
Choosing a selection results in a full page refresh.