Skip to content

Touch Scroll Fix - Scroll su Dispositivi Mobile

Il Problema

Quando si usa SheetRoot di Radix UI (o qualsiasi Dialog/Sheet basato su Radix UI Dialog), su dispositivi touch (smartphone e tablet) lo scroll verticale del contenuto interno non funziona correttamente.

Sintomi

  • ✅ Lo scroll funziona perfettamente su desktop (con mouse o trackpad)
  • ❌ Su mobile, lo scroll touch non funziona o è molto difficoltoso
  • ❌ Il contenuto lungo non è accessibile
  • ❌ L'utente non può navigare nel menu/sidebar mobile

Causa Tecnica

Radix UI Dialog/Sheet applica gestori di eventi touch sull'overlay (il backdrop scuro) per implementare la funzionalità "chiudi al click fuori".

Problema:

Utente tocca il contenuto scrollabile

Radix UI overlay intercetta l'evento touch

L'evento non raggiunge il contenitore scrollabile

❌ Scroll non funziona

Il problema è che i gestori touch dell'overlay interferiscono con lo scroll del contenuto interno, impedendo agli eventi touch di raggiungere il container scrollabile.

La Soluzione

Il fix richiede due proprietà CSS/JS applicate al contenitore scrollabile:

1. touchAction: 'pan-y'

typescript
style={{ touchAction: 'pan-y' }}

Cosa fa:

  • Forza il browser a permettere SOLO lo scroll verticale (pan-y)
  • Impedisce gesti orizzontali (swipe per chiudere)
  • Ha massima priorità se impostato come stile inline

Perché inline:

  • Garantisce massima priorità CSS
  • Compatibilità cross-browser ottimale
  • Non può essere sovrascritto da classi CSS

2. onTouchMove con stopPropagation()

typescript
onTouchMove={(e) => e.stopPropagation()}

Cosa fa:

  • Impedisce agli eventi touch di "salire" (bubble up) fino all'overlay
  • Blocca la propagazione, assicurando che il touch venga gestito SOLO dal contenitore scrollabile
  • Permette lo scroll verticale fluido su tutti i dispositivi touch

Meccanismo:

Utente tocca e scrolla il contenuto

onTouchMove intercetta l'evento

stopPropagation() blocca la propagazione

✅ L'overlay NON riceve l'evento

✅ Scroll funziona perfettamente

Implementazione

Pattern Standard

typescript
<div
    ref={scrollContainerRef}
    className="flex-1 overflow-y-scroll overscroll-contain p-4 [-webkit-overflow-scrolling:touch]"
    style={{ touchAction: 'pan-y' }}
    onTouchMove={(e) => e.stopPropagation()}
>
    {/* Contenuto scrollabile */}
</div>

Classi CSS importanti:

  • overflow-y-scroll - Abilita scroll verticale
  • overscroll-contain - Previene scroll chaining (iOS)
  • [-webkit-overflow-scrolling:touch] - Smooth scrolling su iOS

Dove È Applicato

1. Sidebar Mobile - SidebarContent

File: app/components/ui/sidebar.tsx:366-398

typescript
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
    /*
     * Fix per lo scroll touch su dispositivi mobile:
     *
     * 1. touchAction: 'pan-y' - Forza il browser a permettere SOLO lo scroll verticale.
     *    Questo previene che gesti orizzontali (come lo swipe per chiudere lo Sheet)
     *    interferiscano con lo scroll. Viene impostato come proprietà inline invece che
     *    classe CSS per garantire la massima priorità e compatibilità cross-browser.
     *
     * 2. onTouchMove con stopPropagation() - Impedisce agli eventi touch di "salire"
     *    (bubble up) fino all'overlay dello Sheet. Senza questo, l'overlay intercetta
     *    gli eventi touch e impedisce lo scroll del contenuto. stopPropagation() blocca
     *    la propagazione, assicurando che il touch venga gestito SOLO dal contenitore
     *    scrollabile, permettendo lo scroll verticale fluido su tutti i dispositivi touch.
     *
     * Questo fix è necessario perché Radix UI Dialog/Sheet (che è la base di Sheet)
     * applica pointer-events e gestori touch sull'overlay per implementare la chiusura
     * al click fuori, ma questo può interferire con lo scroll interno su touch devices.
     */
    return (
        <div
            data-slot="sidebar-content"
            data-sidebar="content"
            className={cn(
                "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
                className
            )}
            style={{ touchAction: 'pan-y' }}
            onTouchMove={(e) => e.stopPropagation()}
            {...props}
        />
    )
}

