22 settembre 2026
Swipe to remove sul web moderno: un’implementazione fluida con Scroll Snap, Intersection Observer e Web Animations
Come costruire un gesto “swipe per rimuovere” stabile, performante e cross‑browser senza finire in codice fragile e animazioni a scatti.
Implementare lo swipe-to-remove sul web può diventare rapidamente un groviglio di gesture, calcoli e animazioni poco fluide. In questo articolo vediamo un approccio moderno basato su tecnologie native: CSS Scroll Snap per la meccanica del gesto, Intersection Observer per determinare la soglia di “rimozione”, e Web Animations per transizioni coerenti e performanti. Con note pratiche su compatibilità e fallback.
Costruire un gesto di swipe per rimuovere (tipico delle app mobile) in un’interfaccia web è uno di quei compiti che sembrano semplici finché non ci sbatti contro: gestione del touch, soglie, inertia, conflitti con lo scroll, animazioni che “saltano” e mille edge case.
Un approccio più solido consiste nel smettere di “simulare” la fisica a mano e appoggiarsi il più possibile a primitive native della piattaforma. Una combinazione particolarmente efficace è:
- CSS Scroll Snap: per rendere naturale il gesto di trascinamento e l’aggancio a posizioni predefinite.
- Intersection Observer: per capire in modo affidabile quando l’utente ha “superato la soglia” di rimozione.
- Web Animations API: per animare in modo fluido la rimozione (collasso, fade, slide out) senza layout thrashing.
Di seguito un modello mentale e una struttura che puoi adattare a liste, inbox, carrelli, notifiche.
1) Il pattern: uno “snap” tra stato normale e stato rimuovi
L’idea è costruire ogni riga come un piccolo contenitore scrollabile orizzontalmente con due “pagine”:
- Stato normale: la card/item principale.
- Stato azione: area a destra (o sinistra) che rappresenta l’azione di rimozione.
Con scroll-snap l’utente trascina, il browser gestisce inerzia e decelerazione, e l’item “aggancia” in modo prevedibile.
Struttura HTML indicativa
<li class="row">
<div class="swipe">
<div class="pane pane--content">
<!-- contenuto dell’item -->
<h3>Messaggio</h3>
<p>Anteprima...</p>
</div>
<button class="pane pane--remove" type="button" aria-label="Rimuovi">
Rimuovi
</button>
</div>
</li>
CSS: scroll-snap “a pagine”
.row {
list-style: none;
}
.swipe {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 100%;
overflow-x: auto;
overscroll-behavior-x: contain;
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.swipe::-webkit-scrollbar { display: none; }
.pane {
scroll-snap-align: start;
}
.pane--remove {
display: grid;
place-items: center;
background: #d92c2c;
color: white;
border: 0;
}
Con questa base ottieni già una gesture utilizzabile: trascinamento orizzontale, snapping e un’area “Rimuovi” raggiungibile.
2) La soglia di rimozione con Intersection Observer
Il passo successivo è determinare quando considerare l’azione “confermata”. Puoi farlo in due modi tipici:
- Swipe fino allo snap dell’area remove → rimuovi automaticamente.
- Swipe parziale → mostra l’azione e richiedi un tap su “Rimuovi”.
Per evitare calcoli manuali basati su scrollLeft e dimensioni (fragili con font, zoom, RTL, layout responsive), puoi usare un sentinel osservato.
Esempio: inserisci un piccolo elemento “marker” all’inizio della seconda pagina (remove) e osserva quando entra sufficientemente in view.
<div class="pane pane--remove">
<span class="sentinel" aria-hidden="true"></span>
Rimuovi
</div>
.sentinel {
width: 1px;
height: 1px;
}
const swipeEls = document.querySelectorAll('.swipe');
swipeEls.forEach((swipeEl) => {
const sentinel = swipeEl.querySelector('.pane--remove .sentinel');
if (!sentinel) return;
const io = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
// entry.isIntersecting + threshold gestiscono la “soglia”
if (entry.isIntersecting && entry.intersectionRatio >= 0.6) {
swipeEl.dispatchEvent(new CustomEvent('swipe:readyToRemove', { bubbles: true }));
}
}
},
{
root: swipeEl,
threshold: [0.6],
}
);
io.observe(sentinel);
});
Questa soluzione è robusta perché:
- non dipende da frame-by-frame JS durante il drag;
- delega al browser la parte complessa (scroll + inertia);
- rende semplice cambiare la soglia (es. 0.5, 0.7) senza rifare matematica.
3) Rimozione fluida con Web Animations API (senza reflow a raffica)
Quando decidi di rimuovere l’item, l’effetto “giusto” non è solo farlo sparire, ma:
- dare feedback (fade/slide);
- collassare lo spazio nella lista in modo elegante.
Una sequenza comune:
- anima l’opacità e un leggero translate;
- poi anima l’altezza da
offsetHeighta0.
document.addEventListener('swipe:readyToRemove', async (e) => {
const swipeEl = e.target.closest('.swipe');
const row = e.target.closest('.row');
if (!row || !swipeEl) return;
// evita doppi trigger
if (row.dataset.removing === 'true') return;
row.dataset.removing = 'true';
// 1) feedback visivo
await row.animate(
[
{ opacity: 1, transform: 'translateX(0px)' },
{ opacity: 0, transform: 'translateX(-8px)' }
],
{ duration: 160, easing: 'ease-out', fill: 'forwards' }
).finished;
// 2) collasso
const h = row.offsetHeight;
row.style.height = `${h}px`;
row.style.overflow = 'clip';
await row.animate(
[ { height: `${h}px` }, { height: '0px' } ],
{ duration: 180, easing: 'ease-in', fill: 'forwards' }
).finished;
row.remove();
});
Nota: per accessibilità e controllo, spesso conviene non rimuovere automaticamente al primo superamento soglia, ma richiedere conferma (tap su bottone) o offrire undo. Il pattern sopra è un buon “motore” su cui costruire queste scelte di UX.
4) Compatibilità e fallback: cosa fare quando manca una feature
Le primitive citate sono in gran parte ben supportate. Dove tipicamente sorgono problemi:
- comportamenti specifici legati allo scroll (es. particolari proprietà CSS meno diffuse);
- differenze di feeling tra browser su inerzia e snapping.
Strategia pratica:
- Progressive enhancement: se
scroll-snap-typenon è disponibile, mantieni un’interazione base (tap su “Rimuovi”). - Feature detection in CSS e JS:
- CSS:
@supports (scroll-snap-type: x mandatory) { ... } - JS: controlli su API (
'IntersectionObserver' in window,'animate' in Element.prototype)
- CSS:
- Alternative semplici: se manca Web Animations, passa a transizioni CSS; se manca Intersection Observer, degrada a controllo
scrollLeftsuscrollend(o un debounce suscroll).
L’obiettivo non è inseguire la perfezione identica ovunque, ma garantire:
- gesto naturale dove supportato;
- comportamento coerente (niente stati bloccati);
- fallback funzionale.
Sintesi: meno “gesture logic”, più piattaforma
Uno swipe-to-remove ben fatto non nasce da decine di listener touch e calcoli per-frame. La via moderna è comporre primitive native:
- Scroll Snap per rendere lo swipe prevedibile e fisico “gratis”;
- Intersection Observer per decidere soglie e stati in modo affidabile;
- Web Animations per una rimozione fluida senza scatti.
Il risultato è un’interazione più stabile, più semplice da mantenere e più facile da far evolvere (undo, multi-azioni, layout diversi) senza trasformare ogni lista in un mini motore di gesture.