# Shadcn Responsive Dialog: Karte am Desktop, Sheet am Handy

> Bau einen shadcn responsive dialog nur einmal: zentrierte Karte ab 768px, ziehbares Bottom Sheet darunter, ein einziger Inhalt und kein Hydration-Flackern.

- 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: 10 Min.
- Kanonische URL: https://wingo-ui.com/de/blog/shadcn-responsive-dialog-on-mobile
- Englische Fassung: https://wingo-ui.com/blog/shadcn-responsive-dialog-on-mobile

## Kurz gesagt

Ein shadcn responsive dialog rendert ab 768px den Dialog und darunter den Drawer, ein Bottom Sheet, das du nach unten ziehen kannst. Leg diese Umschaltung in eine einzige Root-Komponente, deren Teile den Modus aus dem Context lesen: So schreibst du den Inhalt nur einmal, und der offene Zustand übersteht ein Resize. Lies den Breakpoint mit useSyncExternalStore, damit Server und Hydration-Render übereinstimmen, und halte alles, was vor dem ersten Antippen sichtbar ist, in beiden Modi gleich, damit nichts flackern kann.

Der shadcn responsive dialog ist das Muster aus der Drawer-Doku von shadcn/ui: am Desktop ein zentrierter Dialog, auf dem Handy ein Drawer, also ein Bottom Sheet, das du nach unten ziehen kannst. Das Rezept funktioniert. Aber du schreibst Trigger und Header doppelt, und der Zweig wird von einem Hook gewählt, der beim ersten Render rät. Beides führt zu Bugs, sobald du einen shadcn Dialog mobile-tauglich machst: Die Kopien laufen auseinander, und der Zweig kippt nach dem Mount. Dieses Tutorial baut eine einzige Dialog-API, die ab 768px eine Karte und darunter ein ziehbares Sheet rendert. Den Inhalt schreibst du einmal, und während der Hydration flackert nichts.

## Was macht das Rezept für den shadcn responsive dialog?

