# React Wheel Picker: iOS-Räder für Uhrzeit und Datum

> So baust du einen React Wheel Picker auf einem nativen Snap-Scroller: iOS-Schwung, Zeilen auf einem 3D-Zylinder, Sheet am Handy, Textfeld am Desktop, Tastatur.

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

## Kurz gesagt

Ein React Wheel Picker, der sich nativ anfühlt, überlässt das Scrollen dem Browser: Eine Scroll-Snap-Liste bringt den Schwung und den Gummiband-Effekt des Handys selbst mit, jede Zeile wird in einem Animation Frame aus scrollTop auf einem 3D-Zylinder gedreht, und der Wert wird genau einmal übernommen, wenn das Rad stillsteht. Nimm Räder für Uhrzeiten, Dauern und Geburtsdaten auf dem Handy, behalte am Desktop ein Textfeld und gib Maus-Drag, Mausrad und Tastatur jeweils einen eigenen Weg. Picker Wheel und Time Picker von Wingo UI Pro liefern all das mit, unter 768px mit einem Bottom Sheet.

Du willst einen React Wheel Picker, weil niemand auf dem Handy 14:30 auf einer Tastatur tippen sollte, die das halbe Formular verdeckt. Einen Kalender 36 Jahre zurückzublättern, bis zum Geburtsdatum, ist noch schlimmer. Schwierig ist das Gefühl beim Drehen: Die React-Wheel-Pakete, die wir uns angesehen haben, ersetzen das Scrollen des Browsers durch ihre eigene Touch-Physik. Dieser Guide baut das Rad auf einem nativen Snap-Scroller, dessen Zeilen auf einem 3D-Zylinder gedreht werden. Danach geht es um Maus und Tastatur, um die Uhrzeit- und Datumsfelder, die darauf aufbauen, und um die Stolperfallen. Wie sich Wheel Picker zwischen Bibliotheken unterscheiden, steht in unserem Vergleich der [besten React-Komponentenbibliothek fürs Handy](https://wingo-ui.com/de/blog/best-react-component-library-for-mobile).

## Wann ist ein Wheel Picker das richtige Control?

Wenn die Liste mittellang bis lang und geordnet ist und man erraten kann, was verdeckt ist: Stunden, Minuten, Dauern, Mengen, ein Geburtsjahr. Apples [Human Interface Guidelines zu Pickern](https://developer.apple.com/design/human-interface-guidelines/pickers) empfehlen einen Picker für mittellange bis lange Listen, einen Pull-down-Button für eine recht kurze Auswahl und eine Liste oder Tabelle für sehr große Mengen. Außerdem raten sie zu Minutenschritten, die 60 glatt teilen, etwa Viertelstunden.

Im Web ergibt das vier Regeln:

- Zwei bis fünf Optionen: ein [Segmented Control](https://wingo-ui.com/components/segmented-control), damit jede Option sichtbar ist.
- 12 Stunden, 60 Minuten, 1 bis 99 Stück, die Jahre seit 1920: ein Rad.
- Hunderte ungeordnete Namen (Länder, Produkte): ein durchsuchbares [Select](https://wingo-ui.com/components/select). Ein Rad springt nur zu einem getippten Anfang.
- Ein Datum nah an heute (eine Lieferung, eine Buchung): ein Kalender, weil der Wochentag zählt.

## Warum ein Rad auf einem nativen Snap-Scroller bauen?

Weil sich Wischen, Schwung und der Gummiband-Effekt an den Enden dann genau wie auf dem Rest des Handys anfühlen, und ein Trackpad oder Mausrad das Rad ohne zusätzlichen Code dreht. Eine Suche nach „ios picker web“ im Oktober 2026 mischte ein Mendix-Widget, React-Native-Pakete und Flutters CupertinoPicker unter die Web-Antworten. Die beiden React-DOM-Pakete, die wir gefunden haben, bringen jeweils ihre eigene Physik mit:

| Paket (Stand Oktober 2026) | Lizenz, letztes Release | Wie es sich bewegt |
| --- | --- | --- |
| [@ncdai/react-wheel-picker](https://react-wheel-picker.chanhdai.com/docs/getting-started) 1.2.3 | MIT, 23. August 2026 | eigene Touch-, Maus- und Wheel-Handler; Zeilen auf einem 3D-Zylinder mit CSS-Transforms |
| [react-mobile-picker](https://www.npmjs.com/package/react-mobile-picker) 1.2.0 | MIT, 3. August 2025 | eigener `touchmove`-Handler und `translate3d`; Mausrad standardmäßig aus |

Beide funktionieren. @ncdai/react-wheel-picker, das auch Pfeiltasten, Home / End und Type-ahead beherrscht, ist die bessere Wahl, wenn du ein kostenloses, ungestyltes Rad willst und das Feld selbst baust. Das Scrollgefühl stellst du dort über `dragSensitivity` und `scrollSensitivity` ein; ein nativer Scroller bremst so ab wie jede andere Liste auf diesem Handy.

Das native Rezept hat drei Teile. Die Liste scrollt mit `scroll-snap-type: y mandatory`, jede Zeile rastet in der Mitte ein, und zwei Zeilen Padding lassen die erste und die letzte Zeile die Mitte erreichen. Ein `requestAnimationFrame` pro Scroll liest `scrollTop` und schreibt `rotateX` und Opacity direkt auf die Zeilen, nie über React-State. Und der Wert wird einmal übernommen, im Stillstand: bei `scrollend`, das laut [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollend_event) ab Chrome 114, Firefox 109 und Safari 26.2 verfügbar ist, mit einem Idle-Timer von 120ms für ältere Safari-Versionen.

```tsx
"use client";

import { useEffect, useRef } from "react";

const ROW = 44; // fünf Zeilen in einem 220px-Fenster

export function MiniWheel({ options, onSettle }: { options: string[]; onSettle: (value: string) => void }) {
  const listRef = useRef<HTMLDivElement>(null);
  const settleRef = useRef(onSettle);
  useEffect(() => {
    settleRef.current = onSettle;
  });

  useEffect(() => {
    const list = listRef.current;
    if (!list) return;
    const rows = Array.from(list.firstElementChild?.children ?? []) as HTMLElement[];
    let frame = 0;
    let idle: ReturnType<typeof setTimeout> | undefined;
    let last = "";

    // Drehung und Ausblenden gehen direkt auf die Zeilen, nie über React-State
    const paint = () => {
      frame = 0;
      const pos = list.scrollTop / ROW;
      rows.forEach((row, i) => {
        const d = i - pos;
        row.style.transform = `perspective(400px) rotateX(${Math.max(-55, Math.min(55, -d * 20))}deg)`;
        row.style.opacity = String(Math.max(0.35, 1 - Math.min(Math.abs(d), 3) * 0.22));
      });
    };
    // ein Commit, wenn das Rad stillsteht, nie einer pro durchlaufener Zeile
    const settle = () => {
      clearTimeout(idle);
      const value = options[Math.round(list.scrollTop / ROW)];
      if (value !== undefined && value !== last) settleRef.current((last = value));
    };
    const onScroll = () => {
      if (!frame) frame = requestAnimationFrame(paint);
      clearTimeout(idle);
      idle = setTimeout(settle, 120); // Safari vor 26.2 hat kein scrollend
    };

    paint();
    list.addEventListener("scroll", onScroll, { passive: true });
    list.addEventListener("scrollend", settle);
    return () => {
      list.removeEventListener("scroll", onScroll);
      list.removeEventListener("scrollend", settle);
      cancelAnimationFrame(frame);
      clearTimeout(idle);
    };
  }, [options]);

  return (
    <div ref={listRef} className="h-[220px] snap-y snap-mandatory overflow-y-scroll overscroll-contain [scrollbar-width:none]">
      <div className="py-[88px]">
        {options.map((option) => (
          <div key={option} className="flex h-11 snap-center items-center justify-center text-xl tabular-nums">
            {option}
          </div>
        ))}
      </div>
    </div>
  );
}
```

Das ist der Kern des [Picker Wheel](https://wingo-ui.com/components/picker-wheel) von Wingo UI. Der Skizze fehlen ein Startwert, deaktivierte Zeilen, das Endlosdrehen, jede Eingabe außer dem Finger und Räder, die unsichtbar gemountet werden.

## Wie drehen Maus- und Tastaturnutzer das Rad?

Mausrad und Trackpad scrollen es nativ; Ziehen und Tasten brauchen eigenen Code. Im Picker Wheel läuft nur die Maus über Pointer Events: Ab 8 Pixeln Bewegung schaltet sich das Einrasten ab, und `scrollTop` folgt dem Zeiger. Beim Loslassen setzt eine Inertia-Animation von `motion` die gemessene Geschwindigkeit fort, und ihr `modifyTarget` rundet den Landepunkt auf eine Zeile. Ein Klick auf eine sichtbare Zeile lässt sie in die Mitte gleiten.

Jede Spalte ist ein Tab-Stopp mit `role="listbox"`. ArrowUp und ArrowDown drehen das Rad um eine Zeile, PageUp und PageDown um fünf, Home und End springen an die Enden, und Tippen springt zur nächsten Option, die mit dem Text beginnt („19“, dann „199“ auf einem Jahresrad). ArrowLeft und ArrowRight wechseln die Spalte. `aria-activedescendant` zeigt auf die zentrierte Option, die `aria-selected` trägt. Der Fokusring liegt eng um die mittlere Zeile, wo das Ausblenden am Rand ihn nicht verdecken kann.

## Welche Props braucht das Picker Wheel?

`columns` (oder `options` für ein einzelnes Rad), einen Wert und `onValueChange`. Alles andere hat einen Default:

```tsx
"use client";

import { useState } from "react";
import { PickerWheel, pickerRange } from "@/components/ui/picker-wheel";

const hours = pickerRange(0, 12);
const minutes = pickerRange(0, 59, { pad: 2 });

export function CallDuration() {
  const [duration, setDuration] = useState(["1", "25"]);
  return (
    <PickerWheel
      aria-label="Anrufdauer"
      value={duration}
      // einmal pro Stillstand, mit den Werten aller Spalten und der id der Spalte, die sich bewegt hat
      onValueChange={(value) => setDuration(value)}
      columns={[
        { id: "hours", label: "h", options: hours, align: "end" },
        { id: "minutes", label: "min", options: minutes, align: "start", loop: true },
      ]}
    />
  );
}
```

Die Props, die du kennen solltest:

- `onValueChange` feuert einmal pro Stillstand, nie pro durchlaufener Zeile. Du kannst dort also gefahrlos speichern.
- `loop` (auf dem Rad oder pro Spalte) lässt 59 wieder zu 00 springen: Die Spalte rendert zusätzliche Kopien und springt im Stillstand unbemerkt zur mittleren. Nur die mittlere Kopie hat `role="option"`.
- `size` setzt die Zeilenhöhe auf 36, 44 oder 52px; `visibleRows` (ungerade, Default 5) macht das Rad bei `md` 220px hoch; `perspective` kippt jede Zeile um 20, 10 oder 0 Grad (`ios`, `subtle`, `flat`).
- Eine deaktivierte Option ist durchgestrichen, und ein Rad, das auf ihr landet, gleitet weiter zur nächsten aktiven Zeile: `pickerRange(36, 46, { disabled: (n) => n === 38 })` für eine ausverkaufte Schuhgröße.
- `haptics` löst einen Auswahl-Tick aus, wann immer Finger, Maus-Drag oder Mausrad eine neue Zeile in die Mitte bringen, nie bei Tasten. Das funktioniert in Chrome auf Android; warum ein Scroll auf dem iPhone nicht ticken kann, erklärt unser [Guide zur Web Vibration API](https://wingo-ui.com/de/blog/web-vibration-api-haptic-feedback).

`PickerWheelField` packt dieselben Räder in ein Formularfeld: unter 768px ein Bottom Sheet mit den Buttons „Done“ und „Cancel“, am Desktop ein Popover, das Änderungen sofort übernimmt.

## Wie baust du ein React Time Picker Wheel, in das man am Desktop trotzdem tippen kann?

Nimm den [Time Picker](https://wingo-ui.com/components/time-picker), der je nach Breite umschaltet. Unter 768px ist das Feld schreibgeschützt mit `inputMode="none"`, damit keine Tastatur aufgeht, und ein Tap öffnet ein Bottom Sheet mit Stunden- und Minutenrad. Ab 768px ist es ein Textfeld, das „1430“, „2:30pm“ oder „2p“ versteht, mit ArrowUp und ArrowDown die Minuten weiterschaltet und per Klick ein Panel mit Spalten öffnet. Der Wert ist immer „HH:mm“ im 24-Stunden-Format:

```tsx
"use client";

import { useState } from "react";
import { TimePicker } from "@/components/ui/time-picker";

const lunchBreak = (time: string) => time >= "13:00" && time < "14:00";

export function PickupTime() {
  const [time, setTime] = useState("11:30");
  return (
    <TimePicker
      label="Abholzeit"
      name="pickup"
      locale="en-US"
      min="09:00"
      max="18:00"
      minuteStep={15}
      disabledTimes={lunchBreak}
      value={time}
      onValueChange={setTime}
    />
  );
}
```

> Live-Demo (Time Picker): Tipp 2:30pm oder 1430 ein und drück Enter, oder klick ins Feld, um die Spalten zu öffnen; in Handybreite öffnet ein Tap die Räder in einem Bottom Sheet, und die Mittagsstunde bleibt in beiden Fällen gesperrt. Probier sie unter [Time Picker](https://wingo-ui.com/components/time-picker) aus und installier sie mit `npx wingo-ui@latest add time-picker`.

Das Sheet bearbeitet einen Entwurf: „Done“ übernimmt ihn, und wer das Sheet wegzieht, behält die alte Uhrzeit. Zeiten außerhalb von `min` und `max` oder durch `disabledTimes` gesperrte Zeiten sind auch auf den Rädern deaktivierte Zeilen.

## Sollte ein Datum ein Rad oder ein Kalender sein?

Ein Rad für ein Datum, das man auswendig kennt und das weit von heute entfernt liegt, etwa ein Geburtsdatum: drei Wischgesten, statt Jahr um Jahr zurückzublättern. Ein Kalender für Daten nah an heute, bei denen der Wochentag zählt. Deshalb öffnet der [Date Picker](https://wingo-ui.com/components/date-picker) einen Kalender, auf dem Handy in einem Bottom Sheet.

Für das Geburtsdatum ist `useDateWheel` ein React Date Wheel Picker in einem einzigen Hook: Tages-, Monats- und Jahresspalte in der Reihenfolge der Locale (Monat, Tag, Jahr für en-US), Monatsnamen in dieser Sprache, Tage, die zum Monat passen (der 29. Februar nur in Schaltjahren), und Jahre zwischen `min` und `max`. Die Werte sind ISO-Daten wie „1990-05-17“.

> Live-Demo (Picker Wheel): Wisch über Monat, Tag oder Jahr, zieh sie mit der Maus oder spring per Tab zum Jahr und tipp 1985: Datum und Alter unter dem Rad aktualisieren sich, sobald es stillsteht. Probier sie unter [Picker Wheel](https://wingo-ui.com/components/picker-wheel) aus und installier sie mit `npx wingo-ui@latest add picker-wheel`.

Für das Rad auf dem Handy und ein Textfeld am Desktop renderst du beides und schaltest per CSS um, damit das Server-HTML zum ersten Paint passt (der Guide zu [Mobile-first React-Komponenten](https://wingo-ui.com/de/blog/mobile-first-react-components) erklärt, warum ein `useIsMobile`-Check kurz aufblitzen würde). Ein einziger ISO-String versorgt beide:

```tsx
"use client";

import { useState } from "react";
import { DatePicker } from "@/components/ui/date-picker";
import { PickerWheelField, useDateWheel } from "@/components/ui/picker-wheel";
import { formatDate, fromISODate, toISODate } from "@/lib/date-utils";

export function BirthdayField({ initial }: { initial: string }) {
  const [birthday, setBirthday] = useState(initial); // "1990-05-17"
  const [today] = useState(() => new Date());
  const wheel = useDateWheel({ value: birthday, onValueChange: setBirthday, locale: "en-US", min: "1920-01-01", max: toISODate(today) });

  return (
    <>
      {/* Handy: drei Räder in einem Bottom Sheet, Done übernimmt den Wert */}
      <div className="md:hidden">
        <PickerWheelField
          label="Geburtsdatum"
          {...wheel}
          formatValue={() => formatDate(fromISODate(birthday), "en-US", { dateStyle: "long" })}
        />
      </div>
      {/* ab 768px: 05171990 tippen oder den Kalender öffnen */}
      <div className="hidden md:block">
        <DatePicker
          label="Geburtsdatum"
          locale="en-US"
          max={today}
          clearable={false}
          value={fromISODate(birthday)}
          onValueChange={(date) => date && setBirthday(toISODate(date))}
        />
      </div>
      <input type="hidden" name="birthday" value={birthday} />
    </>
  );
}
```

Ein Haken: `useDateWheel` hält immer ein Datum (heute, wenn du keins übergibst), das Feld ist also nie leer. Das passt zum Bearbeiten eines Profils; ein Pflichtdatum, das leer startet, gehört in den Date Picker.

## Was geht bei einem Wheel Picker auf dem Handy schief?

Die meisten Bugs kommen aus der Umgebung des Rads. Das Picker Wheel fängt sie ab; baust du dein eigenes, begegnen sie dir:

- **Das Sheet klaut den Drag.** Ein Rad an seiner ersten Zeile gibt einen Drag nach unten an den Drawer weiter. Die Spalten tragen `data-vaul-no-drag`, und `overscroll-behavior: contain` verhindert, dass ein harter Wisch die Seite scrollt.
- **Ein Rad, das unsichtbar gemountet wird.** In einem geschlossenen Sheet hat die Liste keine Höhe, also bewirkt das Setzen von `scrollTop` nichts. Ein `ResizeObserver` scrollt zum Wert, sobald die Liste eine Größe hat.
- **Commit pro Zeile.** Wer bei jeder durchlaufenen Zeile speichert, flutet die API und löst mitten im Drehen die Validierung aus.
- **Reduzierte Bewegung.** Mit `prefers-reduced-motion` bleiben die Zeilen flach, und programmatische Bewegungen springen, statt zu gleiten.
- **Feedback nur im Tick.** Nutzer am Desktop und auf dem iPhone spüren nichts, also müssen die mittlere Zeile und der Feldtext den Wert tragen.

## Wo fängst du an?

Öffne diese Seite auf deinem Handy und wisch durch die beiden Demos oben. Picker Wheel, Time Picker und Date Picker gehören zu [Wingo UI Pro](https://wingo-ui.com/de/pricing); mit einem Pro-Konto, angemeldet über `npx wingo-ui@latest login`, kopiert `npx wingo-ui@latest add picker-wheel` die Komponente und alles, was sie importiert, in dein Projekt. Eine React Wheel Picker Komponente ist nur ein Baustein eines handyfreundlichen Formulars; die anderen Muster findest du unter [Mobile UI in React](https://wingo-ui.com/de/blog/category/mobile-ui).

## Komponenten in diesem Artikel

- [Picker Wheel](https://wingo-ui.com/components/picker-wheel) (Pro): A React iOS-style picker wheel whose columns spin with momentum and snap on a 3D cylinder. Installation: `npx wingo-ui@latest add picker-wheel`
- [Time Picker](https://wingo-ui.com/components/time-picker) (Pro): A React time picker to type a time or pick it from columns, with 12 / 24 hours by locale, minute steps, limits and wheels on phones. Installation: `npx wingo-ui@latest add time-picker`
- [Date Picker](https://wingo-ui.com/components/date-picker) (Pro): A React date picker: type a date in your locale or pick it from a calendar, with presets, limits, optional time and a phone sheet. Installation: `npx wingo-ui@latest add date-picker`

## Häufige Fragen

### Gibt es einen Wheel Picker im iOS-Stil für React-Web-Apps?

Ja. Stand Oktober 2026 gibt es als Open Source @ncdai/react-wheel-picker (1.2.3, MIT) und react-mobile-picker (1.2.0, MIT), die beide ihre eigene Touch-Physik mitbringen. Das Picker Wheel von Wingo UI (Pro) scrollt nativ und ergänzt ein Formularfeld, das auf dem Handy ein Bottom Sheet öffnet, einen Datums-Hook und haptische Ticks.

### Sollte ich input type=time statt eines Wheel Pickers nehmen?

Nimm das native Input-Element, wenn Aussehen und Verhalten reichen, denn es kostet nichts und ist von Haus aus barrierefrei. Den Picker, den es öffnet, kannst du nicht gestalten, er unterscheidet sich je nach Browser und OS-Version und kann keine gesperrten Zeiten wie eine Mittagspause zeigen. Genau dann lohnt sich ein eigenes Rad.

### Wie lasse ich einen Wheel Picker in React endlos drehen?

Rendere die Optionen mehrmals und spring zurück zur mittleren Kopie, sobald das Rad stillsteht. Das sieht identisch aus. Im Picker Wheel von Wingo UI erledigt loop auf dem Rad oder auf einer Spalte genau das, mit mindestens 40 Zeilen auf jeder Seite der mittleren Kopie, damit auch ein harter Wisch nie das Ende erreicht.

### Vibriert ein Web Wheel Picker auf dem iPhone?

Nein. Safari hat keine Vibration API, und die einzige Web-Haptik des iPhones, der Tick eines nativen Switches, kommt nur bei einem Tap, nie beim Scrollen. In Chrome auf Android tickt das Picker Wheel jedes Mal, wenn eine neue Zeile die Mitte erreicht.

### Ist das Picker Wheel von Wingo UI kostenlos?

Nein. Picker Wheel, Time Picker und Date Picker sind Pro-Komponenten; Drawer und Field, auf denen sie aufbauen, sind kostenlos. Pro kostet $8 im Monat, $80 im Jahr oder einmalig $150.

---

Quelle: https://wingo-ui.com/de/blog/react-wheel-picker
