#169 - Autoplay slider with an optional manual selection. v0.1
Add on scroll autoplay to your Webflow sliders with optional pause-on-hover, custom slider dots navigation.
<!-- 💙 MEMBERSCRIPT #169 v0.1 💙 - AUTOPLAY SLIDER WITH OPTIONAL MANUAL SELECTION -->
<script>
(function() {
'use strict';
// Wait for 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 !== 'false';
this.resumeDelay = parseInt(this.slider.dataset.msResumeDelay) || 3000;
this.autoplayOnVisible = this.slider.dataset.msAutoplayOnVisible === 'true';
this.visibleThreshold = Number.isNaN(parseFloat(this.slider.dataset.msVisibleThreshold))
? 0.3
: 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 default dot classes from markup if no attributes provided
this.detectDotClasses();
// Sync with Webflow's current state
this.syncWithWebflow();
// Bind event listeners
this.bindEvents();
// Start autoplay (optionally only when visible)
if (this.autoplayOnVisible) {
this.setupVisibilityObserver();
} else {
this.startAutoplay();
}
console.log('AutoSlider initialized with', this.slides.length, 'slides');
}
setupStyles() {
// No CSS modifications - work with existing Webflow slider styles
// Only add data attributes to slides for tracking
this.slides.forEach((slide, index) => {
slide.dataset.slideIndex = index;
});
// Improve accessibility for custom dots without altering styles
if (this.dots && this.dots.length) {
this.dots.forEach((dot, index) => {
if (!dot.hasAttribute('role')) dot.setAttribute('role', 'button');
if (!dot.hasAttribute('tabindex')) dot.setAttribute('tabindex', '0');
if (!dot.hasAttribute('aria-label')) dot.setAttribute('aria-label', `Show slide ${index + 1}`);
// If data-ms-slide is missing, infer from position
if (!dot.dataset.msSlide) dot.dataset.msSlide = String(index);
});
}
}
bindEvents() {
// No custom prev/next controls
// Custom dot navigation (data-ms-code dots). Use event delegation if container exists.
if (this.dotsContainer) {
this.dotsContainer.addEventListener('click', (e) => {
const dot = e.target.closest('[data-ms-code="slider-dot"]');
if (!dot || !this.dotsContainer.contains(dot)) return;
e.preventDefault();
let slideIndex = parseInt(dot.dataset.msSlide);
if (Number.isNaN(slideIndex)) {
// Recollect in case DOM changed
this.dots = this.dotsContainer.querySelectorAll('[data-ms-code="slider-dot"]');
slideIndex = Array.from(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.preventDefault();
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 for custom dots
dot.addEventListener('keydown', (e) => {
const key = e.key;
if (key === 'Enter' || key === ' ') {
e.preventDefault();
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 for Webflow slider interactions to pause autoplay
const webflowDots = this.slider.querySelectorAll('.w-slider-dot');
const webflowArrows = this.slider.querySelectorAll('.w-slider-arrow-left, .w-slider-arrow-right');
webflowDots.forEach(dot => {
dot.addEventListener('click', () => {
this.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 support (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 of 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's native slider navigation if available
const webflowDots = this.slider.querySelectorAll('.w-slider-dot');
const webflowRight = this.slider.querySelector('.w-slider-arrow-right');
const webflowLeft = this.slider.querySelector('.w-slider-arrow-left');
if (webflowDots.length > 0) {
// Clicking dots is reliable for 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: this.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 === this.currentSlide);
slide.setAttribute('aria-hidden', index !== this.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', String(isActive));
if (isActive) {
dot.setAttribute('data-active', 'true');
} else {
dot.removeAttribute('data-active');
}
// Generic active class toggle (if they style it)
dot.classList.toggle('active', isActive);
// 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 class 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('.w-slider-dot.w-active');
if (activeWebflowDot) {
const webflowDots = this.slider.querySelectorAll('.w-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 for 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], .feature-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 for 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 if needed
// This is a simplified version
}
setupVisibilityObserver() {
if (!('IntersectionObserver' in window)) {
// Fallback: start immediately
this.startAutoplay();
return;
}
const thresholds = [];
const step = 0.1;
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 if 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 class globally for external access if needed
window.MemberScript169 = {
AutoSlider: AutoSlider,
init: initSliders
};
})();
</script>
Creación del escenario Make.com
1. Descargue el proyecto JSON a continuación para empezar.
2. Navegue hasta Make.com y Cree un nuevo escenario...

3. Haga clic en el pequeño cuadro con 3 puntos y luego Importar Blueprint...

4. Sube tu archivo y ¡voilá! Ya está listo para vincular sus propias cuentas.
¿Necesitas ayuda con este MemberScript?
Todos los clientes de Memberstack pueden solicitar asistencia en el Slack 2.0. Tenga en cuenta que no se trata de funciones oficiales y que no se puede garantizar la asistencia.
Únete al Slack 2.0Autenticación y pagos para sitios Webflow
Añada inicios de sesión, suscripciones, contenido cerrado y mucho más a su sitio Webflow: fácil y totalmente personalizable.
.webp)
"Hemos estado utilizando Memberstack durante mucho tiempo, y nos ha ayudado a lograr cosas que nunca hubiéramos creído posible usando Webflow. Nos ha permitido construir plataformas con gran profundidad y funcionalidad y el equipo que hay detrás siempre ha sido súper servicial y receptivo a los comentarios"

"He estado construyendo un sitio de membresía con Memberstack y Jetboost para un cliente. Se siente como magia construir con estas herramientas. Como alguien que ha trabajado en una agencia donde algunas de estas aplicaciones fueron codificadas desde cero, finalmente entiendo el bombo ahora. Esto es mucho más rápido y mucho más barato."

"Uno de los mejores productos para iniciar un sitio de membresía - Me gusta la facilidad de uso de Memberstack. Yo era capaz de mi sitio de membresía en marcha y funcionando dentro de un día. No hay nada más fácil que eso. También proporciona la funcionalidad que necesito para hacer la experiencia del usuario más personalizada."

"Mi negocio no sería lo que es sin Memberstack. Si crees que 30 $/mes es caro, prueba a contratar a un desarrollador para que integre recomendaciones personalizadas en tu sitio por ese precio. Increíblemente flexible conjunto de herramientas para aquellos dispuestos a poner en algunos esfuerzos mínimos para ver su documentación bien elaborado."


"La comunidad de Slack es una de las más activas que he visto y los clientes están dispuestos a responder preguntas y ofrecer soluciones. He realizado evaluaciones en profundidad de herramientas alternativas y siempre volvemos a Memberstack: ahórrate el tiempo y dale una oportunidad"."

¿Necesitas ayuda con este MemberScript? ¡Únete a nuestra comunidad Slack!
Únete al Slack de la comunidad Memberstack y ¡pregunta! Espera una respuesta rápida de un miembro del equipo, un experto de Memberstack o un compañero de la comunidad.
Únete a nuestro Slack