React Bottom Sheet fürs Web: Snap Points, Drag, Tastatur

Wer nach einem React Bottom Sheet sucht, landet oft bei React-Native-Paketen wie @gorhom/bottom-sheet, die den Browser nur über React Native Web erreichen. Dieser Guide zeigt, was ein React Bottom Sheet webtauglich macht. Im Web hast du es mit einem echten Dialog im DOM zu tun, mit Pointer Events und position: fixed, mit einer Bildschirmtastatur, die den halben Screen verdeckt, und mit einem Home-Indikator unter deinem Speichern-Button. Er behandelt die fünf Teile, die entscheiden, ob sich ein Sheet auf dem Smartphone nativ anfühlt (Snap Points, Drag to Dismiss, verschachtelte Sheets, die Tastatur und die Safe Area), mit Code für vaul und für den kostenlosen Drawer von Wingo UI, der darauf aufbaut.
Welche React-Bottom-Sheet-Bibliothek funktioniert im Web?
Für Apps, die ins DOM rendern, gibt es zwei ernsthafte Kandidaten: vaul und den Drawer von Base UI. Beide sitzen auf einem Dialog-Primitive, also bringen sie Fokus-Falle, Escape und den barrierefreien Titel gleich mit. Stand Oktober 2026, laut npm-Registry und der Doku der Projekte:
Drei Fakten, die die Tabelle nicht zeigt, geben den Ausschlag. Das README von vaul (opens in a new tab) erklärt das Repo für nicht mehr gepflegt, und seit Dezember 2024 kam kein Release mehr; die Peer-Range enthält trotzdem React 19, und der Drawer von Wingo UI läuft darauf mit React 19.3. Die Peer-Range von react-spring-bottom-sheet endet bei React 18. Das README von react-modal-sheet (opens in a new tab) sagt, dass die Bibliothek keine eingebauten Barrierefreiheits-Eigenschaften mitbringt, also musst du Fokus-Falle und Dialog-Semantik selbst ergänzen.
Wenn dein Projekt schon Base UI nutzt, seit Juli 2026 (opens in a new tab) der Standard für neue shadcn/ui-Projekte, nimm dessen Drawer: Er wird gepflegt und deckt Snap Points, Verschachtelung und die Tastatur ab (Drawer.VirtualKeyboardProvider). Auf Radix ist vaul immer noch die vollständigste Option, und der Rest dieses Guides behandelt die Lücken, die du drumherum schließt.
Wie funktionieren vaul Drawer Snap Points?
Vaul Drawer Snap Points sind Haltepunkte auf dem Weg nach oben, aufsteigend sortiert, vom kleinsten bis zum größten sichtbaren Teil: Eine Zahl ist ein Bruchteil der Bildschirmhöhe (0.45 sind 45%), ein String sind Pixel ("320px"). Das Sheet rendert in voller Höhe und wird bis zum aktiven Punkt nach unten verschoben. Drei Details bringen in vaul 1.1.2 viele ins Stolpern:
- Brüche von 0 bis 1. Der Typ-Kommentar spricht von einem Array mit Zahlen von 0 bis 100, aber der Code multipliziert jede Zahl mit der Fensterhöhe.
50heißt also fünfzig Screens. Schreib0.5. - Nur Pixel. Ein String läuft durch
parseInt, also wird"30rem"zu 30 Pixeln. - Definier das Array außerhalb der Komponente. Vaul führt seinen Snap-Effekt jedes Mal neu aus, wenn sich die Identität von
snapPointsändert. Ein Inline-Literal lässt das Sheet also bei jedem Render der Elternkomponente neu einrasten.
Dann entscheidest du, was scrollt. Unterhalb des letzten Punkts sollte der Inhalt stillstehen, damit ein Wischen nach oben das Sheet zieht, statt eine Liste zu scrollen, die nur halb zu sehen ist. fadeFromIndex legt fest, ab welchem Punkt das Overlay abdunkelt; Standard ist der letzte, also bleibt die Seite bei einem halb offenen Sheet hell.
Mach das React Bottom Sheet modal, wenn es eine Aufgabe enthält. Gib modal={false} an, wenn die Seite dahinter bedienbar bleiben muss, etwa eine Ergebnisliste über einer Karte:
Mit Snap Points hält dismissible={false} das Sheet an seinem ersten Punkt an, die Liste lässt sich also nie hinter die Karte wegziehen. Der Drawer hält für jede unterschiedliche Punkteliste ein einziges Array, auch wenn du ein Literal übergibst, lässt den Inhalt erst am letzten Punkt scrollen und springt bei einem Tippen auf den Griff zum nächsten Punkt.
Wie entscheidet Drag to Dismiss, wann das Sheet schließt?
Nach Strecke oder Geschwindigkeit, gemessen in dem Moment, in dem der Finger loslässt. Ohne Snap Points schließt vaul das Sheet, wenn du es nach mehr als 25% seiner Höhe loslässt (closeThreshold, Standard 0.25) oder schneller als 0.4 Pixel pro Millisekunde wischst; alles darunter gleitet in 500ms mit cubic-bezier(0.32, 0.72, 0, 1) zurück. Mit Snap Points landet es auf dem nächstgelegenen Punkt, ein schneller Wisch bewegt es einen Punkt in seine Richtung, und ein Wisch über 2 Pixel pro Millisekunde schließt das Sheet oder öffnet es ganz. Schwieriger ist die Frage, wann ein Drag gar nicht erst starten sollte. Vaul 1.1.2 lehnt ihn ab:
- in einem scrollbaren Element, das nicht ganz nach oben gescrollt ist, damit eine lange Liste zuerst scrollt;
- für einen kurzen Moment, nachdem der Inhalt gescrollt hat (
scrollLockTimeout: 100ms im Code, obwohl der Typ-Kommentar 500ms nennt); - während Text markiert ist, auf einem nativen
<select>oder auf allem innerhalb von[data-vaul-no-drag]; - in den ersten 500ms nach dem Öffnen.
Linke und rechte Drawer überspringen die Scroll- und Zeitprüfungen. Für Inhalte mit eigenen horizontalen Gesten (ein Karussell, eine Karte, ein Range-Slider) setzt du handleOnly, damit nur der Griff zieht. Und gib jedem Drag eine sichtbare Alternative: Escape, ein Tippen auf die abgedunkelte Seite und einen Schließen-Button. Der Drawer von Wingo UI lässt den Schließen-Button bei Bottom Sheets standardmäßig weg, also übergib showClose (Panels und Dialoge haben immer einen).
Die Demo unten ist eine Seite mit Inseraten. „Filters“ öffnet ein Sheet mit Filtern für Preis, Zustand und Kategorie und einem fixierten Button mit der Zahl der Treffer.
$ npx wingo-ui@latest add drawerWie verschachtelst du Bottom Sheets?
Öffne das zweite Sheet aus dem ersten heraus und schieb das erste nach hinten, wie unter iOS. In vaul ist das Drawer.NestedRoot, gerendert innerhalb von Drawer.Content des Eltern-Drawers. Ohne Eltern-Drawer wirft es einen Fehler. Während sich der Kind-Drawer öffnet, macht es den Eltern-Drawer 16px schmaler, hebt ihn um 16px an und folgt dem Drag des Kindes. Den Eltern-Drawer bewegt es nur aus seinem eigenen onOpenChange. Öffnest du das Kind über open aus dem State, bleibt der Eltern-Drawer also unverändert.
Der Drawer von Wingo UI erkennt die Verschachtelung über den Context. Ein Drawer, der im Inhalt eines anderen Drawers rendert, nutzt den Nested Root von selbst und schiebt den Eltern-Drawer auch bei kontrolliertem Öffnen zurück:
Hör bei zwei Ebenen auf. Auf einem 390px-Screen ist ein drittes Sheet eigentlich eine Seite, also navigier zu einer eigenen.
Was passiert, wenn sich in einem Bottom Sheet die Tastatur öffnet?
Die Tastatur verdeckt das Sheet, solange nichts es verschiebt. In Safari auf iOS und in Chrome auf Android seit Version 108 verkleinert die Tastatur nur den Visual Viewport (Ankündigung von Chrome (opens in a new tab)), also bleibt ein Element mit position: fixed; bottom: 0 dahinter. Die Option repositionInputs von vaul ist standardmäßig aktiv und hört auf Resize-Events von visualViewport: Solange ein Feld den Fokus hat, hebt es das Sheet um die Höhe an, die die Tastatur verdeckt, und verkleinert es, wenn es nicht mehr über die Tastatur passt.
Zwei Regeln sorgen dafür, dass das in der Praxis funktioniert:
- Fokussier beim Öffnen kein Feld. Der Drawer von Wingo UI fokussiert das Sheet selbst, damit die Tastatur nicht das halbe Sheet verdeckt, bevor jemand es gelesen hat; Tab springt dann zum ersten Control. Wenn Tippen die ganze Aufgabe ist, etwa beim Umbenennen einer Liste, übergib
onOpenAutoFocus, rufevent.preventDefault()auf und fokussier das Feld selbst. - Nimm 16px Schrift in Eingabefeldern (
text-base md:text-sm). Safari auf iOS zoomt in ein fokussiertes Feld mit kleinerer Schrift hinein.
Schalt repositionInputs ab, wenn das Sheet die Tastatur selbst behandelt, zum Beispiel ein Chat-Eingabefeld, das der Tastatur mit dem Pro-Hook useKeyboardInset folgt.
Wie hältst du ein Bottom Sheet vom Home-Indikator fern?
Lass die Seite mit viewport-fit=cover bis an die Bildschirmränder reichen (in Next.js viewportFit: "cover" im viewport-Export des Root-Layouts) und gib dann dem Teil des Sheets, der den unteren Rand berührt, max(env(safe-area-inset-bottom), 16px) als Padding. Das max() lässt 16px Luft auf Geräten, die einen Inset von 0 melden. Gib nur dem letzten Teil dieses Padding: den Footer, wenn es einen gibt, sonst den scrollenden Inhalt, damit seine letzte Zeile über den Indikator scrollen kann. Begrenz die Höhe auf 92dvh, damit ein Streifen der Seite zum Antippen sichtbar bleibt. Bei einem einfachen vaul Drawer:
Der Drawer von Wingo UI gibt das Padding immer dem Teil, der zuletzt kommt. Mit variant="floating" schwebt er stattdessen 8px über der Safe Area, mit allen Ecken abgerundet, so wie das Action Sheet in seiner iOS-Variante. Dieselben Insets für Tab Bars, FABs und Toasts findest du in unserem Tutorial zu Safe Area Insets in Tailwind (auf Englisch).
Welches Bottom Sheet nimmst du für welche Aufgabe?
Die eine Bottom Sheet UI React-Komponente, die jeden Zweck abdeckt, gibt es nicht. Wähl nach der Aufgabe:
Das Action Sheet funktioniert auch imperativ: Bind einmal einen ActionSheetProvider ein, hol dir in einer Komponente const actionSheet = useActionSheet(), und in einem Click-Handler liefert await actionSheet.show({ actions }) die ID der gewählten Aktion, oder null bei Abbrechen oder Schließen. Gibt das onSelect einer Aktion ein Promise zurück, bleibt das Sheet offen und zeigt einen Spinner in ihrer Zeile; es schließt, wenn das Promise erfüllt wird, und wenn es abgelehnt wird, wackelt die Zeile und bleibt bereit für einen neuen Versuch.
$ npx wingo-ui@latest add action-sheetDer Family Drawer behält ein einziges schwebendes Sheet und animiert dessen Höhe zwischen den Ansichten. Das passt zu einem kurzen Ablauf wie einem Menü, das zu einer Bestätigung führt.
$ npx wingo-ui@latest add family-drawerIn der Registry (Release 1.7.0) importieren 39 weitere Items den Drawer, darunter Popover, Select, Dropdown Menu und Dialog, die sich unter 768px als Bottom Sheets öffnen. Jedes Sheet einer App verhält sich beim Ziehen, Einrasten und Padding gleich.
Wie geht es weiter?
Installier den kostenlosen Drawer mit npx wingo-ui@latest add drawer, öffne deine App bei 390px und zieh ein Sheet zu, dessen Liste halb gescrollt ist. Für den Wechsel zwischen zentrierter Karte und Sheet lies das Tutorial zum responsiven shadcn Dialog; für Touch-Ziele, Safe Areas und die übrigen Smartphone-Regeln den Guide zu Mobile-First-React-Komponenten und die Kategorie Mobile UI in React. Das Action Sheet gehört zu Wingo UI Pro.
Häufige Fragen
Kann ich @gorhom/bottom-sheet in einer React-Web-App nutzen?
Nur über React Native Web, denn es ist eine React-Native-Bibliothek, die Reanimated und Gesture Handler braucht. In einer Next.js- oder Vite-App, die ins DOM rendert, nimmst du vaul oder den Drawer von Base UI, die auf barrierefreien Dialog-Primitives aufbauen.
Wird vaul noch gepflegt?
Nein. Stand Oktober 2026 erklärt das README das Repo für nicht mehr gepflegt, und das letzte Release ist 1.1.2 vom 14. Dezember 2024. React 19 steht weiterhin in den unterstützten Versionen; die gepflegte Alternative ist der Drawer von Base UI.
Warum funktionieren meine vaul Snap Points mit Werten wie 50 nicht?
Vaul multipliziert jede Zahl mit der Bildschirmhöhe, also müssen Zahlen Brüche von 0 bis 1 sein (0.5 für den halben Screen), auch wenn ein Typ-Kommentar 0 bis 100 nennt. Strings werden als Pixel gelesen, schreib also "320px" und nie rem.
Wie verhindere ich, dass sich ein vaul Drawer mitbewegt, wenn ich in einem Karussell darin wische?
Setz handleOnly, damit nur der Griff das Sheet zieht, oder gib dem Root-Element des Karussells das Attribut data-vaul-no-drag. Drags, die auf einem nativen Select starten oder während Text markiert ist, ignoriert vaul bereits.
Ist der Drawer von Wingo UI kostenlos?
Ja. Drawer, Family Drawer und Sheet sind kostenlos, das Action Sheet gehört zu Pro. npx wingo-ui@latest add drawer kopiert die Komponente samt Button, Hooks und Helfern, die sie importiert, in dein Projekt.
- Mobile UI
- Bottom Sheet
- Vaul
- React
- Next.js


