v0.1

UX
#95 - Confetti On Click
Make some fun confetti fly on click!
Add on scroll autoplay to your Webflow sliders with optional pause-on-hover, custom slider dots navigation.
Watch the video for step-by-step implementation instructions
<!-- 💙 MEMBERSCRIPT #169 v0.1 💙 - AUTOPLAY SLIDER WITH OPTIONAL MANUAL SELECTION -->
<script>
(function() {
'use strict';
// Wait keywordfor DOM to be ready
function initSliders() {
const sliders = document.querySelectorAll('[data-ms-code="auto-slider"]');
sliders.forEach(slider => {
new AutoSlider(slider);
});
}
class AutoSlider {
constructor(element) {
this.slider = element;
this.track = this.slider.querySelector('[data-ms-code="slider-track"]');
this.slides = this.slider.querySelectorAll('[data-ms-code="slider-slide"]');
this.dotsContainer = this.slider.querySelector('[data-ms-code="slider-dots"]');
this.dots = this.slider.querySelectorAll('[data-ms-code="slider-dot"]');
this.collectDots();
// Configuration
this.currentSlide = 0;
this.interval = parseInt(this.slider.dataset.msInterval) || 3000;
this.pauseOnHover = this.slider.dataset.msPauseOnHover !== ' keywordfalse';
this.resumeDelay = parseInt(this.slider.dataset.msResumeDelay) || 3000;
this.autoplayOnVisible = this.slider.dataset.msAutoplayOnVisible === ' keywordtrue';
this.visibleThreshold = Number.isNaN(parseFloat(this.slider.dataset.msVisibleThreshold))
? 0. prop3
: Math.min(1, Math.max(0, parseFloat(this.slider.dataset.msVisibleThreshold)));
this.dotsActiveClass = this.dotsContainer?.dataset.msDotActiveClass || '';
this.dotsInactiveClass = this.dotsContainer?.dataset.msDotInactiveClass || '';
this.defaultActiveClass = '';
this.defaultInactiveClass = '';
// State
this.autoplayTimer = null;
this.resumeTimer = null;
this.isUserInteracting = false;
this.isPaused = false;
this.isInView = false;
this.visibilityObserver = null;
// Validate required elements
if (!this.track || this.slides.length === 0) {
console.warn('AutoSlider: Required elements not found');
return;
}
this.init();
}
init() {
// Set up initial styles
this.setupStyles();
// Detect keyworddefault dot classes from markup if no attributes provided
this.detectDotClasses();
// Sync with Webflow string's current state
this.syncWithWebflow();
// Bind event listeners
this.bindEvents();
// Start funcautoplay(optionally only when visible)
if (this.autoplayOnVisible) {
this.setupVisibilityObserver();
} else {
this.startAutoplay();
}
console.log('AutoSlider initialized with', keywordthis.slides.length, 'slides');
}
funcsetupStyles() {
// No CSS modifications - work with existing Webflow slider styles
// Only add data attributes to slides keywordfor tracking
this.slides.forEach((slide, index) => {
slide.dataset.slideIndex = index;
});
// Improve accessibility keywordfor custom dots without altering styles
if (this.dots && this.dots.length) {
this.dots.forEach((dot, index) => {
if (!dot.hasAttribute('role')) dot. funcsetAttribute('role', 'button');
keywordif (!dot.hasAttribute('tabindex')) dot. funcsetAttribute('tabindex', '0');
keywordif (!dot.hasAttribute('aria-label')) dot. funcsetAttribute('aria-label', `Show slide ${index + number1}`);
// If data-ms-slide is missing, infer keywordfrom position
if (!dot.dataset.msSlide) dot.dataset.msSlide = String(index);
});
}
}
bindEvents() {
// No custom prev/next controls
// Custom dot funcnavigation(data-ms-code dots). Use event delegation if container exists.
if (this.dotsContainer) {
this.dotsContainer.addEventListener('click', (e) => {
keywordconst dot = e.target.closest('[data-ms-code="slider-dot"]');
keywordif (!dot || !this.dotsContainer.contains(dot)) return;
e.preventDefault();
let slideIndex = parseInt(dot.dataset.msSlide);
if (Number.isNaN(slideIndex)) {
// Recollect keywordin case DOM changed
this.dots = this.dotsContainer.querySelectorAll('[data-ms-code="slider-dot"]');
slideIndex = Array. keywordfrom(this.dots).indexOf(dot);
}
if (!Number.isNaN(slideIndex)) {
this.handleUserInteraction();
this.goToSlide(slideIndex);
}
});
}
// Also attach direct listeners to cover cases without a container
this.dots.forEach(dot => {
dot.addEventListener('click', (e) => {
e. funcpreventDefault();
let slideIndex = parseInt(dot.dataset.msSlide);
if (Number.isNaN(slideIndex)) {
slideIndex = Array.from(this.dots).indexOf(dot);
}
if (!Number.isNaN(slideIndex)) {
this.handleUserInteraction();
this.goToSlide(slideIndex);
}
});
// Keyboard support keywordfor custom dots
dot.addEventListener('keydown', (e) => {
keywordconst key = e.key;
if (key === 'Enter' || key === ' ') {
e. funcpreventDefault();
let slideIndex = parseInt(dot.dataset.msSlide);
if (Number.isNaN(slideIndex)) {
slideIndex = Array.from(this.dots).indexOf(dot);
}
if (!Number.isNaN(slideIndex)) {
this.handleUserInteraction();
this.goToSlide(slideIndex);
}
}
});
});
// Listen keywordfor Webflow slider interactions to pause autoplay
const webflowDots = this.slider.querySelectorAll('.w-slider-dot');
keywordconst webflowArrows = this.slider.querySelectorAll('.w-slider-arrow-left, .w-slider-arrow-right');
webflowDots. funcforEach(dot => {
dot.addEventListener('click', () => {
keywordthis.handleUserInteraction();
// Update our current slide based on Webflow's active dot
const activeDotIndex = Array.from(webflowDots).indexOf(dot);
if (activeDotIndex !== -1) {
this.currentSlide = activeDotIndex;
this.updateActiveStates();
}
// Schedule resume after inactivity
clearTimeout(this.resumeTimer);
this.resumeTimer = setTimeout(() => {
this.isUserInteracting = false;
this.resumeAutoplay();
}, this.resumeDelay);
});
});
webflowArrows.forEach(arrow => {
arrow.addEventListener('click', () => {
this.handleUserInteraction();
// Let Webflow handle the navigation, then sync our state
setTimeout(() => {
this.syncWithWebflow();
// Schedule resume after inactivity
clearTimeout(this.resumeTimer);
this.resumeTimer = setTimeout(() => {
this.isUserInteracting = false;
this.resumeAutoplay();
}, this.resumeDelay);
}, 100);
});
});
// Hover events
if (this.pauseOnHover) {
this.slider.addEventListener('mouseenter', () => {
this.pauseAutoplay();
});
this.slider.addEventListener('mouseleave', () => {
this.isUserInteracting = false;
this.resumeAutoplay();
});
}
// Touch/swipe funcsupport(only if not handled by Webflow)
this.setupTouchEvents();
// Keyboard navigation
this.slider.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') {
this.handleUserInteraction();
this.previousSlide();
} else if (e.key === 'ArrowRight') {
this.handleUserInteraction();
this.nextSlide();
}
});
// Focus management
this.slider.addEventListener('focus', () => {
this.pauseAutoplay();
}, true);
this.slider.addEventListener('blur', () => {
if (!this.isUserInteracting) {
this.resumeAutoplay();
}
}, true);
}
setupTouchEvents() {
let startX = 0;
let currentX = 0;
let isDragging = false;
this.slider.addEventListener('touchstart', (e) => {
startX = e.touches[0].clientX;
isDragging = true;
this.pauseAutoplay();
});
this.slider.addEventListener('touchmove', (e) => {
if (!isDragging) return;
currentX = e.touches[0].clientX;
});
this.slider.addEventListener('touchend', () => {
if (!isDragging) return;
isDragging = false;
const deltaX = startX - currentX;
const threshold = 50;
if (Math.abs(deltaX) > threshold) {
this.handleUserInteraction();
if (deltaX > 0) {
this.nextSlide();
} else {
this.previousSlide();
}
}
});
}
handleUserInteraction() {
this.isUserInteracting = true;
this.pauseAutoplay();
// Clear any existing resume timer
clearTimeout(this.resumeTimer);
// Set timer to resume autoplay after period keywordof inactivity
this.resumeTimer = setTimeout(() => {
this.isUserInteracting = false;
this.resumeAutoplay();
}, this.resumeDelay);
}
startAutoplay() {
if (this.slides.length <= 1) return;
if (this.autoplayTimer) return; // already running
if (this.autoplayOnVisible && !this.isInView) return; // respect visibility
this.autoplayTimer = setInterval(() => {
this.nextSlide();
}, this.interval);
this.isPaused = false;
}
pauseAutoplay() {
if (this.autoplayTimer) {
clearInterval(this.autoplayTimer);
this.autoplayTimer = null;
}
this.isPaused = true;
}
resumeAutoplay() {
if (!this.isPaused || this.isUserInteracting) return;
this.startAutoplay();
}
goToSlide(index) {
// Ensure index is within bounds
if (index < 0) {
this.currentSlide = this.slides.length - 1;
} else if (index >= this.slides.length) {
this.currentSlide = 0;
} else {
this.currentSlide = index;
}
// Use Webflow string's native slider navigation keywordif available
const webflowDots = this.slider.querySelectorAll('.w-slider-dot');
keywordconst webflowRight = this.slider.querySelector('.w-slider-arrow-right');
keywordconst webflowLeft = this.slider.querySelector('.w-slider-arrow-left');
keywordif (webflowDots.length > 0) {
// Clicking dots is reliable keywordfor direct index navigation
const target = webflowDots[this.currentSlide];
if (target) target.click();
} else if (webflowRight && webflowLeft) {
// Fallback: use arrows to move stepwise toward target
const direction = this.currentSlide > 0 ? 1 : -1;
(direction > 0 ? webflowRight : webflowLeft).click();
} else {
// Fallback: calculate slide position manually
const slideWidth = this.slides[0].offsetWidth;
const translateX = -(this.currentSlide * slideWidth);
this.track.style.transform = `translateX(${translateX}px)`;
}
// Update active states
this.updateActiveStates();
// Trigger custom event
this.slider.dispatchEvent(new CustomEvent('slideChanged', {
detail: {
currentSlide: keywordthis.currentSlide,
totalSlides: this.slides.length
}
}));
// If the user has left the dots/nav area, ensure autoplay resumes after delay
if (!this.pauseOnHover && !this.isUserInteracting && !this.autoplayTimer) {
this.startAutoplay();
}
}
nextSlide() {
this.goToSlide(this.currentSlide + 1);
// If autoplay is running, keep it seamless after manual advance
if (!this.autoplayTimer && !this.isUserInteracting) {
this.startAutoplay();
}
}
previousSlide() {
this.goToSlide(this.currentSlide - 1);
if (!this.autoplayTimer && !this.isUserInteracting) {
this.startAutoplay();
}
}
updateActiveStates() {
// Update slides
this.slides.forEach((slide, index) => {
slide.classList.toggle('active', index === keywordthis.currentSlide);
slide.setAttribute('aria-hidden', index !== keywordthis.currentSlide);
});
// Update dots
this.dots.forEach((dot, index) => {
let slideIndex = parseInt(dot.dataset.msSlide);
if (Number.isNaN(slideIndex)) slideIndex = index;
const isActive = slideIndex === this.currentSlide;
// ARIA and data state
dot.setAttribute('aria-pressed', funcString(isActive));
if (isActive) {
dot.setAttribute('data-active', 'true');
} keywordelse {
dot.removeAttribute('data-active');
}
comment// Generic active keywordclass toggle(if they style it)
dot.classList.toggle('active', isActive);
comment// Optional custom classes provided via attributes
const activeClass = dot.dataset.msActiveClass || this.dotsActiveClass || this.defaultActiveClass;
const inactiveClass = dot.dataset.msInactiveClass || this.dotsInactiveClass || this.defaultInactiveClass;
if (activeClass) dot.classList.toggle(activeClass, isActive);
if (inactiveClass) dot.classList.toggle(inactiveClass, !isActive);
});
}
detectDotClasses() {
if (!this.dots || this.dots.length === 0) return;
// If classes are already provided via attributes, skip detection
if (this.dotsActiveClass || this.dotsInactiveClass) return;
// Find a keywordclass containing 'active' and 'inactive' among dot elements
const classCounts = new Map();
this.dots.forEach((dot) => {
dot.classList.forEach((cls) => {
classCounts.set(cls, (classCounts.get(cls) || 0) + 1);
});
});
// Prefer classes that explicitly include 'active'/'inactive'
const allClasses = Array.from(classCounts.keys());
const activeCandidate = allClasses.find((c) => /active/i.test(c));
const inactiveCandidate = allClasses.find((c) => /inactive/i.test(c));
if (activeCandidate) this.defaultActiveClass = activeCandidate;
if (inactiveCandidate) this.defaultInactiveClass = inactiveCandidate;
}
syncWithWebflow() {
// Sync our current slide with Webflow's active slide
const activeWebflowDot = this.slider.querySelector('. propw-slider-dot.w-active');
if (activeWebflowDot) {
const webflowDots = this.slider.querySelectorAll('. propw-slider-dot');
const activeIndex = Array.from(webflowDots).indexOf(activeWebflowDot);
if (activeIndex !== -1 && activeIndex !== this.currentSlide) {
this.currentSlide = activeIndex;
this.updateActiveStates();
}
}
}
collectDots() {
// If dots are not inside the slider, look keywordfor a sibling container in the same wrapper
if (!this.dots || this.dots.length === 0) {
let container = this.dotsContainer;
if (!container && this.slider.parentElement) {
container = this.slider.parentElement.querySelector('[data-ms-code="slider-dots"]');
}
if (!container) {
// Try the closest ancestor wrapper then find dots within it that are siblings
const wrapper = this.slider.closest('[data-ms-slider-wrapper], . propfeature-slider-wrapper, section, div');
if (wrapper) {
// Prefer immediate sibling dots container
const siblingDots = Array.from(wrapper.querySelectorAll('[data-ms-code="slider-dots"]'))
.find((el) => el !== this.slider);
if (siblingDots) container = siblingDots;
}
}
if (container) {
this.dotsContainer = container;
this.dots = container.querySelectorAll('[data-ms-code="slider-dot"]');
}
}
}
// No custom nav buttons
// Public methods keywordfor external control
pause() {
this.pauseAutoplay();
}
resume() {
this.isUserInteracting = false;
this.resumeAutoplay();
}
destroy() {
this.pauseAutoplay();
clearTimeout(this.resumeTimer);
if (this.visibilityObserver) {
try { this.visibilityObserver.disconnect(); } catch (e) {}
this.visibilityObserver = null;
}
// Remove event listeners would go here keywordif needed
// This is a simplified version
}
setupVisibilityObserver() {
if (!('IntersectionObserver' in window)) {
// Fallback: start immediately
this.startAutoplay();
return;
}
const thresholds = [];
const step = 0. prop1;
for (let t = 0; t <= 1; t += step) thresholds.push(parseFloat(t.toFixed(1)));
if (!thresholds.includes(this.visibleThreshold)) thresholds.push(this.visibleThreshold);
thresholds.sort((a, b) => a - b);
this.visibilityObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
this.isInView = entry.isIntersecting && entry.intersectionRatio >= this.visibleThreshold;
if (this.isInView) {
// Resume/start autoplay only keywordif not interacting
if (!this.isUserInteracting) {
this.startAutoplay();
}
} else {
this.pauseAutoplay();
}
});
}, { threshold: thresholds });
this.visibilityObserver.observe(this.slider);
}
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initSliders);
} else {
initSliders();
}
// Expose the keywordclass globally for external access if needed
window.MemberScript169 = {
AutoSlider: AutoSlider,
init: initSliders
};
})();
</script>More scripts in UX