2. ModuleMenuMobile - Contenitore Scroll

File: app/components/layout/topbar/module-menu-mobile.tsx:150-167

typescript
<SheetRoot open={openMobile} onOpenChange={setOpenMobile}>
    <SheetContent
        side="left"
        className="w-[280px] p-0 flex flex-col overflow-hidden"
        hideCloseButton={true}
    >
        {/*
         * Fix per lo scroll touch su dispositivi mobile:
         *
         * 1. touchAction: 'pan-y' - Forza il browser a permettere SOLO lo scroll verticale.
         *    Questo previene che gesti orizzontali (come lo swipe per chiudere lo Sheet)
         *    interferiscano con lo scroll. Viene impostato come proprietà inline invece che
         *    classe CSS per garantire la massima priorità e compatibilità cross-browser.
         *
         * 2. onTouchMove con stopPropagation() - Impedisce agli eventi touch di "salire"
         *    (bubble up) fino all'overlay dello Sheet. Senza questo, l'overlay intercetta
         *    gli eventi touch e impedisce lo scroll del contenuto. stopPropagation() blocca
         *    la propagazione, assicurando che il touch venga gestito SOLO dal contenitore
         *    scrollabile, permettendo lo scroll verticale fluido su tutti i dispositivi touch.
         *
         * Questo fix è necessario perché Radix UI Dialog/Sheet (che è la base di Sheet)
         * applica pointer-events e gestori touch sull'overlay per implementare la chiusura
         * al click fuori, ma questo può interferire con lo scroll interno su touch devices.
         */}
        <div
            ref={scrollContainerRef}
            className="flex-1 overflow-y-scroll overscroll-contain p-4 [-webkit-overflow-scrolling:touch]"
            style={{ touchAction: 'pan-y' }}
            onTouchMove={(e) => e.stopPropagation()}
        >
            {renderMobileMenuItems(menuData)}
        </div>
    </SheetContent>
</SheetRoot>

Test

Setup Test

Dispositivi:

  • iPhone/iPad (Safari)
  • Android phone/tablet (Chrome)
  • Desktop con DevTools touch emulation

Procedura Test

  1. Apri il componente mobile

    • Sidebar mobile: Riduci finestra < 1024px, apri sidebar
    • ModuleMenuMobile: Riduci finestra < 1024px, clicca icona Menu
  2. Verifica contenuto scrollabile

    • Assicurati che il contenuto sia più lungo della viewport
    • Dovrebbero esserci almeno 10-15 voci nel menu
  3. Test scroll touch

    • Su dispositivo fisico: usa il dito per scrollare verticalmente
    • Su desktop: usa DevTools touch emulation
    • Verifica che lo scroll sia fluido e reattivo
  4. Test overlay click-outside

    • Clicca/tocca l'overlay scuro fuori dal contenuto
    • Verifica che il Sheet si chiuda correttamente
  5. Test swipe gesture (se abilitato)

    • Prova a swipare orizzontalmente
    • Verifica che lo swipe non interferisca con lo scroll verticale

Risultati Attesi

  • ✅ Scroll verticale funziona perfettamente su tutti i touch devices
  • ✅ Scroll è fluido e reattivo (nessun lag o blocco)
  • ✅ Click outside chiude correttamente il Sheet
  • ✅ Nessuna interferenza tra scroll verticale e gesti orizzontali

Troubleshooting

Scroll non funziona su iOS:

  • Verifica che -webkit-overflow-scrolling: touch sia applicato
  • Controlla che overscroll-contain sia presente

Scroll funziona ma è scattoso:

  • Aggiungi will-change: scroll-position al container
  • Verifica che non ci siano animazioni CSS pesanti durante lo scroll

Overlay non chiude il Sheet:

  • Verifica che stopPropagation() sia applicato SOLO a onTouchMove
  • NON applicare stopPropagation() a onClick o onTouchEnd