Es rendert zwei komplette Bäume und wählt einen per Media Query. Stand Oktober 2026 schlägt die [Drawer-Doku von shadcn](https://ui.shadcn.com/docs/components/radix/drawer) vor, Dialog und Drawer zu einem responsiven Dialog zu kombinieren, und das [Beispiel drawer-dialog](https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/new-york-v4/examples/drawer-dialog.tsx) sieht etwa so aus:

```tsx
const [open, setOpen] = React.useState(false)
const isDesktop = useMediaQuery("(min-width: 768px)")

if (isDesktop) {
  return <Dialog open={open} onOpenChange={setOpen}>{/* Trigger, Header, Formular */}</Dialog>
}
return <Drawer open={open} onOpenChange={setOpen}>{/* Trigger, Header, Formular, Footer */}</Drawer>
```

Dass `open` über dem Zweig liegt, ist richtig: Ein Resize über 768px hinweg lässt den Dialog offen. Drei Dinge kosten dich später:

1. **Zwei Kopien.** Trigger, Titel und Beschreibung stehen in beiden Zweigen, und der Cancel-Footer existiert nur im Drawer. Jede Textänderung musst du an zwei Stellen einpflegen.
2. **Eine Vermutung bei jedem Mount.** Der [`useMediaQuery`](https://github.com/shadcn-ui/ui/blob/main/apps/v4/hooks/use-media-query.tsx) des Beispiels startet mit `useState(false)` und liest `matchMedia` in einem Effect. Das ergibt auf dem Server false, was in Ordnung ist, aber auch beim ersten Client-Render jedes späteren Mounts. Am Desktop rendert also jeder Mount zuerst den Drawer-Baum und tauscht ihn nach dem Effect aus. Ein Dialog, der schon offen gemountet wird (etwa über eine URL mit `?edit=1`), startet als falsche Komponente.
3. **Ein Remount bei jedem Wechsel.** Dialog und Drawer sind verschiedene Komponententypen. Ein Wechsel unmountet also den einen Baum und mountet den anderen, samt allem State darin.

Fertige Wrapper lösen den ersten Punkt. Stand Oktober 2026 packen [Credenza](https://github.com/redpangilinan/credenza) (MIT) und der [Responsive Dialog von Dice UI](https://diceui.com/docs/components/radix/responsive-dialog) einen Dialog und einen Vaul-Drawer in ein gemeinsames Set von Teilen. Credenza schaltet bei 768px um, und auch sein `useIsMobile` startet mit `false` und kippt in einem Effect. Das zweite Problem bleibt also. Wenn du beim Standard-shadcn/ui bleiben willst, nimm einen davon oder den Wrapper unten, und prüf, was sein Hook zurückgibt, bevor der erste Effect läuft.

## Wo sollte die Umschaltung zwischen Dialog und Drawer sitzen?

In der Root, und nur dort. Die Root liest den Breakpoint, hält den offenen Zustand und sagt jedem Teil, in welchem Modus es ist. Jedes Teil rendert dann das Dialog-Stück oder das Drawer-Stück. Das klappt, weil `Trigger`, `Close`, `Title` und `Description` von Vaul die Teile von Radix Dialog selbst sind. Der Trigger ist also in beiden Modi derselbe Button.

Hier ist dieser Wrapper für einen responsive dialog drawer auf Basis von Dialog und Drawer aus dem Standard-shadcn/ui (Radix-Versionen, Stand Oktober 2026 gegen ihren Quellcode typgeprüft):

```tsx
"use client";

import { createContext, useContext, useState, type ComponentType, type ReactNode } from "react";
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from "@/components/ui/drawer";
import { useIsMobile } from "@/hooks/use-is-mobile";
import { cn } from "@/lib/utils";

const SheetMode = createContext(false);

export function ResponsiveDialog({ children }: { children: ReactNode }) {
  const sheet = useIsMobile();
  // ein open-State über beiden Bäumen, so bleibt der Dialog offen, wenn die Breite 768px überschreitet
  const [open, setOpen] = useState(false);
  return (
    <SheetMode.Provider value={sheet}>
      {sheet ? (
        <Drawer open={open} onOpenChange={setOpen}>{children}</Drawer>
      ) : (
        <Dialog open={open} onOpenChange={setOpen}>{children}</Dialog>
      )}
    </SheetMode.Provider>
  );
}

type PartProps = { asChild?: boolean; className?: string; children?: ReactNode };

// jedes Teil rendert das Dialog- oder das Drawer-Stück, dazu die Klassen, die der Drawer braucht
function part(Card: ComponentType<PartProps>, Sheet: ComponentType<PartProps>, sheetClass = "") {
  return function ResponsivePart({ className, ...props }: PartProps) {
    const sheet = useContext(SheetMode);
    const Part = sheet ? Sheet : Card;
    return <Part className={cn(sheet && sheetClass, className)} {...props} />;
  };
}

const Div = ({ className, children }: PartProps) => <div className={className}>{children}</div>;

export const ResponsiveDialogTrigger = part(DialogTrigger, DrawerTrigger);
export const ResponsiveDialogClose = part(DialogClose, DrawerClose);
export const ResponsiveDialogContent = part(DialogContent, DrawerContent);
export const ResponsiveDialogHeader = part(DialogHeader, DrawerHeader);
export const ResponsiveDialogTitle = part(DialogTitle, DrawerTitle);
export const ResponsiveDialogDescription = part(DialogDescription, DrawerDescription);
// der Drawer hat kein Padding im Body und stapelt seinen Footer von oben nach unten
export const ResponsiveDialogBody = part(Div, Div, "px-4");
export const ResponsiveDialogFooter = part(DialogFooter, DrawerFooter, "flex-col-reverse");
```

Die beiden zusätzlichen Klassen sind wichtig. `DrawerContent` hat kein eigenes Padding, also braucht der Body im Sheet-Modus `px-4`. `DrawerFooter` stapelt seine Buttons von oben nach unten. Ohne `flex-col-reverse` stünde im Sheet Abbrechen über Speichern. Mit der Klasse schreibst du Abbrechen zuerst und Speichern zuletzt, und Speichern landet in der Karte rechts und im Sheet oben. Den Header lässt du in Ruhe: Der Drawer zentriert ihn mit einem Selektor `group-data-[vaul-drawer-direction=bottom]`, der ein einfaches `text-left` überstimmt. Deshalb ändert auch das `className="text-left"` im shadcn-Beispiel nichts.

## Wie vermeidest du ein Hydration-Flackern mit useMediaQuery?

Sorg dafür, dass die Antwort des Servers und die erste Antwort des Clients gleich sind, und halte alles, was vor dem ersten Antippen sichtbar ist, in beiden Modi identisch. Ein geschlossener Dialog rendert nur seinen Trigger, und dieser Trigger ist in beiden Bäumen derselbe Radix-Button. Der Server kann ihn also rendern, ohne die Bildschirmbreite zu kennen. Der Hook muss während der Hydration nur mit dem Server übereinstimmen und bei jedem Render danach stimmen. Genau das macht `useSyncExternalStore`:

```ts
// hooks/use-is-mobile.ts
import { useSyncExternalStore } from "react";

const QUERY = "(max-width: 767.98px)";

function subscribe(onChange: () => void) {
  const list = window.matchMedia(QUERY);
  list.addEventListener("change", onChange);
  return () => list.removeEventListener("change", onChange);
}

export function useIsMobile() {
  return useSyncExternalStore(
    subscribe,
    () => window.matchMedia(QUERY).matches,
    // Server und Hydration-Render gehen von einem Desktop aus
    () => false,
  );
}
```

React ruft die dritte Funktion auf dem Server und während der Hydration auf. Beide stimmen also überein, und es gibt keine Mismatch-Warnung. Jeder Render danach, auch der erste Render eines Dialogs, der später gemountet wird, ruft die zweite Funktion auf und bekommt die echte Antwort, ohne Effect dazwischen. Handys korrigieren die Vermutung direkt nach der Hydration: Die Root tauscht Dialog gegen Drawer unter einem geschlossenen Trigger, und auf dem Bildschirm ändert sich nichts. Wenn du die Sidebar von shadcn installiert hast, exportiert `hooks/use-mobile.ts` bereits ein `useIsMobile` mit `useState` und einem Effect. Gib ihm diesen Funktionsrumpf, und die Sidebar bekommt denselben Fix.

Den Fall „wird später gemountet“ haben wir am Dialog von Wingo UI geprüft, der genau so gebaut ist. Sein Playground lädt die Demo nach der Hydration. Wir haben den Dialog also beim Laden öffnen lassen und die Seite in Chromium über Playwright bei 390px in jedem Animation Frame abgetastet. Ab dem ersten Frame, in dem der Dialog existierte, war er das Sheet, nie die zentrierte Karte, und die Konsole blieb leer.

Was weiterhin flackert, ist sichtbares Markup, das anhand des Hooks verzweigt, etwa ein Formular, das am Desktop inline steht und auf dem Handy hinter einem Button. Schalte so etwas mit CSS-Breakpoints um. Der Guide zu [Mobile-first-React-Komponenten](https://wingo-ui.com/de/blog/mobile-first-react-components) zeigt die richtige und die falsche Version nebeneinander. Der kostenlose Hook [useMediaQuery](https://wingo-ui.com/components/use-media-query) ist dieses Snippet, verallgemeinert auf jede Query, mit einem Parameter `serverFallback`, und er exportiert dasselbe `useIsMobile`. Unser [Tutorial zum SSR-sicheren React use mobile Hook](https://wingo-ui.com/de/blog/react-use-mobile-hook-ssr) vergleicht ihn in einer echten Hydration mit den Versionen mit `useState` und `typeof window`.

## Wie baust du das mit dem Dialog von Wingo UI?

Installier den kostenlosen [Dialog](https://wingo-ui.com/components/dialog) und schreib ihn einmal: Er erledigt die Umschaltung selbst über die Prop `responsive`, die standardmäßig `true` ist. Die Root liest den Breakpoint wie oben über `useSyncExternalStore` und rendert ab 768px einen Radix-Dialog und darunter das Bottom Sheet aus dem [Drawer](https://wingo-ui.com/components/drawer). Jedes Teil (Header, Titel, Beschreibung, Body, Footer) rendert das passende Stück.

### 1. Den Dialog installieren

```bash
npx wingo-ui@latest add dialog
```

Die CLI kopiert `components/ui/dialog.tsx` und alles, was die Datei importiert, etwa den Drawer, den Media-Query-Hook, den Button und die Motion-Helfer. Alles davon ist kostenlos.

### 2. Den Inhalt einmal schreiben

```tsx
"use client";

import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Dialog, DialogClose } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";

type Profile = { name: string; email: string };

export function EditProfileDialog({ profile, onSave }: { profile: Profile; onSave: (next: Profile) => Promise<void> }) {
  const [open, setOpen] = useState(false);
  // der Entwurf liegt über dem Dialog, so bleibt das Getippte erhalten, wenn der offene Dialog 768px überschreitet
  const [draft, setDraft] = useState(profile);
  const [saving, setSaving] = useState(false);

  async function save() {
    setSaving(true);
    try {
      await onSave(draft);
      setOpen(false);
    } finally {
      setSaving(false);
    }
  }

  return (
    <Dialog
      open={open}
      onOpenChange={setOpen}
      trigger={
        <Button variant="soft" onClick={() => setDraft(profile)}>
          Profil bearbeiten
        </Button>
      }
      title="Profil bearbeiten"
      description="Dein Name und deine E-Mail stehen auf jeder Rechnung."
      footer={
        <>
          <DialogClose asChild>
            <Button variant="soft">Abbrechen</Button>
          </DialogClose>
          <Button loading={saving} loadingText="Wird gespeichert" onClick={save}>
            Speichern
          </Button>
        </>
      }
    >
      <div className="flex flex-col gap-4">
        <Input label="Name" autoComplete="name" value={draft.name} onValueChange={(name) => setDraft({ ...draft, name })} />
        <Input label="E-Mail" type="email" autoComplete="email" value={draft.email} onValueChange={(email) => setDraft({ ...draft, email })} />
      </div>
    </Dialog>
  );
}
```

Die Hauptaktion kommt zuletzt in den Footer. Am Desktop landet sie rechts; im Sheet auf dem Handy stapeln sich die Buttons in voller Breite mit 48px Höhe, Speichern oben.

### 3. Bei beiden Breiten öffnen

Probier den Playground unten erst am Laptop aus, dann auf dem Handy oder in einem Fenster, das du vor dem Antippen auf unter 768px verkleinerst.

> Live-Demo (Dialog): Tippe auf Edit: Auf einem breiten Bildschirm skaliert die Karte herein und fokussiert das erste Feld; unter 768px öffnet sich derselbe Dialog als Bottom Sheet, das du zum Schließen nach unten ziehst. Probier sie unter [Dialog](https://wingo-ui.com/components/dialog) aus und installier sie mit `npx wingo-ui@latest add dialog`.

Unter 768px ändert dasselbe JSX neben dem Layout auch das Verhalten:

- **Schließen.** Ein Griff erscheint, das Sheet folgt deinem Finger, und es schließt sich, wenn du es weiter als ein Viertel seiner Höhe ziehst und loslässt oder schnell nach unten wischst.
- **Fokus.** Die Karte am Desktop fokussiert das erste Feld. Das Sheet fokussiert sich selbst, damit die Tastatur nicht das halbe Sheet verdeckt, bevor die Person ein Feld wählt; Tab geht von oben weiter.
- **Scrollen.** Ein langer Body scrollt zuerst und zieht das Sheet erst, wenn er wieder oben ist, und unten bleibt Platz für den Home Indicator.
- **Größe.** `size="full"` ist auf dem Handy ein Sheet mit 92% Höhe und am Desktop eine Karte mit 16px Abstand zu jedem Rand.

`classNames` gelten in beiden Modi, und die Teile des Sheets behalten die Namen `data-slot="dialog-*"`. CSS oder Tests, die auf `[data-slot=dialog-footer]` zielen, treffen also Karte und Sheet gleichermaßen.

### 4. Formular-State über dem Dialog halten

Beim Überschreiten von 768px bleibt der Dialog offen, aber sein Inhalt wird neu gemountet, und State darin wird zurückgesetzt. Wir haben bei 1024px in das Titelfeld des Playgrounds getippt und das Fenster auf 390px verkleinert: Das Sheet war offen, und das Feld stand wieder auf seinem ursprünglichen Wert. Das shadcn-Beispiel und der Wrapper oben verhalten sich genauso, weil die Root den Komponententyp wechselt. In der Praxis passiert das bei einem Tablet, das über den Breakpoint hinweg gedreht wird, etwa ein iPad mini mit 744px Breite im Hochformat und 1133px im Querformat. Der Fix ist der aus Schritt 2: Halte den Entwurf in der Komponente, die den Dialog rendert. Innerhalb des Dialogs liefert dir `useDialog()` das Objekt `{ open, setOpen, sheet }`, falls ein Teil im Sheet ein anderes Layout braucht.

### 5. Abschalten, wo ein Sheet nicht passt

Übergib `responsive={false}`, um auf dem Handy eine zentrierte Karte zu behalten, oder setz es für die ganze App mit `<UIProvider defaults={{ dialog: { responsive: false } }}>`. Der [Alert Dialog](https://wingo-ui.com/components/alert-dialog) wird nie zum Sheet: Auf dem Handy bleibt eine destruktive Frage eine zentrierte Karte mit Buttons in voller Breite übereinander. Er gehört zu [Wingo UI Pro](https://wingo-ui.com/de/pricing).

## Shadcn drawer vs sheet: Was solltest du nehmen?

Der Drawer ist ein Bottom Sheet mit Ziehgeste; das Sheet ist ein Panel, das mit einer CSS-Animation von einer Kante hereingleitet, ohne Ziehgeste. Beide können von jeder Kante aus öffnen, der Drawer standardmäßig von unten und das Sheet von rechts. Nimm den Drawer oder einen responsiven Dialog für Aufgaben und Auswahlen auf dem Handy, und das Sheet für Inhalte, die am Desktop neben der Seite stehen, etwa Navigation, Filter oder einen Warenkorb. Stand Oktober 2026:

| Komponente | Basiert auf | Zum Schließen ziehen | Ab 768px |
| --- | --- | --- | --- |
| [shadcn Drawer](https://ui.shadcn.com/docs/components/radix/drawer) (Radix) | Vaul | Ja | Derselbe Drawer |
| [shadcn Sheet](https://ui.shadcn.com/docs/components/radix/sheet) | Radix Dialog | Nein | Ein Seitenpanel |
| Wingo UI [Dialog](https://wingo-ui.com/components/dialog) | Radix Dialog + Drawer | Auf dem Handy | Eine zentrierte Karte |
| Wingo UI [Drawer](https://wingo-ui.com/components/drawer) | Vaul | Ja | Sheet, Seitenpanel oder Dialog |
| Wingo UI [Sheet](https://wingo-ui.com/components/sheet) | Radix Dialog + motion | Auf Touchscreens | Ein Seitenpanel |

Zum selben Zeitpunkt steht im [README von Vaul](https://github.com/emilkowalski/vaul), dass das Repo nicht mehr gepflegt wird, und laut der [Base-UI-Doku des shadcn Drawer](https://ui.shadcn.com/docs/components/drawer) nutzt er inzwischen Base UI statt Vaul. Der Drawer von Wingo UI und damit der Handy-Modus des Dialogs laufen weiterhin auf Vaul 1.1.2. Snap Points, verschachtelte Sheets und die Tastatur in einem Sheet behandelt unser [Guide zum React Bottom Sheet](https://wingo-ui.com/de/blog/react-bottom-sheet-for-the-web).

Das Sheet deckt den anderen häufigen Fall ab: ein Filter-Panel, das am Desktop von rechts kommt und auf dem Handy über `mobileSide="bottom"` zum Bottom Sheet wird.

> Live-Demo (Sheet): Tippe auf Filters: Auf einem breiten Bildschirm gleitet das Panel von rechts herein; unter 768px steigt es von unten auf, und auf einem Touchscreen ziehst du seinen Griff zum Schließen nach unten. Probier sie unter [Sheet](https://wingo-ui.com/components/sheet) aus und installier sie mit `npx wingo-ui@latest add sheet`.

## Modal vs bottom sheet: Wann sollte ein Handy trotzdem eine zentrierte Karte bekommen?

Nimm ein Bottom Sheet für Aufgaben und Auswahlen, die man abschließt, um dann zur Seite zurückzukehren, und behalte eine zentrierte Karte für eine kurze Frage, die beantwortet werden muss. Die [Human Interface Guidelines von Apple](https://developer.apple.com/design/human-interface-guidelines/sheets) beschreiben ein Sheet als nützlich, um gezielt Informationen abzufragen oder eine einfache Aufgabe zu zeigen, nach der man zur Ausgangsansicht zurückkehrt. Für komplexe oder lange Abläufe raten sie, Alternativen zu Sheets zu prüfen. Unsere Regeln:

- **Formulare, Picker, Filter und Teilen-Optionen:** ein Bottom Sheet. Es öffnet sich dort, wo der Daumen schon ist, und schließt mit derselben Bewegung.
- **Destruktive Bestätigungen** wie „3 Rechnungen löschen?“: ein zentrierter Alert Dialog. Durch das Wegziehen eines Sheets lässt sich eine Frage, die eine Antwort braucht, zu leicht beiseiteschieben.
- **Lange Editoren und mehrstufige Abläufe:** ein Sheet in voller Höhe (`size="full"`) oder eine eigene Seite.

Weitere Muster dieser Art findest du in der Kategorie [Mobile UI in React](https://wingo-ui.com/de/blog/category/mobile-ui).

## Was solltest du als Nächstes tun?

Installier den kostenlosen Dialog mit `npx wingo-ui@latest add dialog`, öffne deine App bei 390px und zieh ein Sheet zu. Wenn du beim Standard-shadcn/ui bleibst, kopier den Wrapper und den Hook mit `useSyncExternalStore` von oben in dein Projekt.

## Komponenten in diesem Artikel

- [Dialog](https://wingo-ui.com/components/dialog) (Free): A React modal dialog for focused tasks: a spring-scaled card on desktop and a bottom sheet on phones, with nesting and scrolling. Installation: `npx wingo-ui@latest add dialog`
- [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`
- [Sheet](https://wingo-ui.com/components/sheet) (Free): A React sheet that slides in from any edge on a spring for filters, a cart, an inspector or phone navigation, swipeable to close on touch. Installation: `npx wingo-ui@latest add sheet`
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) (Free): A React useMediaQuery hook that is SSR-safe, plus hooks for phone widths, touch screens and the current Tailwind breakpoint. Installation: `npx wingo-ui@latest add use-media-query`

## Häufige Fragen

### Wie mache ich einen shadcn Dialog auf dem Handy responsive?

Render unter 768px den Drawer und darüber den Dialog, aus einer Root, die den offenen Zustand hält, und mit Teilen, die das passende Drawer- oder Dialog-Stück wählen. Die shadcn-Doku zeigt eine Version mit zwei Zweigen; ein Wrapper mit Context, Credenza, Dice UI oder der Dialog von Wingo UI halten den Inhalt an einer Stelle.

### Was ist der Unterschied zwischen dem shadcn Drawer und dem Sheet?

Der Drawer ist ein Bottom Sheet, das du zum Schließen ziehen kannst. In der Radix-Version basiert er auf Vaul, in der Base-UI-Version auf Base UI. Das Sheet erweitert den Dialog um ein Panel, das von einer beliebigen Kante hereingleitet; die Radix-Version animiert mit CSS und hat keine Ziehgeste.

### Warum zeigt mein responsiver Dialog kurz die falsche Komponente?

Viele Media-Query-Hooks, auch der im shadcn-Beispiel, starten mit false und korrigieren sich in einem Effect. Der erste Render jedes Mounts arbeitet also mit einer Vermutung. Lies die Query mit useSyncExternalStore und verzweige nie darauf für etwas, das vor dem Öffnen des Dialogs sichtbar ist.

### Ab welcher Breite sollte ein Dialog zum Bottom Sheet werden?

768px, der md-Breakpoint von Tailwind, ist die übliche Wahl: Das shadcn-Beispiel, Credenza und Wingo UI schalten alle dort um. Darunter lässt eine zentrierte Karte wenig Platz über der Bildschirmtastatur und setzt ihre Buttons weit weg vom Daumen.

### Ist der Dialog von Wingo UI kostenlos?

Ja. Dialog, Drawer, Sheet und der Hook useMediaQuery sind kostenlos, und npx wingo-ui@latest add dialog installiert auch den Drawer, den er unter 768px nutzt. Der Alert Dialog, der auf dem Handy eine zentrierte Karte bleibt, gehört zu Pro.

---

Quelle: https://wingo-ui.com/de/blog/shadcn-responsive-dialog-on-mobile
