# Tailwind Touch Manipulation: touch-action für jedes Control

> Tailwind Touch Manipulation: welche touch-action-Utility Button, Slider, Swipe-Zeile und Sheet jeweils brauchen und welche Tap- und Zoom-Bugs sie beheben.

- Autor: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Veröffentlicht: 13. Okt. 2026
- Kategorie: [Mobile UI mit React](https://wingo-ui.com/de/blog/category/mobile-ui)
- Lesezeit: 9 Min.
- Kanonische URL: https://wingo-ui.com/de/blog/tailwind-touch-manipulation
- Englische Fassung: https://wingo-ui.com/blog/tailwind-touch-manipulation

## Kurz gesagt

Setz touch-manipulation auf alles, was angetippt wird, also Buttons, Links, Tabs und Stepper: Scrollen und Pinch-Zoom bleiben erhalten, der Doppeltipp-Zoom fällt weg, also zoomen schnelle Taps nie die Seite. Nutze touch-none nur auf Flächen, auf denen du jede Richtung selbst behandelst, etwa bei einem Slider oder einem Signature Pad, und touch-pan-y auf allem, was seitlich gewischt wird, damit ein vertikaler Swipe weiter scrollt. Der Preflight von Tailwind CSS v4 macht das Tap-Highlight bereits transparent, deshalb braucht jedes drückbare Element einen eigenen Pressed-State.

Tipp auf dem iPhone dreimal schnell auf einen Plus-Button, und die Seite zoomt hinein, statt bis drei zu zählen. Zieh den Griff eines Sliders, und die Seite scrollt mit. Wisch ein Karussell zur Seite, und der ganze Screen rutscht nach oben. Hinter jedem dieser Bugs steckt eine fehlende CSS-Eigenschaft, `touch-action`, und die Utilities für Tailwind Touch Manipulation setzen sie pro Element: `touch-manipulation`, `touch-none`, `touch-pan-y` und sieben weitere. Dieses Tutorial zeigt, was jeder Wert macht, welches Control welchen braucht und welche Tap-Highlight- und Zoom-Bugs sie beheben. Dazu kommen die Klassen, die wir in Button, Slider und Drawer von Wingo UI ausliefern.

Der Artikel gehört zu unserer Reihe [Mobile UI in React](https://wingo-ui.com/de/blog/category/mobile-ui). Der Leitfaden [Mobile-first React-Komponenten](https://wingo-ui.com/de/blog/mobile-first-react-components) stellt diese Regeln neben Touch Targets und Safe Areas.

## Was steuert touch-action eigentlich?

Welche Gesten der Browser selbst übernimmt, wenn ein Finger auf einem Element landet. Standardmäßig gehören Panning (Scrollen) und Pinch-Zoom dem Browser. Sobald er ein Panning startet, hören deine Pointer Events auf: Du bekommst ein `pointercancel` und kein weiteres `pointermove` ([MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/touch-action)). Deshalb funktioniert ein Drag auf Basis von Pointer Events mit der Maus und bricht auf dem Smartphone ab, sobald sich der Finger bewegt.

Drei Regeln aus der Spezifikation erklären die meisten Überraschungen:

- **Der Wert wird nicht vererbt, erreicht die Kinder aber trotzdem.** Beginnt eine Geste, bildet der Browser die Schnittmenge aus den `touch-action`-Werten des berührten Elements und seiner Vorfahren, bis zum nächsten Scroll-Container. Ein `touch-manipulation` auf einem Button gilt also auch für das Icon darin.
- **Er endet am Scroll-Container.** Werte oberhalb des scrollenden Elements gelten nicht für eine Berührung darin. Ein Sheet mit `touch-action: none` kann trotzdem eine Liste enthalten, die nativ scrollt. Das nutzen wir weiter unten beim Drawer.
- **Er wird beim Start der Geste gelesen.** Änderst du ihn mitten im Ziehen, wirkt sich das auf diese Geste nicht mehr aus.

## Welche Utilities für Tailwind touch-action gibt es?

Zehn, jede für einen Wert ([Tailwind-Doku](https://tailwindcss.com/docs/touch-action)):

| Klasse | `touch-action` | Einsatz |
|---|---|---|
| `touch-auto` | `auto` | der Standard; setzt einen Wert an einem Breakpoint zurück |
| `touch-manipulation` | `manipulation` | Buttons, Links, Tabs, Stepper, Toggles |
| `touch-none` | `none` | Slider, Signature Pads, Canvas, Karten |
| `touch-pan-y` | `pan-y` | alles, was seitlich gewischt wird: Zeilen, Karten, Karussells |
| `touch-pan-x` | `pan-x` | vertikal gezogene Elemente in einem horizontalen Scroller |
| `touch-pan-left`, `touch-pan-right` | `pan-left`, `pan-right` | horizontale Gesten in nur eine Richtung |
| `touch-pan-up`, `touch-pan-down` | `pan-up`, `pan-down` | vertikale Gesten in nur eine Richtung |
| `touch-pinch-zoom` | `pinch-zoom` | zusammen mit einem Pan-Wert, damit Zoom erhalten bleibt |

Die Pan- und Zoom-Klassen lassen sich kombinieren. In Tailwind CSS 4.3.3 (der Version, die wir im Oktober 2026 geprüft haben) setzt jede eine Variable wie `--tw-pan-y`, und eine einzige Deklaration liest sie alle: `touch-action: var(--tw-pan-x,) var(--tw-pan-y,) var(--tw-pinch-zoom,)`. Aus `touch-pan-y touch-pinch-zoom` wird also `pan-y pinch-zoom`, eine Fläche zum seitlichen Wischen, auf der man trotzdem zoomen kann. Varianten funktionieren wie gewohnt, zum Beispiel `touch-pan-y md:touch-auto`.

## Was behebt touch-manipulation auf Buttons?

Den Doppeltipp-Zoom. `manipulation` erlaubt Panning und Pinch-Zoom und schaltet den Rest ab, vor allem den Zoom per Doppeltipp. MDN beschreibt den Wert als Alias für `pan-x pan-y pinch-zoom` und merkt an, dass der Browser Klicks ohne Doppeltipp-Zoom nicht mehr verzögern muss.

Die 300ms-Verzögerung durch den Doppeltipp-Zoom ist größtenteils Geschichte. Chrome hat sie in Version 32 für Seiten mit `width=device-width` abgeschafft, Safari folgte mit iOS 9.3 ([Beitrag von Chrome](https://developer.chrome.com/blog/300ms-tap-delay-gone-away)). Der [Beitrag von WebKit zu schnellem Tippen](https://webkit.org/blog/5610/more-responsive-tapping-on-ios/) (Dezember 2015) ergänzt das Detail, das bis heute zählt: Der schnelle Weg gilt nur, solange die Seite auf ihrer anfänglichen Zoomstufe steht. Hat jemand hineingezoomt, wartet Safari wieder auf einen möglichen zweiten Tap. Mit `touch-action: manipulation` reagieren Taps auf jeder Zoomstufe sofort.

Der Bug, den du heute noch siehst, ist der Zoom selbst. Der Plus-Button eines Steppers, eine Taste im Taschenrechner oder ein zweimal hintereinander angetipptes „Mehr laden“ kann als Doppeltipp zählen und die Seite zoomen. Willst du den Zoom beim Doppeltippen verhindern, setz die Klasse auf das Control:

```tsx
"use client";

import { useState } from "react";
import { Plus } from "lucide-react";

export function GuestCount() {
  const [count, setCount] = useState(1);
  return (
    <div className="flex items-center gap-3">
      <span aria-live="polite">{count} Gäste</span>
      <button
        type="button"
        aria-label="Gast hinzufügen"
        onClick={() => setCount((n) => n + 1)}
        className="grid size-11 touch-manipulation select-none place-items-center rounded-full bg-foreground/5 active:bg-foreground/10"
      >
        <Plus aria-hidden className="size-5" />
      </button>
    </div>
  );
}
```

Setz sie auf Controls, nicht auf `html` oder `body`. Mit dem Doppeltipp-Zoom auf Text lesen manche Menschen Kleingedrucktes. Schaltest du ihn für die ganze Seite ab, nimmst du ihnen das, nur um ein Problem zu lösen, das allein deine Controls haben.

Der kostenlose [Button](https://wingo-ui.com/components/button) von Wingo UI trägt `touch-manipulation`, `select-none` und das Zurücksetzen des Tap-Highlights in seinen eigenen Basisklassen. Ein kopierter Button behält sie also in einem Projekt mit beliebigem globalem Stylesheet:

```tsx
import { Plus } from "lucide-react";
import { Button } from "@/components/ui/button";

export function AddGuest({ onAdd }: { onAdd: () => void }) {
  return (
    <Button variant="soft" size="icon" radius="full" aria-label="Gast hinzufügen" onClick={onAdd}>
      <Plus />
    </Button>
  );
}
```

> Live-Demo (Button): Tipp auf dem Smartphone auf den Button: Er gibt federnd nach und zeigt seinen Ladezustand, ohne seine Breite zu ändern, und schnelle wiederholte Taps zoomen nie die Seite. Probier sie unter [Button](https://wingo-ui.com/components/button) aus und installier sie mit `npx wingo-ui@latest add button`.

## Wie gehst du mit der Tap Highlight Color in Tailwind um?

Meist, indem du einen Pressed-State ergänzt, denn Tailwind hat das Highlight schon entfernt. `-webkit-tap-highlight-color` legt das halbtransparente Rechteck fest, das Safari und Chromium über einen angetippten Link oder Button zeichnen. Die Eigenschaft ist nicht standardisiert, wird vererbt und von WebKit und Blink unterstützt, nicht von Firefox ([MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-tap-highlight-color)).

Der Preflight von Tailwind CSS v4 setzt `-webkit-tap-highlight-color: transparent` auf `html` (in 4.3.3 erklärt der Kommentar daneben, dass das die Tap-Highlights auf iOS abschaltet). Weil die Eigenschaft vererbt wird, startet jedes Element in einer Tailwind-v4-App ohne Highlight. Das hat einen Preis, den viele übersehen: Ein einfaches `<a>` oder `<button>` ohne `active:`-Style zeigt bei einem Tap überhaupt nichts an.

In einer Tailwind-v4-App laufen die Regeln also andersherum:

- **Jedes drückbare Element bekommt einen Pressed-State.** `active:bg-foreground/10`, `active:scale-[0.97]` oder ein Spring-Press wie beim Button.
- **Komponenten, die du teilst, setzen den Wert selbst.** Code, der in ein Projekt ohne Preflight kopiert wird oder in eines, das die Eigenschaft zurücksetzt, blitzt wieder grau auf. Nutze in der Komponente die Arbitrary Property `[-webkit-tap-highlight-color:transparent]` oder definiere einmal eine Utility:

```css
/* app/globals.css */
@utility tap-highlight-none {
  -webkit-tap-highlight-color: transparent;
}
```

- **Entferne das Highlight nie ohne Ersatz.** Hat ein Control keinen Pressed-Style, ist ein sichtbares Tap-Highlight besser als gar nichts.

## Wann nutzt du touch-none in Tailwind?

Wenn dein Code auf dem Element jede Bewegungsrichtung behandelt und es dich nicht stört, dass ein Swipe, der dort beginnt, nie die Seite scrollt. Das passt zu einem Slider, einem Signature Pad, einem Zeichen-Canvas, einer Karte oder einem Crop-Handle. Für alles Große ist es die falsche Wahl: Ein bildschirmfüllendes Element mit `touch-none` hält Leute fest, die daran vorbeiscrollen wollen. MDN warnt außerdem, dass `none` auch den Pinch-Zoom blockiert, auf den Menschen mit Sehschwäche angewiesen sind.

Der [Slider](https://wingo-ui.com/components/slider) von Wingo UI zeigt, wie du das Element klein hältst. Nur das Root-Element hat `touch-none`, eine schmale Zeile, die bei Coarse Pointern (Touch) per Pseudo-Element eine Trefferfläche von 44px bekommt. Ein Ziehen darauf scrollt nie die Seite, während der Rest des Formulars normal scrollt:

```tsx
"use client";

import { useState } from "react";
import { Slider } from "@/components/ui/slider";

export function PriceFilter() {
  const [price, setPrice] = useState([500, 2000]);
  return (
    <Slider
      label="Preis"
      value={price}
      onValueChange={setPrice}
      min={0}
      max={5000}
      step={50}
      locale="en-US"
      format={{ style: "currency", currency: "USD", maximumFractionDigits: 0 }}
      showValueLabel
    />
  );
}
```

> Live-Demo (Slider): Zieh auf dem Smartphone einen Griff: Die Seite bleibt stehen, während die Wertanzeige deinem Finger folgt. Wisch dann neben dem Slider nach oben, und die Seite scrollt. Probier sie unter [Slider](https://wingo-ui.com/components/slider) aus und installier sie mit `npx wingo-ui@latest add slider`.

Die Griffe (Thumbs) setzen zusätzlich `touch-manipulation`, damit auch ein schneller Doppeltipp auf einen Griff nie zoomt.

## Wann nutzt du touch-pan-y oder touch-pan-x?

Wenn sich dein Element auf einer Achse bewegt und die Seite auf der anderen scrollt. Eine Zeile, die du zum Löschen nach links wischst, eine Karte, die du wegwischst, oder ein Slide im Karussell bekommen alle `touch-pan-y`: Der Browser behält das vertikale Scrollen, und eine horizontale Bewegung geht an deine Pointer-Handler.

```tsx
"use client";

import { useRef, type PointerEvent, type ReactNode } from "react";

export function SwipeRow({ children, onDelete }: { children: ReactNode; onDelete: () => void }) {
  const startX = useRef<number | null>(null);
  const offset = useRef(0);

  function down(event: PointerEvent<HTMLLIElement>) {
    startX.current = event.clientX;
    event.currentTarget.setPointerCapture(event.pointerId);
  }
  function move(event: PointerEvent<HTMLLIElement>) {
    if (startX.current === null) return;
    offset.current = Math.min(0, event.clientX - startX.current);
    event.currentTarget.style.transform = `translateX(${offset.current}px)`;
  }
  function end(event: PointerEvent<HTMLLIElement>) {
    if (offset.current < -120) onDelete();
    startX.current = null;
    offset.current = 0;
    event.currentTarget.style.transform = "";
  }

  return (
    <li
      onPointerDown={down}
      onPointerMove={move}
      onPointerUp={end}
      onPointerCancel={end}
      className="touch-pan-y select-none"
    >
      {children}
    </li>
  );
}
```

Mit eigenen Pointer-Handlern wie diesen ist die Klasse Pflicht. Ohne sie behandelt der Browser die Berührung als mögliches Scrollen, und eine Bewegung mit auch nur leichter vertikaler Abweichung endet in `pointercancel`. `setPointerCapture` ist die andere Hälfte: Die Zeile bekommt weiter Bewegungen, auch wenn der Finger von ihr abrutscht. Die Handler schreiben das `transform` direkt ins Element, statt bei jeder Bewegung `setState` aufzurufen. Motion erledigt das für dich. Sein `drag="x"` schreibt `touch-action: pan-y` als Inline-Style (`pan-x` bei `drag="y"`, `none` bei `drag`), und ein Inline-Style gewinnt gegen jede Klasse, die du ergänzt. Unsere Tutorials [React Swipe to Delete](https://wingo-ui.com/de/blog/react-swipe-to-delete) und [React Swipe Cards](https://wingo-ui.com/de/blog/react-swipe-cards) bauen beide Muster.

`touch-pan-x` ist der gespiegelte und seltenere Fall: ein vertikales Ziehen, etwa an einem Handle zum Umsortieren, in einer Zeile, die seitlich scrollt. Für sortierbare Listen vergleicht das [Tutorial zu dnd kit auf dem Smartphone](https://wingo-ui.com/de/blog/dnd-kit-mobile-touch-drag) die Kombinationen aus `touch-action` und Sensoren, mit denen ein Long Press zieht und ein Swipe trotzdem scrollt.

## Wie lassen sich Sheets gleichzeitig ziehen und scrollen?

Indem du dich auf die Regel verlässt, dass `touch-action` am Scroll-Container endet. vaul, die Bibliothek hinter dem kostenlosen [Drawer](https://wingo-ui.com/components/drawer), setzt `touch-action: none` auf das ganze Sheet. Ein Ziehen irgendwo auf Header oder Footer bewegt also das Sheet. Der Body des Drawers ist ein eigener Scroll-Container (`overflow-y-auto overscroll-contain`). Für eine Berührung im Body zählen deshalb nur die Werte bis zum Body selbst, und die Liste darin scrollt nativ. vaul bewegt das Sheet erst, wenn die Liste wieder ganz oben ist.

> Live-Demo (Drawer): Tipp auf „Filters“, scroll die Liste im Sheet und zieh dann vom Header nach unten: Die Liste scrollt nativ, und der Header zieht das Sheet. Probier sie unter [Drawer](https://wingo-ui.com/components/drawer) aus und installier sie mit `npx wingo-ui@latest add drawer`.

`overscroll-contain` übernimmt die andere Hälfte: Erreicht die Liste ihr Ende, setzt sich das Scrollen nicht auf die Seite hinter dem Sheet fort. `touch-action` steuert dieses Scroll Chaining gar nicht, das macht `overscroll-behavior`. Für ein Sheet, dessen Inhalt eigene horizontale Gesten hat, etwa ein Karussell oder eine Karte, gibst du `handleOnly` mit, damit es sich nur am Handle ziehen lässt. Der [Guide zum React Bottom Sheet](https://wingo-ui.com/de/blog/react-bottom-sheet-for-the-web) (auf Englisch) behandelt Snap Points und die Tastatur.

## Welche Klasse braucht welches Control?

| Control | Klassen |
|---|---|
| Button, Link, Tab, Toggle, Menüeintrag | `touch-manipulation select-none` und ein `active:`-Style |
| Stepper oder jeder Button, der wiederholt angetippt wird | `touch-manipulation`, hier Pflicht |
| Slider, Signature Pad, Canvas, Crop-Handle | `touch-none` auf dem kleinsten Element, das funktioniert |
| Zeile, Karte oder Slide, die seitlich gewischt wird | `touch-pan-y` (oder `drag="x"` von Motion) |
| Vertikales Ziehen in einem horizontalen Scroller | `touch-pan-x` |
| Scroller in einem Sheet oder Dialog | `touch-action` unverändert lassen, `overscroll-contain` ergänzen |
| `html` oder `body` | nichts; hier nie `touch-none` oder `touch-manipulation` |

## Wie testest du touch-action?

Lies zuerst den berechneten Wert: Im Web Inspector von Safari oder in den Chrome DevTools zeigt der Tab Computed den `touch-action`-Wert des Elements. Der Wert der Eltern zählt ebenfalls, bis zum nächsten Scroller. Der Device Mode von Chrome emuliert Touch. So prüfst du, dass ein Ziehen auf einer Fläche mit `touch-none` nicht scrollt. Doppeltipp-Zoom und Tap-Highlights brauchen ein echtes Smartphone: Tipp deinen Stepper auf einem iPhone und einem Android-Smartphone schnell hintereinander an, zoom in die Seite hinein und tipp noch einmal.

In Playwright kannst du in einem Context mit `hasTouch: true` `page.touchscreen.tap()` aufrufen. Das reicht, um zu prüfen, dass Klicks feuern. Die Gestenerkennung des Browsers bildet es aber nicht nach, also teste das Zoomverhalten weiter auf einem echten Gerät.

## Wo fängst du am besten an?

Durchsuch deine Codebase nach `onClick` auf Elementen ohne `touch-manipulation` und nach Drag-Handlern ohne `touch-*`-Klasse. Diese beiden Listen sind meist schon der ganze Fix. Prüf dann, ob jedes drückbare Element einen Pressed-State hat, denn Tailwind v4 hat das Tap-Highlight bereits ausgeblendet. Wenn du lieber mit Komponenten startest, die diese Klassen schon tragen: Button und Drawer sind kostenlos, `npx wingo-ui@latest add button drawer`. Der Slider gehört zu [Wingo UI Pro](https://wingo-ui.com/de/pricing).

## Komponenten in diesem Artikel

- [Button](https://wingo-ui.com/components/button) (Free): The React button component every screen starts with: five variants, six tones, all sizes, and a loading state that never jumps. Installation: `npx wingo-ui@latest add button`
- [Slider](https://wingo-ui.com/components/slider) (Pro): A React slider for a number or a range: any number of thumbs, labelled marks, a value bubble, vertical mode, tones and finger-sized thumbs. Installation: `npx wingo-ui@latest add slider`
- [Drawer](https://wingo-ui.com/components/drawer) (Free): A React drawer and bottom sheet built on vaul: drag to dismiss, snap points, nesting, and a side panel or dialog on desktop. Installation: `npx wingo-ui@latest add drawer`

## Häufige Fragen

### Was macht touch-manipulation in Tailwind?

Die Klasse setzt touch-action: manipulation. Der Browser darf scrollen und per Pinch zoomen, andere Gesten wie der Doppeltipp-Zoom sind aus. Ohne Doppeltipp-Zoom muss der Browser nicht auf einen zweiten Tap warten, Taps auf dem Element feuern also sofort.

### Gibt es auf dem Smartphone noch die 300ms-Verzögerung beim Tippen?

Nicht auf Seiten mit width=device-width auf der anfänglichen Zoomstufe: Chrome hat sie in Version 32 entfernt, Safari mit iOS 9.3. WebKit hat dokumentiert, dass der schnelle Tap nur auf der anfänglichen Zoomstufe greift, und schnelle Taps können weiterhin den Doppeltipp-Zoom auslösen. Genau den schaltet touch-manipulation pro Element ab.

### Wann nutze ich touch-none in Tailwind?

Nur auf Elementen, auf denen dein Code jede Bewegungsrichtung behandelt, etwa ein Slider, ein Signature Pad, ein Canvas oder eine Karte. Ein Swipe, der dort beginnt, scrollt oder zoomt die Seite nie. Halte solche Elemente also klein oder lass der Seite einen anderen Bereich zum Scrollen.

### Wie entferne ich die Tap Highlight Color in Tailwind?

In Tailwind CSS v4 musst du das meist nicht: Der Preflight setzt -webkit-tap-highlight-color: transparent auf html, und die Eigenschaft wird vererbt. Ohne Preflight nimmst du die Arbitrary Property [-webkit-tap-highlight-color:transparent] und ergänzt immer einen active- oder Pressed-Style, damit ein Tap sichtbar bleibt.

### Kann ich touch-pan-y und touch-pinch-zoom in Tailwind kombinieren?

Ja. In Tailwind CSS v4 schreiben die Pan- und Pinch-Utilities eigene CSS-Variablen, die eine einzige touch-action-Deklaration liest. touch-pan-y touch-pinch-zoom wird also zu touch-action: pan-y pinch-zoom.

---

Quelle: https://wingo-ui.com/de/blog/tailwind-touch-manipulation
