# React Action Sheet: wie unter iOS, async und responsive

> So baust du ein React Action Sheet wie unter iOS: rote Zeilen zum Löschen, Abbrechen in eigener Karte, show() mit await für async Aktionen, ab 768px ein Dialog.

- Autor: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Veröffentlicht: 16. 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/react-action-sheet
- Englische Fassung: https://wingo-ui.com/blog/react-action-sheet

## Kurz gesagt

Ein React Action Sheet ist eine kurze Liste von Aktionen, die auf dem Smartphone von unten hochfährt, mit einem Titel, destruktiven Zeilen in Rot und Abbrechen in einer eigenen Karte ganz unten. Ein gutes Sheet wartet mit einem Spinner in der Zeile auf eine async Aktion, bleibt offen und wiederholbar, wenn die Aktion scheitert, und öffnet sich auf breiteren Screens als kompakter Dialog oder als verankertes Menü. Das Action Sheet von Wingo UI kann all das und bringt zusätzlich ein show() mit, auf das du mit await wartest: Es liefert die id der gewählten Aktion oder null, wenn abgebrochen wird.

Ein React Action Sheet ist das, was jemand auf dem Smartphone nach einem Tipp auf ⋯ bei einem Foto, einer Nachricht oder einem Inserat erwartet: Die Seite wird abgedunkelt, eine kurze Liste von Aktionen fährt von unten hoch, Löschen ist rot, und Abbrechen sitzt abgesetzt genau dort, wo der Daumen schon ist. Die meisten Web-Nachbauten des iOS Action Sheets kriegen das Hochfahren hin, und dabei bleibt es. Sie schließen, bevor der Lösch-Request fertig ist, zeigen nichts, wenn er scheitert, bieten keinen Weg per Tastatur und sehen auf einem 1440px-Monitor aus wie ein in die Breite gezogenes Smartphone. Dieser Artikel zeigt, was das Muster braucht, und baut es dann mit dem [Action Sheet](https://wingo-ui.com/components/action-sheet) von Wingo UI, inklusive `show()` mit `await` und dem Dialog oder Menü, zu dem es ab 768px wird.

## Wann nimmst du ein Action Sheet statt eines Menüs oder Alerts?

Nimm ein Action Sheet, wenn die Person gerade etwas angefangen hat und entscheiden muss, wie es endet, zum Beispiel beim Schließen einer halb geschriebenen E-Mail: Entwurf löschen oder speichern. Genau dieses Beispiel nutzen Apples [Richtlinien für Action Sheets](https://developer.apple.com/design/human-interface-guidelines/action-sheets) (geprüft im Oktober 2026). Sie grenzen das Muster auch von seinen Nachbarn ab: Ein Alert kommt laut Apple meist unerwartet und meldet ein Problem, ein Menü dagegen erwartet man nur, wenn man es selbst aufruft. Dieselbe Seite verlangt drei Dinge, die jede Umsetzung prägen:

- **Destruktive Optionen fallen auf.** Nutze den destruktiven Stil und setz diese Zeilen nach oben, wo man sie bemerkt.
- **Abbrechen kommt nach unten.** So kann man eine Aktion ablehnen, die Daten zerstören könnte.
- **Kein Scrollen.** Für iOS rät Apple davon ab, ein Action Sheet scrollen zu lassen: Je mehr Buttons, desto länger dauert die Wahl, und Scrollen verleitet zu versehentlichen Tipps. Der Abschnitt zu watchOS setzt zusätzlich eine Grenze von vier Buttons inklusive Abbrechen, eine sinnvolle Obergrenze auch auf dem Smartphone.

Im Web wird daraus ein mobiles Aktionsmenü, das ein einzelner Daumen erreicht, und auf dem Desktop etwas Ruhigeres. So teilen sich die vier verwandten Komponenten von Wingo UI die Arbeit:

| Komponente | Wofür | Auf dem Smartphone | Ab 768px | Zugang |
|---|---|---|---|---|
| [Action Sheet](https://wingo-ui.com/components/action-sheet) | Wenige Optionen zu einer Aktion, async Zeilen, `await show()` | iOS-Karten oder ein Material-Sheet | 400px-Dialog, verankertes Menü oder dasselbe Sheet | Pro |
| [Dropdown Menu](https://wingo-ui.com/components/dropdown-menu) | Befehle durchsehen, Checkbox- und Radio-Einträge, Untermenüs, Shortcuts | Ein Action Sheet mit 48px-Zeilen (`responsive`) | Ein schwebendes Menü | Kostenlos |
| [Alert Dialog](https://wingo-ui.com/components/alert-dialog) | Eine Ja-oder-Nein-Frage, die eine Erklärung braucht | Eine zentrierte Karte, Buttons in voller Breite gestapelt | Eine zentrierte Karte | Pro |
| [Drawer](https://wingo-ui.com/components/drawer) | Beliebiger Inhalt in einem Sheet: Formulare, Filter, Details | Ein Bottom Sheet auf Basis von vaul, mit Snap Points | Ein zentriertes Sheet, Seitenpanel oder Dialog | Kostenlos |

## Was rendert das Action Sheet auf dem Smartphone?

Auf dem Smartphone rendert es ein Bottom Sheet in einem von zwei Stilen. `variant="ios"` (der Standard) lässt eine Karte mit Titel, Nachricht und 56px hohen, zentrierten Zeilen 8px über dem Home-Indikator schweben, mit Abbrechen in einer eigenen Karte darunter. `variant="material"` ist ein einziges angedocktes Sheet mit Griff, 48px-Zeilen mit Icons am Anfang und Abbrechen als letzter Zeile. Beide schließt du, indem du sie nach unten ziehst, denn das Sheet fürs Smartphone ist der kostenlose [Drawer](https://wingo-ui.com/components/drawer), der auf vaul aufbaut.

Jede Zeile ist eine `ActionSheetAction`: `{ id, label, icon?, description?, tone?, disabled?, onSelect?, keepOpen?, badge? }`, wobei `tone` `"neutral"`, `"brand"` oder `"danger"` ist. Hier eine deklarative Action-Sheet-Komponente auf der Karte eines Inserats, mit der destruktiven Zeile zuerst, wie Apple es empfiehlt:

```tsx
"use client";

import { Link2, MoreHorizontal, Pencil, Trash2 } from "lucide-react";
import { ActionSheet } from "@/components/ui/action-sheet";
import { Button } from "@/components/ui/button";
import { toast } from "@/components/ui/toast";

type Listing = { id: string; title: string; posted: string };

export function ListingActions({
  listing,
  onEdit,
  onDeleted,
}: {
  listing: Listing;
  onEdit: (id: string) => void;
  onDeleted: (id: string) => void;
}) {
  return (
    <ActionSheet
      title={listing.title}
      message={listing.posted}
      trigger={
        <Button variant="ghost" size="icon" radius="full" aria-label={`Aktionen für ${listing.title}`}>
          <MoreHorizontal />
        </Button>
      }
      actions={[
        {
          id: "delete",
          label: "Inserat löschen",
          icon: <Trash2 />,
          tone: "danger",
          // das Sheet wartet mit einem Spinner in dieser Zeile, bis das Promise abgeschlossen ist
          onSelect: async () => {
            const res = await fetch(`/api/listings/${listing.id}`, { method: "DELETE" });
            if (!res.ok) {
              toast.error("Das Inserat ließ sich nicht löschen. Versuch es noch einmal.");
              // erneut werfen, damit die Zeile wackelt und für einen neuen Versuch bereit bleibt
              throw new Error(`DELETE failed with ${res.status}`);
            }
            onDeleted(listing.id);
          },
        },
        { id: "edit", label: "Inserat bearbeiten", icon: <Pencil />, onSelect: () => onEdit(listing.id) },
        {
          id: "copy",
          label: "Link kopieren",
          icon: <Link2 />,
          // void: das Sheet schließt sofort, statt auf die Zwischenablage zu warten
          onSelect: () => void navigator.clipboard.writeText(`${location.origin}/listings/${listing.id}`),
        },
      ]}
    />
  );
}
```

`trigger` rendert mit `asChild`, also übergib ein einzelnes Element, das eine Ref annimmt, etwa einen `Button`. Im iOS-Sheet bleiben Icons verborgen, solange du `showIcons` nicht setzt; in Material und auf dem Desktop erscheinen sie standardmäßig. Für mehrere Gruppen übergibst du statt `actions` die Prop `groups` (ein Array aus Action-Arrays): Unter iOS wird jede Gruppe zu einer eigenen Karte.

> Live-Demo (Action Sheet): Tipp auf ⋯ beim Inserat, um das iOS-Sheet zu öffnen, und probier dann „Delete listing“: Die Zeile zeigt kurz einen Spinner, die anderen Zeilen werden blasser, und das Sheet schließt, sobald das Promise erfüllt ist. Probier sie unter [Action Sheet](https://wingo-ui.com/components/action-sheet) aus und installier sie mit `npx wingo-ui@latest add action-sheet`.

## Wie geht das Sheet mit async und destruktiven Aktionen um?

Gibt `onSelect` ein Promise zurück, zeigt die Zeile einen Spinner, die anderen Zeilen werden blasser, und das Sheet weigert sich zu schließen: Escape, ein Tipp auf die abgedunkelte Seite und das Ziehen warten alle, bis das Promise abgeschlossen ist. Wird es erfüllt, feuert `onAction(id)` und das Sheet schließt. Wird es abgelehnt, wackelt die Zeile wie bei einem falschen Entsperrcode und bleibt für einen neuen Versuch bereit. Das ist der Unterschied zwischen einem Sheet, das wirklich „Gelöscht“ meldet, und einem, das nur so aussieht.

Das Wackeln sagt der Person, dass etwas schiefging, aber nicht was. Deshalb ruft das Beispiel mit dem Inserat `toast.error()` auf, bevor es den Fehler erneut wirft: Der [Toast](https://wingo-ui.com/components/toast) sagt warum, das Wackeln sagt wo, und die Zeile ist für einen zweiten Tipp bereit. Der Toast braucht einen `Toaster`, den du einmal in deinem Layout einbindest. Zwei Props ändern das Schließen: `closeOnAction={false}` hält das Sheet nach jeder Aktion offen, und `keepOpen: true` auf einer einzelnen Zeile tut dasselbe nur für diese Zeile. Das passt zu einem Schalter wie „Benachrichtigungen stummschalten“.

Wenn du ein Löschen bestätigen willst, ist oft schon das Sheet selbst der richtige Schritt. Ein Titel wie „Dieses Inserat löschen?“, eine Nachricht darunter, eine rote Zeile und Abbrechen ergeben eine Bestätigung mit zwei Tipps, genau dort, wo der Daumen ist. Hat die erste aktive Aktion den Ton `danger`, setzt das Sheet den Tastaturfokus auf Abbrechen statt auf Löschen. Ein Druck auf Enter direkt nach dem Öffnen zerstört also nie etwas.

## Wie wartest du mit show() auf ein Action Sheet?

Ruf `useActionSheet().show(options)` in einem beliebigen Event-Handler auf und warte mit `await` auf das Ergebnis: Du bekommst die id der gewählten Aktion zurück oder `null`. Dafür bindest du vorher einmal `ActionSheetProvider` weit oben im Baum ein. `show()` nimmt dieselben Props wie `ActionSheet` außer `open`, `defaultOpen`, `onOpenChange` und `trigger` und liefert `null`, wenn die Person abbricht oder das Sheet schließt. Das Ergebnis kommt erst, nachdem das Promise der gewählten Zeile erfüllt ist, also läuft der Code nach dem `await`, sobald die Arbeit erledigt ist. Das entspricht Apples Beispiel aus Mail, dem Entwurf, den du schließt:

```tsx
"use client";

import { useActionSheet } from "@/components/ui/action-sheet";
import { Button } from "@/components/ui/button";

export function CloseDraftButton({
  saveDraft,
  discardDraft,
  onClose,
}: {
  saveDraft: () => Promise<void>;
  discardDraft: () => Promise<void>;
  onClose: () => void;
}) {
  const actionSheet = useActionSheet();

  const close = async () => {
    const choice = await actionSheet.show({
      actions: [
        { id: "discard", label: "Entwurf löschen", tone: "danger", onSelect: discardDraft },
        { id: "save", label: "Entwurf speichern", onSelect: saveDraft },
      ],
    });
    // null heißt Abbrechen, Escape oder Wegwischen: weiter bearbeiten
    if (choice) onClose();
  };

  return (
    <Button variant="ghost" onClick={close}>
      Abbrechen
    </Button>
  );
}
```

```tsx
// app/layout.tsx
import type { ReactNode } from "react";
import { ActionSheetProvider } from "@/components/ui/action-sheet";

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="de">
      <body>
        <ActionSheetProvider defaults={{ cancelLabel: "Abbrechen" }}>{children}</ActionSheetProvider>
      </body>
    </html>
  );
}
```

`defaults` gilt für jeden Aufruf, die eigenen Optionen des Aufrufs haben aber Vorrang. Dort landen übersetzte Labels oder eine hauseigene `variant`. Ein zweites `show()` ersetzt das offene Sheet, und der erste Aufruf liefert `null`. Schließt das Sheet, geht der Fokus an das Element zurück, das ihn vor dem Aufruf hatte. `useActionSheet()` wirft außerhalb des Providers einen Fehler, damit ein fehlender Provider in der Entwicklung laut scheitert. Eine abgelehnte Zeile lässt das Promise offen, denn die Person kann es noch einmal versuchen oder abbrechen.

## Wie sieht das Action Sheet auf dem Desktop aus?

Smartphones bekommen immer das Sheet; ab 768px wählt die Prop `desktop` eines von drei Layouts:

- `desktop="dialog"` (der Standard): ein kompakter 400px-Dialog mit denselben Zeilen, linksbündig und mit Icons.
- `desktop="popover"`: ein Menü, das am Trigger verankert ist. Es zeigt kein Abbrechen, denn Menüs schließen mit Escape und mit Klicks außerhalb. Ein Aufruf von `show()` hat keinen Trigger, an dem es hängen könnte, und fällt deshalb auf den Dialog zurück.
- `desktop="sheet"`: das Sheet fürs Smartphone in jeder Breite, für Kiosk-Anwendungen oder Oberflächen, die vor allem fürs Tablet gebaut sind.

```tsx
<ActionSheet desktop="popover" title="Angebot #Q-2026-118" actions={actions} trigger={moreButton} />
```

Framework7 macht etwas Ähnliches: Seine [Doku zum Action Sheet](https://framework7.io/docs/action-sheet) nennt eine Option `convertToPopover`, die das Sheet auf großen Screens in ein Popover verwandelt. Ionics [`ion-action-sheet`](https://ionicframework.com/docs/api/action-sheet) (`@ionic/react` 9.0.8, beide geprüft im Oktober 2026) kennt die Button-Rollen `destructive` und `cancel` und rendert den Cancel-Button immer zuletzt. Ein eigenes Layout für breite Screens beschreibt die Doku aber nicht, und `present()` liefert nichts zurück: Die Auswahl liest du aus dem Ergebnis von `onDidDismiss`. Sind deine Aktionen eigentlich ein Befehlsmenü mit Checkboxen, Untermenüs und Shortcuts, fang lieber mit dem [Dropdown Menu](https://wingo-ui.com/components/dropdown-menu) an: Auf dem Desktop ist es ein schwebendes Menü, und unter 768px wird es von selbst zum Action Sheet.

> Live-Demo (Dropdown Menu): Öffne das Menü auf dem Desktop, um ein schwebendes Menü mit Icons und Shortcuts zu sehen; auf dem Smartphone fahren dieselben Einträge als Action Sheet mit Abbrechen-Button hoch. Probier sie unter [Dropdown Menu](https://wingo-ui.com/components/dropdown-menu) aus und installier sie mit `npx wingo-ui@latest add dropdown-menu`.

## Lässt sich das Action Sheet per Tastatur bedienen?

Ja, und zwar nach denselben Regeln wie ein Menü. Beim Öffnen landet der Fokus auf der ersten Aktion, oder auf Abbrechen, wenn die erste Aktion destruktiv ist. Pfeil nach oben und unten wechseln zwischen den Aktionen und Abbrechen, Home und End springen an die Enden, Enter oder Leertaste führen die fokussierte Zeile aus, Tab wandert im Kreis durch das Sheet, und Escape bricht ab. Der Titel benennt das Sheet; fehlt er, wird `labels.sheetTitle` (standardmäßig „Actions“) zum versteckten Titel für Screenreader, und `message` wird zur Beschreibung. Ist reduzierte Bewegung eingestellt, wird das Hochfahren des Sheets auf dem Smartphone verkürzt, der Dialog auf dem Desktop blendet nur ein, und gescheiterte Zeilen wackeln nicht.

## Welche Stolperfallen zeigen sich auf echten Smartphones?

Vier tauchen auf, sobald das Sheet auf echte Requests und echte Geräte trifft:

- **Ein Promise zurückgeben, auf das du nicht warten willst.** Jedes Promise aus `onSelect` hält das Sheet offen. `navigator.share()` ist der klassische Fall: Es ist erst abgeschlossen, wenn die Person mit dem nativen Teilen-Dialog fertig ist, und wird abgelehnt, wenn sie es abbricht, was deine Zeile wackeln lässt. Ruf es mit `void` auf oder schließ das Sheet vorher.
- **Zu viele Zeilen.** Jede zusätzliche Zeile muss man lesen, bevor sich der Daumen bewegt. Deshalb verlangt Apple ein Sheet, das nie scrollt. Das Sheet begrenzt seine Liste auf `60dvh` und scrollt innerhalb seiner Karte, mit Abbrechen fest darunter, aber eine lange Liste mit Personen oder Ordnern gehört in einen Picker. Unser Guide zum [React Bottom Sheet fürs Web](https://wingo-ui.com/de/blog/react-bottom-sheet-for-the-web) behandelt Sheets mit echtem Inhalt.
- **Der Home-Indikator.** Ein Sheet, das an `bottom: 0` klebt, legt Abbrechen unter den Home-Balken des iPhones. Die iOS-Karten schweben darüber, und das Material-Sheet polstert mit `env(safe-area-inset-bottom)`; der [Guide zu Safe Area Insets](https://wingo-ui.com/de/blog/tailwind-safe-area-insets) erklärt das CSS.
- **Das Sheet für endgültige Löschvorgänge nutzen, die eine Erklärung brauchen.** „Konto und alle Rechnungen löschen“ verdient einen [Alert Dialog](https://wingo-ui.com/components/alert-dialog): eine zentrierte Karte, bei der ein Tipp auf die abgedunkelte Seite nichts tut, der Fokus auf Abbrechen startet und `await confirm()` `true` oder `false` liefert.

> Live-Demo (Alert Dialog): Drück auf „Delete“, um den Alert Dialog zu öffnen: Der Fokus startet auf „Cancel“, die abgedunkelte Seite ignoriert Klicks, und der Bestätigungs-Button zeigt einen Spinner, solange der Request läuft. Probier sie unter [Alert Dialog](https://wingo-ui.com/components/alert-dialog) aus und installier sie mit `npx wingo-ui@latest add alert-dialog`.

Das Action Sheet ist eines der kleinen Details aus unserer Liste, was eine [Web-App ausmacht, die sich nativ anfühlt](https://wingo-ui.com/de/blog/web-app-that-feels-native). Für die ganze mobile Ebene, von Touch-Zielen bis zu Bottom Sheets, lies den [Guide zu mobile-first React-Komponenten](https://wingo-ui.com/de/blog/mobile-first-react-components) oder stöbere in [Mobile UI in React](https://wingo-ui.com/de/blog/category/mobile-ui).

## Wie installierst du das Action Sheet?

Führ `npx wingo-ui@latest add action-sheet` aus. Der Befehl kopiert `components/ui/action-sheet.tsx` samt Button, Dialog, Drawer, Popover und Spinner, die es importiert, die gemeinsamen Hooks und ihre npm-Pakete (`motion`, `lucide-react`, `vaul` und die Radix-Primitives). Das Action Sheet gehört zu [Wingo UI Pro](https://wingo-ui.com/de/pricing), also melde dich vorher mit `npx wingo-ui@latest login` an; Drawer und Dropdown Menu, neben denen es steht, sind kostenlos.

## Komponenten in diesem Artikel

- [Action Sheet](https://wingo-ui.com/components/action-sheet) (Pro): A React iOS-style action sheet: a short list of actions with a title, red destructive rows, a separate Cancel and an awaitable show(). Installation: `npx wingo-ui@latest add action-sheet`
- [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`
- [Dropdown Menu](https://wingo-ui.com/components/dropdown-menu) (Free): A React dropdown menu: icons, descriptions, shortcuts, badges, checkbox and radio items, submenus, and an action sheet on phones. Installation: `npx wingo-ui@latest add dropdown-menu`
- [Alert Dialog](https://wingo-ui.com/components/alert-dialog) (Pro): A React alert dialog for confirmations: an async confirm that loads, fails inline and shakes, plus an await confirm() API. Installation: `npx wingo-ui@latest add alert-dialog`

## Häufige Fragen

### Was ist ein Action Sheet in React?

Ein Action Sheet ist eine modale Auswahlliste zu etwas, das die Person gerade getan hat, etwa auf einen Tipp auf den ⋯-Button eines Fotos. Auf dem Smartphone fährt es von unten hoch, zeigt destruktive Optionen in Rot und endet mit einem Abbrechen-Button. In React ist es ein Bottom Sheet mit einer Liste echter Buttons; die Version von Wingo UI baut ihr Sheet fürs Smartphone über den Drawer auf vaul auf.

### Was ist der Unterschied zwischen einem Action Sheet und einem Dropdown-Menü?

Ein Dropdown-Menü öffnet man bewusst, um Befehle durchzusehen, und es kann Checkboxen, Radio-Einträge und Untermenüs enthalten. Ein Action Sheet bietet wenige Optionen zu einer einzigen Aktion, mit Titel und Abbrechen. Das Dropdown Menu von Wingo UI wird unter 768px schon von selbst zum Action Sheet. Nimm das Action Sheet, wenn du async Zeilen, Gruppen oder einen Aufruf mit await brauchst.

### Soll ich ein Löschen mit einem Action Sheet oder einem Alert Dialog bestätigen?

Laut Apples Richtlinien gehören Optionen zu einer Aktion, die die Person selbst gestartet hat, in ein Action Sheet und nicht in einen Alert; Alerts beschreibt Apple meist als unerwartete Nachricht über ein Problem. Ein Sheet mit einer roten Zeile und Abbrechen reicht für die meisten Löschvorgänge; nimm einen Alert Dialog, wenn sich das Löschen nicht rückgängig machen lässt und einen erklärenden Satz oder eine getippte Bestätigung braucht.

### Wie viele Buttons sollte ein Action Sheet haben?

So wenige, dass das Sheet nie scrollt: Apples iOS-Richtlinien raten von einem scrollenden Action Sheet ab, und die Richtlinien für watchOS begrenzen es auf vier Buttons inklusive Abbrechen. Übergibst du mehr Zeilen, begrenzt das Action Sheet von Wingo UI seine Liste auf 60% der dynamischen Viewport-Höhe und scrollt innerhalb seiner Karte, mit Abbrechen fest darunter.

### Ist das Action Sheet von Wingo UI kostenlos?

Nein. Action Sheet und Alert Dialog sind Pro-Komponenten. Drawer, Dialog, Popover und Dropdown Menu, auf denen es aufbaut oder neben denen es steht, sind kostenlos.

---

Quelle: https://wingo-ui.com/de/blog/react-action-sheet