Compatibilità

Browser Supportati

BrowserVersioneSupportoNote
Safari iOS12+✅ FullRichiede -webkit-overflow-scrolling
Chrome Android80+✅ FullNativo
Firefox Android80+✅ FullNativo
Edge Mobile80+✅ FullNativo
Samsung Internet12+✅ FullNativo

CSS Properties

PropertySupportFallback
touch-action: pan-y✅ iOS 13+, Android 4.4+Nessun fallback necessario
-webkit-overflow-scrolling: touch✅ iOS 5-12Ignorato su iOS 13+ (momentum scroll è default)
overscroll-contain✅ Chrome 63+, Safari 16+Degrada gracefully

Alternative Considerate

❌ Solo touch-action: pan-y

Problema: Non sufficiente da solo

typescript
// Non funziona completamente
style={{ touchAction: 'pan-y' }}

L'overlay di Radix UI continua a intercettare gli eventi anche con touch-action.

❌ Solo stopPropagation()

Problema: Non previene gesti orizzontali

typescript
// Comportamento inconsistente
onTouchMove={(e) => e.stopPropagation()}

Senza touch-action: pan-y, lo swipe orizzontale può interferire.

preventDefault() su touch events

Problema: Rompe lo scroll nativo

typescript
// ❌ NON FARE - Disabilita completamente lo scroll
onTouchMove={(e) => {
    e.preventDefault();
    e.stopPropagation();
}}

preventDefault() disabilita lo scroll invece di facilitarlo.

✅ Soluzione Combinata (Scelta)

typescript
// ✅ Soluzione completa e funzionante
style={{ touchAction: 'pan-y' }}
onTouchMove={(e) => e.stopPropagation()}

Le due proprietà insieme risolvono completamente il problema.

Best Practices

DO ✅

  1. Applica entrambe le proprietà

    typescript
    style={{ touchAction: 'pan-y' }}
    onTouchMove={(e) => e.stopPropagation()}
  2. Usa stile inline per touchAction

    • Garantisce massima priorità
    • Non può essere sovrascritto da CSS esterni
  3. Aggiungi classi iOS-specific

    typescript
    className="[-webkit-overflow-scrolling:touch] overscroll-contain"
  4. Documenta il fix nel codice

    • Commenti inline spiegano il perché
    • Aiuta futuri sviluppatori a non rimuoverlo

DON'T ❌

  1. Non usare preventDefault()

    typescript
    // ❌ Disabilita lo scroll
    onTouchMove={(e) => e.preventDefault()}
  2. Non applicare il fix all'overlay

    typescript
    // ❌ Applicare al container scrollabile, NON all'overlay
    <SheetContent style={{ touchAction: 'pan-y' }}>
  3. Non omettere una delle due proprietà

    typescript
    // ❌ Incompleto - serve sia touchAction che stopPropagation
    style={{ touchAction: 'pan-y' }}
    // Manca onTouchMove!
  4. Non usare classi CSS per touchAction

    typescript
    // ❌ Può essere sovrascritto
    className="touch-pan-y"
    
    // ✅ Usa inline style
    style={{ touchAction: 'pan-y' }}

Debugging

Console Logs

Aggiungi log temporanei per debug:

typescript
onTouchMove={(e) => {
    console.log('[TouchScroll] Touch move event:', {
        target: e.target,
        propagationStopped: e.isPropagationStopped(),
    });
    e.stopPropagation();
}}

Chrome DevTools

  1. Apri DevTools → Performance tab
  2. Abilita "Screenshots" e "Memory"
  3. Start recording
  4. Prova lo scroll touch
  5. Stop recording
  6. Analizza:
    • Touch events nella timeline
    • Scroll performance
    • FPS durante lo scroll

Safari Web Inspector (iOS)

  1. Connetti iPhone/iPad al Mac
  2. Apri Safari → Develop → [Your Device]
  3. Inspect la pagina
  4. Nella console, verifica:
    javascript
    // Verifica computed style
    getComputedStyle(document.querySelector('[data-slot="sidebar-content"]')).touchAction
    // Dovrebbe essere: "pan-y"

Risorse Correlate

Documentazione Elerama Frontend