# Mobile First React: 9 Regeln für Komponenten mit Code

> Mobile First React Regel für Regel, mit Code: Bottom Sheets statt Popover, 44px Touch-Ziele, Safe Areas, dvh, Leisten über der Tastatur, Tabellen als Karten.

- 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: 21 Min.
- Kanonische URL: https://wingo-ui.com/de/blog/mobile-first-react-components
- Englische Fassung: https://wingo-ui.com/blog/mobile-first-react-components

## Kurz gesagt

Mobile-First-React-Komponenten sind zuerst für einen Daumen auf einem 390px-Screen gebaut und erst danach für die Maus erweitert. Die neun Regeln: Popover, Selects, Menüs und Dialoge öffnen sich unter 768px als Bottom Sheets; Touch-Ziele haben 44px bei groben Zeigern; kein Control versteckt sich hinter Hover; fixierte Leisten bekommen die Safe-Area-Insets als Padding; volle Höhen in dvh statt vh; untere Leisten verschwinden nicht hinter der Bildschirmtastatur; Tabellenzeilen werden Karten; jede Geste hat eine sichtbare Alternative; und das Layout wechselt über CSS-Breakpoints. Media-Query-Hooks nimmst du nur für Verhalten, denn der Server kennt die Bildschirmbreite nicht.

Öffne eine typische React-App auf dem Smartphone, und die Schwachstellen zeigen sich immer an denselben Stellen: ein Dropdown-Menü, das über den Bildschirmrand hängt, ein 32px kleiner Icon-Button, den du anvisieren musst, eine Checkout-Leiste hinter dem Home-Indikator, eine Datentabelle, die du seitwärts scrollst, um eine einzige Zeile zu lesen. Mobile First React heißt: Du löst diese Probleme einmal, in der Komponente selbst. Jeder Screen, der sie nutzt, macht es dann richtig, ohne eine Media Query in der Seite. Das hier ist das Regelwerk, dem wir in Wingo UI folgen, mit Begründung und Code für jede Regel. Wir bauen Wingo UI, sind also nicht neutral; Fakten von außen verlinken auf ihre Quellen und tragen ein Datum.

## Was macht eine React-Komponente mobile-first?

Eine Mobile-First-Komponente ist für einen Daumen auf einem 360 bis 390px breiten Screen mit Bildschirmtastatur entworfen und wird danach für Maus und breite Fenster erweitert. Sie ordnet nicht nur ihr Layout neu an, sie ändert auch ihr Verhalten: welches Overlay sie öffnet, wie groß ihre Trefferfläche ist, was Hover ersetzt und wo sie relativ zu Notch und Tastatur sitzt.

Die neun Regeln in einer Tabelle, danach ein Abschnitt pro Regel:

| Regel | Auf dem Smartphone | Warum |
| --- | --- | --- |
| Overlays | Popover, Selects, Menüs und Dialoge öffnen sich unter 768px als Bottom Sheets | Ein verankertes Panel hat keinen Platz und liegt unter dem Daumen |
| Touch-Ziele | 44px bei groben Zeigern, 8px Abstand | Ein Finger ist ungenauer als ein Cursor |
| Hover | Nichts erscheint nur bei Hover | Touchscreens haben kein Hover |
| Safe Areas | Fixierte Leisten bekommen `env(safe-area-inset-*)` als Padding | Notch und Home-Indikator verdecken Inhalt |
| Höhe | `dvh` oder `svh`, nie `vh` | `100vh` ist höher als der sichtbare Screen |
| Tastatur | Untere Leisten verschwinden oder fahren mit nach oben, Inputs haben 16px Text | Fixierte Leisten landen hinter der Tastatur |
| Tabellen | Zeilen werden Karten | Seitliches Scrollen versteckt den Großteil jeder Zeile |
| Gesten | Jede Geste hat eine sichtbare Alternative | Gesten sind unsichtbar, und nicht jeder kann sie ausführen |
| Layout und Verhalten | CSS-Breakpoints fürs Layout, ein Hook fürs Verhalten | Der Server kennt die Bildschirmbreite nicht |

Wenn du eine React mobile first UI von Grund auf baust, setz jede Regel in jeder Komponente um, bevor du die nächste angehst: Menschen lernen ein Interface über sein häufigstes Muster.

## Warum sollte ein Popover auf dem Smartphone ein Bottom Sheet werden?

Ein Popover, das an einem kleinen Trigger hängt, hat auf einem 390px-Screen keinen guten Platz, während ein Bottom Sheet genau dort aufgeht, wo der Daumen schon ist. Der Finger, der den Trigger angetippt hat, verdeckt ihn, das Panel wird abgeschnitten oder über den Trigger geklappt, und ein Tap knapp daneben schließt es. Ein Sheet nimmt die volle Breite ein, hat Platz für 48px hohe Zeilen, lässt sich wegziehen und hält seine Aktionen im unteren Teil des Screens, den ein Daumen erreicht, ohne dass du umgreifst.

Wir rendern unter 768px standardmäßig ein Bottom Sheet für Popover, Select, Dropdown Menu, Context Menu und Dialog. Die Ausnahmen sind so wichtig wie die Regel:

- Tooltips werden nie zu Sheets. Auf einem Touchscreen hovert nichts, also steht der Text inline oder hinter einem Long Press.
- Alert Dialogs bleiben bei jeder Breite zentriert. Eine kurze Frage, die beantwortet werden muss, liest sich wie ein iOS-Alert.
- Vorschläge unter einem Textfeld bleiben am Feld hängen, weil ein Sheet verdecken würde, was du tippst, wie beim [Mention Input](https://wingo-ui.com/components/mention-input). Die [Combobox](https://wingo-ui.com/components/combobox) geht den anderen Weg: ein Sheet in voller Höhe mit eigenem Suchfeld.

In Wingo UI ist der Wechsel ein `responsive`-Prop, der bei [Popover](https://wingo-ui.com/components/popover), [Select](https://wingo-ui.com/components/select), [Dropdown Menu](https://wingo-ui.com/components/dropdown-menu), [Dialog](https://wingo-ui.com/components/dialog) und den Date Pickern standardmäßig `true` ist. Du schreibst die Komponente einmal:

```tsx
"use client";

import { ChevronDown } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Popover, PopoverClose } from "@/components/ui/popover";

export function PriceFilter({ onApply }: { onApply: () => void }) {
  return (
    <Popover
      trigger={
        <Button variant="soft" rightIcon={<ChevronDown />}>
          Preis
        </Button>
      }
      title="Preis"
      description="Wähl einen Bereich oder gib deinen eigenen ein."
      footer={
        <PopoverClose asChild>
          <Button size="sm" onClick={onApply}>
            Anwenden
          </Button>
        </PopoverClose>
      }
    >
      <p className="text-muted-foreground">Zwischen 500 $ und 1.500 $</p>
    </Popover>
  );
}
```

Auf dem Desktop ist das ein schwebendes Panel mit einem rechtsbündigen Button zum Anwenden. Unter 768px öffnet dasselbe JSX ein Bottom Sheet mit Griff und einem 48px hohen Button in voller Breite. Gib `responsive={false}` mit, wenn ein Panel an einem Feld verankert bleiben muss.

Das Sheet darunter ist der kostenlose [Drawer](https://wingo-ui.com/components/drawer), gebaut auf vaul. Er folgt deinem Finger, schließt sich, wenn du ihn weiter als ein Viertel seiner Höhe herunterziehst und loslässt oder ihn wegschnippst, stoppt an Snap Points und lässt eine lange Liste zuerst scrollen und das Sheet erst ziehen, wenn die Liste wieder oben ist. Beim Öffnen springt der Fokus auf das Sheet selbst statt auf das erste Feld, damit die Tastatur nicht über den halben Inhalt hochfährt. Ab 768px bleibt er ein zentriertes Sheet oder wird je nach `desktop`-Prop ein Seitenpanel oder ein Dialog.

> Live-Demo (Drawer): Tippe auf Filter: Auf dem Smartphone fährt das Sheet hoch und du kannst es nach unten wegziehen; auf einem breiten Screen öffnet es sich hier als Seitenpanel. Probier sie unter [Drawer](https://wingo-ui.com/components/drawer) aus und installier sie mit `npx wingo-ui@latest add drawer`.

Die Demo ist eine Seite mit Inseraten: Filter öffnet unter 768px ein ziehbares Bottom Sheet und auf breiteren Screens ein Panel vom rechten Rand, in beiden Fällen mit der Trefferzahl auf einem fixierten Button.

Zwei Beiträge gehen bei dieser Regel tiefer. Der [Guide zum React Bottom Sheet](https://wingo-ui.com/de/blog/react-bottom-sheet-for-the-web) behandelt Snap Points, Drag to Dismiss, verschachtelte Sheets und die Tastatur, und das [Tutorial zum responsiven shadcn-Dialog](https://wingo-ui.com/de/blog/shadcn-responsive-dialog-on-mobile) baut denselben Wechsel von Karte zu Sheet auf Standard-shadcn/ui.

## Wie wechselst du ohne Hydration-Flackern zwischen Sheet und Dialog?

Nimm CSS-Breakpoints für alles, was beim ersten Paint sichtbar ist, und einen Media-Query-Hook nur für Verhalten, das nach einer Interaktion beginnt, etwa welches Overlay aufgeht. Der Server hat keinen Bildschirm, also muss jeder Hook, der `matchMedia` liest, beim Server-Rendering und bei der Hydration raten. Steuert diese Vermutung das sichtbare Layout, sehen Smartphones zuerst das Desktop-Layout und dann einen Sprung.

Der `use-mobile`-Hook, der mit der Sidebar von shadcn/ui kommt, [startet als `undefined`, gibt `false` zurück und kippt in einem Effect um](https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/new-york-v4/hooks/use-mobile.ts) (Stand Oktober 2026). Unser kostenloser [useMediaQuery](https://wingo-ui.com/components/use-media-query) nutzt denselben Breakpoint von 768px, liest ihn über `useSyncExternalStore` und macht den Server-Wert zu einem Parameter. Das ist der Kern:

```ts
import { useCallback, useSyncExternalStore } from "react";

export function useMediaQuery(query: string, serverFallback = false) {
  const subscribe = useCallback(
    (onChange: () => void) => {
      const list = window.matchMedia(query);
      list.addEventListener("change", onChange);
      return () => list.removeEventListener("change", onChange);
    },
    [query],
  );
  return useSyncExternalStore(
    subscribe,
    () => window.matchMedia(query).matches,
    // der Server und der Hydration-Render
    () => serverFallback,
  );
}

export const useIsMobile = () => useMediaQuery("(max-width: 767.98px)");
```

Beide Hooks sagen auf dem Server `false`. Für Overlays ist das harmlos, denn beim Server-Rendering ist ein Overlay geschlossen: Wenn jemand den Trigger antippt, kennt der Hook längst den echten Wert. Für Inhalte ist es schädlich:

```tsx
// falsch: der Server rendert die Tabelle, Smartphones wechseln nach der Hydration zu Karten
const isMobile = useIsMobile();
return isMobile ? <InvoiceCards rows={rows} /> : <InvoiceTable rows={rows} />;

// richtig: beides steht im HTML und CSS wählt vor dem ersten Paint
return (
  <>
    <InvoiceTable rows={rows} className="hidden md:table" />
    <InvoiceCards rows={rows} className="md:hidden" />
  </>
);
```

Genau diesen Teil machen Apps beim Thema mobile first design React falsch, ohne es zu merken, denn die falsche Version sieht gut aus, wenn du ein Desktop-Fenster nach dem Laden schmal ziehst. Der Sprung zeigt sich erst, wenn ein Smartphone die server-gerenderte Seite lädt. Unser [Guide zu einem SSR-sicheren React use mobile Hook](https://wingo-ui.com/de/blog/react-use-mobile-hook-ssr) testet drei verbreitete Versionen des Hooks durch die Hydration und sortiert, welche Aufgaben zu CSS gehören.

## Wie groß sollten Touch-Ziele sein?

Mach jedes Ziel auf Touchscreens 44 mal 44 CSS-Pixel groß und halte mindestens 8px Abstand zum Nachbarn. Die Standards sind sich bei der genauen Zahl nicht einig; 44px erfüllt das strengste Web-Kriterium (WCAG Stufe AAA) und Apples Standardgröße für Controls:

| Quelle (Stand Oktober 2026) | Größe | Status |
| --- | --- | --- |
| [WCAG 2.2, 2.5.8 Target Size (Minimum)](https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html) | 24 mal 24 CSS px, oder genug Abstand | Stufe AA |
| [WCAG 2.2, 2.5.5 Target Size (Enhanced)](https://www.w3.org/WAI/WCAG22/Understanding/target-size-enhanced.html) | 44 mal 44 CSS px | Stufe AAA |
| [Apple Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/accessibility) | 44 mal 44 pt Standard auf iOS, 28 mal 28 pt Minimum | Richtlinie |
| [Android-Leitfaden zur Barrierefreiheit](https://developer.android.com/guide/topics/ui/accessibility/apps) | 48 mal 48 dp | Empfehlung |

Der Haken ist der Desktop. Ein 44px-Button überall lässt eine Toolbar oder eine Einstellungsseite neben einem Mauszeiger aufgebläht wirken. Tailwind CSS v4 hat eine [`pointer-coarse`-Variante](https://tailwindcss.com/docs/hover-focus-and-other-states), die zu `@media (pointer: coarse)` kompiliert, also kann die Größe dem Eingabegerät folgen statt der Bildschirmbreite. Tablet und Smartphone bekommen beide die größeren Ziele, ein Laptop mit Trackpad nicht.

Für einen Text-Button lautet die Größe `h-10 pointer-coarse:h-11`: 40px mit der Maus, 44px unter dem Finger. Ein Icon-Button, der klein bleiben muss, bekommt stattdessen eine unsichtbare Trefferfläche:

```tsx
import { Ellipsis } from "lucide-react";

// sieht nach 32px aus, nimmt auf Touch 44px große Taps an
export function MoreButton() {
  return (
    <button
      type="button"
      aria-label="Weitere Aktionen"
      className="relative grid size-8 touch-manipulation place-items-center [-webkit-tap-highlight-color:transparent] pointer-coarse:after:absolute pointer-coarse:after:-inset-1.5"
    >
      <Ellipsis className="size-4" />
    </button>
  );
}
```

Der `::after`-Trick legt auf jeder Seite des 32px-Buttons 6px dazu, ohne etwas zu verschieben. `touch-manipulation` schaltet den Doppeltipp-Zoom auf dem Control ab, sodass Taps ankommen, ohne dass der Browser auf einen zweiten wartet, und das transparente Tap-Highlight entfernt den grauen Kasten, den mobile Browser über ein angetipptes Element malen. Beides sitzt in der Komponente selbst, denn eine Komponente, die du in ein anderes Projekt kopierst, bringt dein globales Stylesheet nicht mit. Der [Button](https://wingo-ui.com/components/button) in Wingo UI ist so gebaut: `md` hat 40px mit der Maus und 44px auf Touch, `sm` und `icon-sm` bleiben 32px und bekommen die unsichtbare Trefferfläche, und `lg` hat überall 48px. Der [Guide zur minimalen Touch-Target-Größe](https://wingo-ui.com/de/blog/minimum-touch-target-size-react-tailwind) vergleicht, was WCAG, Apple und Android verlangen, und zeigt, wie du die Trefferflächen testest.

## Warum gehen Hover-Effekte auf Smartphones kaputt?

Touchscreens haben kein Hover, also existiert alles, was nur bei Hover erscheint, auf dem Smartphone nicht. In Tailwind CSS v4 greift die `hover:`-Variante [nur, wenn das primäre Eingabegerät hovern kann](https://tailwindcss.com/docs/upgrade-guide) (`@media (hover: hover)`). Das behebt den alten Hover-Zustand, der nach einem Tap hängen blieb, und es bedeutet auch, dass ein reines Hover-Control auf Touch überhaupt nie auftaucht.

Der klassische Fall ist eine Zeilenaktion, die einblendet, wenn der Zeiger über der Zeile ist. Die Lösung ist eine Fokus-Variante plus eine Variante für grobe Zeiger:

```tsx
import { Ellipsis } from "lucide-react";

export function ClientRow({ name }: { name: string }) {
  return (
    <li className="group/row flex h-12 items-center justify-between gap-3 px-4">
      {name}
      <button
        type="button"
        aria-label={`Aktionen für ${name}`}
        className="grid size-8 place-items-center opacity-0 transition-opacity group-hover/row:opacity-100 group-focus-within/row:opacity-100 pointer-coarse:opacity-100"
      >
        <Ellipsis className="size-4" />
      </button>
    </li>
  );
}
```

Hover blendet ihn für die Maus ein, Fokus für die Tastatur, und bei einem groben Zeiger ist er immer da. Die [Data Table](https://wingo-ui.com/components/data-table) setzt dieselben Klassen auf den Wrapper um den Trigger ihres Zeilenmenüs, dazu `has-[[data-state=open]]:opacity-100`: Ohne das blendet der Trigger aus, während sein Menü offen ist, sobald der Zeiger von der Zeile ins Menü wandert. Die beiden anderen Ersatzformen für Hover sind Tap und Long Press: Eine Hover Card öffnet sich beim ersten Tap als Sheet, ein Tooltip erscheint bei einem Long Press, und ein Context Menu öffnet sich als Action Sheet, wenn du eine Zeile gedrückt hältst.

## Wie hältst du fixierte Leisten von Notch und Home-Indikator fern?

Setz `viewport-fit=cover` und gib dann jedem Element, das an einem Bildschirmrand klebt, `env(safe-area-inset-*)` als Padding, mit einem Minimum für Geräte, die null melden. `cover` skaliert den Viewport auf das ganze Display, Notch und Home-Indikator inklusive, und [MDN empfiehlt dazu dringend](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/meta/name/viewport) die Safe-Area-Inset-Variablen, damit wichtiger Inhalt sichtbar bleibt.

Im Next.js App Router kommt das Viewport-Meta-Tag aus einem `viewport`-Export im Root-Layout:

```tsx
// app/layout.tsx
import type { Viewport } from "next";

export const viewport: Viewport = {
  width: "device-width",
  initialScale: 1,
  viewportFit: "cover",
};
```

Dann paddest du mit einem Arbitrary Value: `pb-[max(env(safe-area-inset-bottom),12px)]`. Ein einfaches `pb-[env(safe-area-inset-bottom)]` lässt Buttons auf Geräten ohne Home-Indikator, wo der Inset 0 ist, direkt am Bildschirmrand kleben; `max()` lässt etwas Luft.

Schwieriger wird es mit zwei Leisten gleichzeitig. Eine Produktseite auf dem Smartphone hat oft unten eine Tab Bar und darüber eine Kaufleiste, und beide wollen die Safe Area. Die [Bottom Tab Bar](https://wingo-ui.com/components/bottom-tab-bar) lässt Platz für den Home-Indikator und schreibt ihre Höhe in eine CSS-Variable `--bottom-bar-offset`. Die [Sticky CTA Bar](https://wingo-ui.com/components/sticky-cta-bar) sitzt bei `bottom: var(--bottom-bar-offset)` und paddet nur den Teil der Safe Area, den die Tab Bar nicht schon abdeckt, also gibt es keine doppelte Lücke:

```tsx
"use client";

import { House, Search, ShoppingBag, UserRound } from "lucide-react";
import { BottomTabBar, type BottomTabBarItem } from "@/components/ui/bottom-tab-bar";
import { StickyCtaBar } from "@/components/ui/sticky-cta-bar";

const tabs: BottomTabBarItem[] = [
  { label: "Start", href: "/", icon: <House /> },
  { label: "Suche", href: "/search", icon: <Search /> },
  { label: "Warenkorb", href: "/cart", icon: <ShoppingBag />, badge: 2 },
  { label: "Konto", href: "/account", icon: <UserRound /> },
];

export function ProductChrome({ onAddToCart }: { onAddToCart: () => Promise<void> }) {
  return (
    <>
      <StickyCtaBar
        price={349}
        compareAtPrice={449}
        locale="en-US"
        actionLabel="In den Warenkorb"
        tone="brand"
        hideFrom="md"
        onAction={onAddToCart}
      />
      <BottomTabBar items={tabs} className="md:hidden" hideOnScroll />
    </>
  );
}
```

Die Tab Bar nimmt 3 bis 5 Tabs, jeder weit größer als 44px. Mit `hideOnScroll` folgt sie deinem Finger auf dem Weg nach unten aus dem Bild und kommt auf dem Weg nach oben zurück, bewegt von einem Motion Value, sodass pro Frame nichts neu rendert. Die Kaufleiste zeigt einen Spinner, solange das Promise von `onAction` läuft, ohne ihre Breite zu ändern. Das [Tutorial zu Tailwind Safe Area Insets](https://wingo-ui.com/de/blog/tailwind-safe-area-insets) behandelt die Utilities, einen FAB über der Tab Bar und wohin Toasts gehören, und der [Guide zur React Bottom Navigation Bar](https://wingo-ui.com/de/blog/react-bottom-navigation-bar) behandelt Badges, die mittlere Aktion und wann eine Sidebar besser passt.

> Live-Demo (Bottom Tab Bar): Tippe auf die Tabs, dann noch einmal auf den aktiven, um nach oben zurückzuspringen; der erhöhte Add-Button öffnet ein Sheet. Probier sie unter [Bottom Tab Bar](https://wingo-ui.com/components/bottom-tab-bar) aus und installier sie mit `npx wingo-ui@latest add bottom-tab-bar`.

Die Demo ist ein kleines CRM mit vier Tabs, einem Zähler für Ungelesenes auf Inbox und einer erhöhten Add-Aktion. Öffne einen Kunden und tippe dann wieder auf Clients: Der Tab springt zurück auf seinen ersten Screen, so wie Tab Bars auf iOS.

## Solltest du auf Mobile 100vh, dvh oder svh nutzen?

Nimm `dvh` für App Shells und Sheets, `svh` für Blöcke, die beim Scrollen ihre Höhe nicht ändern dürfen, und nie `vh`. [MDN definiert](https://developer.mozilla.org/en-US/docs/Web/CSS/length) `vh` als identisch mit `lvh`, also den großen Viewport, gemessen mit eingefahrenen Browserleisten. Solange die Adressleiste sichtbar ist, ist `100vh` (Tailwinds `h-screen`) höher als der sichtbare Bereich, und das untere Ende deines Layouts, oft genau der wichtige Button, liegt unter der Toolbar.

`dvh` folgt den Leisten, wenn sie ein- und ausfahren, und das will eine App Shell in voller Höhe. MDN warnt aber auch, dass dynamische Einheiten Inhalte während des Scrollens in der Größe ändern können, also kann eine Hero-Section in `dvh` zucken; dort ist `svh` die stabile Wahl. Tailwind CSS v4 liefert `h-dvh`, `min-h-dvh`, `h-svh` und `h-lvh`. In Wingo UI nutzt die [App Shell](https://wingo-ui.com/components/app-shell) `min-h-dvh`, und Bottom Sheets sind auf `max-h-[92dvh]` begrenzt, damit ein hohes Sheet über sich immer einen Streifen der Seite sichtbar lässt, egal was die Leisten tun. Der [100vh-Fix für Mobile](https://wingo-ui.com/de/blog/100vh-mobile-dvh-tailwind) vergleicht die drei Einheiten im Detail und baut beide Arten von App Shell in voller Höhe.

## Was passiert mit unteren Leisten, wenn die Bildschirmtastatur aufgeht?

Sie landen hinter der Tastatur, wenn du sie nicht bewegst. Seit [Chrome 108 auf Android](https://developer.chrome.com/blog/viewport-resize-behavior) verkleinert die Tastatur nur den Visual Viewport; das Chrome-Team beschreibt das als Angleichung an Safari auf iOS. Ein Element mit `position: fixed; bottom: 0` richtet sich am Layout Viewport aus, und der reicht weiterhin bis zum unteren Bildschirmrand, hinter die Tastatur.

Es gibt zwei gute Antworten. Blende die Leiste aus, solange ein Feld den Fokus hat; das passt zu einer Checkout-Leiste mit Gutscheinfeld (der Standard der Sticky CTA Bar, `keyboard="hide"`). Oder heb sie um die Höhe an, die die Tastatur verdeckt; das passt zu einem Chat-Composer (`keyboard="lift"`). Die Anhebung kommt aus dem Visual Viewport: Die verdeckte Höhe ist `innerHeight - visualViewport.height - visualViewport.offsetTop`, solange ein Textfeld den Fokus hat. Der Hook [useKeyboardInset](https://wingo-ui.com/components/use-keyboard-inset) (Pro) misst sie höchstens einmal pro Frame und gibt sie als Motion Value heraus, sodass sich die Leiste ohne Re-Render bewegt:

```tsx
"use client";

import { motion, useTransform } from "motion/react";
import { useKeyboardInset } from "@/hooks/use-keyboard-inset";

export function Composer() {
  const { inset } = useKeyboardInset();
  const y = useTransform(inset, (value) => -value);
  return (
    <motion.form style={{ y }} className="fixed inset-x-0 bottom-0 bg-background px-4 pt-2 pb-[max(env(safe-area-inset-bottom),12px)]">
      <input
        aria-label="Nachricht"
        inputMode="text"
        enterKeyHint="send"
        autoComplete="off"
        className="h-11 w-full rounded-full bg-foreground/5 px-4 text-base md:text-sm"
      />
    </motion.form>
  );
}
```

Zwei weitere Tastaturregeln. Gib Texteingaben auf Smartphones 16px Text (`text-base md:text-sm`): iOS Safari zoomt die Seite in jedes fokussierte Feld mit kleinerem Text und zoomt nicht wieder heraus. Und setz `inputMode`, `enterKeyHint` und `autoComplete` auf jedes Feld, damit das Smartphone die richtige Tastatur, die richtige Beschriftung der Eingabetaste und das richtige Autofill zeigt.

## Wie sollte eine Datentabelle auf dem Smartphone funktionieren?

Unter 768px wird jede Zeile zur Karte: das wichtigste Feld als Titel, ein Status-Badge oder ein Betrag in der Ecke, zwei bis vier weitere Felder als Paare aus Label und Wert, und die Zeilenaktionen in einem Menü, das als Sheet aufgeht. Horizontales Scrollen ist nur in Ordnung, wenn es ums Vergleichen von Spalten geht, wie in einer Tabellenkalkulation oder einer Preismatrix. Bei einer Liste von Rechnungen oder Kunden versteckt es den Großteil jeder Zeile und zwingt Leute, in zwei Richtungen zu scrollen, um einen Datensatz zu lesen.

Die [Data Table](https://wingo-ui.com/components/data-table) macht das standardmäßig (`mobileLayout="cards"`). Jede Spalte kann mit einer `mobile`-Rolle sagen, wo sie auf der Karte landet, und wenn du das nicht tust, rät die Tabelle sinnvoll (die erste Spalte ist der Titel, das erste Badge oder sonst die letzte Zahl ist die Meta-Angabe):

```tsx
"use client";

import { Pencil } from "lucide-react";
import { DataTable, createColumns } from "@/components/ui/data-table";

type Invoice = { id: string; client: string; city: string; status: string; amount: number };

const col = createColumns<Invoice>();
const columns = col([
  col.accessor("client", { header: "Kunde", mobile: "title" }),
  col.accessor("city", { header: "Stadt", mobile: "subtitle" }),
  col.accessor("status", {
    header: "Status",
    type: "badge",
    badge: { Paid: { tone: "success" }, Overdue: { tone: "danger" }, Draft: { tone: "neutral" } },
    mobile: "meta",
  }),
  col.accessor("amount", { header: "Betrag", type: "currency" }),
  col.accessor("id", { header: "Rechnung", mobile: "hidden" }),
]);

export function Invoices({ invoices }: { invoices: Invoice[] }) {
  return (
    <DataTable
      data={invoices}
      columns={columns}
      caption="Rechnungen"
      locale="en-US"
      selectionMode="multiple"
      rowActions={(row) => [{ label: "Bearbeiten", icon: <Pencil />, onSelect: () => console.log(row.id) }]}
    />
  );
}
```

Auf dem Smartphone startest du die Auswahl mit einem Long Press auf eine Karte, tippst weitere an, und die Sammelaktionen schweben unten. Sortierung, Filter und das Zeilenmenü öffnen sich als Bottom Sheets, und statt eines Pagers mit winzigen Seitenzahlen hängt „Load more“ weitere Zeilen an. Tabelle und Karten stehen beide im Server-HTML und werden über Breakpoints ein- und ausgeblendet, also sieht ein Smartphone nie zuerst die Tabelle. Das [Tutorial zur TanStack Table v9 Data Table](https://wingo-ui.com/de/blog/tanstack-table-v9-data-table) baut Filter, Sammelaktionen und Server-Pagination direkt auf TanStack, samt Smartphone-Layout.

> Live-Demo (Data Table): Auf dem Smartphone ist jede Zeile eine Karte, die du per Long Press auswählst; auf einem breiten Screen bekommst du die volle Tabelle mit Sortierung und Spaltenmenüs. Probier sie unter [Data Table](https://wingo-ui.com/components/data-table) aus und installier sie mit `npx wingo-ui@latest add data-table`.

Die Demo ist eine Kundenliste mit Suche, Schnellfiltern, Status-Badges und Salden. Unter 768px wird jede Zeile zu einer Karte mit dem Kundennamen als Titel und dem Status in der Ecke. Welche Spalten es auf eine Karte schaffen und wie Sortierung und Sammelaktionen ohne Spaltenköpfe funktionieren, steht im [Guide zu responsiven Tabellen auf Mobile](https://wingo-ui.com/de/blog/responsive-table-mobile-cards).

## Welche Gesten gehören in eine mobile Web-App?

Drag to Dismiss, Pull to Refresh, Swipe-Aktionen auf Zeilen und Halten zum Bestätigen gehören alle dazu, solange jede eine sichtbare Alternative hat und das native Scrollen in Ruhe lässt. Gesten sind unsichtbar, bis sie dir jemand zeigt, und wer eine Tastatur, eine Schaltersteuerung oder einen Screenreader nutzt, kann sie gar nicht ausführen.

- **Immer ein Weg per Button.** Sheets schließen mit Escape, einem Tap daneben oder einem Schließen-Button. Pull to Refresh kommt zusammen mit einem Button zum Aktualisieren. Swipe-Aktionen einer Zeile stehen auch in ihrem Menü.
- **`touch-action` auf ziehbaren Flächen.** `touch-pan-y` auf etwas, das du seitwärts wischst, hält das vertikale Scrollen nativ; `touch-none` ist für Flächen, die du frei ziehst, etwa ein Unterschriftenfeld oder den Thumb eines Sliders.
- **`overscroll-behavior: contain` auf Scroll-Containern.** Es verhindert, dass ein Scroll in einem Sheet auf die Seite überspringt, und hält das browsereigene Pull to Refresh davon ab, gegen deins zu kämpfen.
- **Motion Values statt State.** Ein Drag, der bei jeder Zeigerbewegung `setState` aufruft, rendert in jedem Frame neu. Schreib den Versatz in einen Motion Value und aktualisiere den State erst, wenn die Geste endet.

Sortierbare Listen und Boards sind die Stelle, an der diese Regeln mit dem nativen Scrollen kollidieren; das [Tutorial zu dnd kit auf Mobile](https://wingo-ui.com/de/blog/dnd-kit-mobile-touch-drag) richtet Touch-Sensoren ein, bei denen ein Wischen die Seite trotzdem scrollt.

[Pull To Refresh](https://wingo-ui.com/components/pull-to-refresh) zeigt, wo sich die Regeln biegen. Die Komponente setzt `overscroll-behavior-y: contain` auf ihren Scroller und bewegt den Inhalt mit einem Motion Value unter Gummiband-Widerstand. `touch-action` kann sie nicht nutzen, weil die Liste darin nativ weiterscrollen muss; also hört sie auf `touchmove` mit `passive: false` und bricht den Scroll nur bei einem Zug nach unten ganz oben ab. Sie tickt, sobald der Zug die Auslöseschwelle erreicht, dreht sich, bis dein Promise erledigt ist (mindestens 500ms, damit nichts aufblitzt), und bietet dasselbe Aktualisieren als API für einen Button an (der [Guide zu React Pull to Refresh](https://wingo-ui.com/de/blog/react-pull-to-refresh) behandelt die Indikator-Stile, Barrierefreiheit und lange virtualisierte Listen):

```tsx
"use client";

import { useState, type ReactNode } from "react";
import { RotateCw } from "lucide-react";
import { Button } from "@/components/ui/button";
import { PullToRefresh, type PullToRefreshApi } from "@/components/ui/pull-to-refresh";

export function Feed({ reload, children }: { reload: () => Promise<void>; children: ReactNode }) {
  const [api, setApi] = useState<PullToRefreshApi>();
  return (
    <div className="flex h-dvh flex-col">
      <header className="flex h-14 items-center justify-end px-4">
        <Button variant="ghost" size="icon" radius="full" aria-label="Neu laden" onClick={() => api?.refresh()}>
          <RotateCw />
        </Button>
      </header>
      <PullToRefresh scroll="self" className="min-h-0 flex-1" setApi={setApi} onRefresh={reload}>
        {children}
      </PullToRefresh>
    </div>
  );
}
```

Halten zum Bestätigen ist die Geste für Aktionen, die mehr als einen Tap und weniger als einen Dialog verdienen, etwa einen Kunden zu löschen. Der [Haptic Button](https://wingo-ui.com/components/haptic-button) füllt sich in `mode="hold"`, während du einen Finger, die Maustaste, Space oder Enter hältst, und führt `onConfirm` aus, wenn die Füllung voll ist. Screenreader und Sprachsteuerung, die mit einem einfachen Klick auslösen und nicht halten können, bekommen stattdessen einen Weg in zwei Schritten: Die erste Aktivierung kündigt „Activate again to confirm“ an, und eine zweite innerhalb von 4 Sekunden bestätigt.

```tsx
"use client";

import { Trash2 } from "lucide-react";
import { HapticButton } from "@/components/ui/haptic-button";

export function DeleteClient({ onDelete }: { onDelete: () => Promise<void> }) {
  return (
    <HapticButton tone="danger" variant="soft" mode="hold" fullWidth leftIcon={<Trash2 />} onConfirm={onDelete}>
      Halten zum Löschen
    </HapticButton>
  );
}
```

Auf Android nutzt der Tick `navigator.vibrate`. Safari auf dem iPhone hat keine Vibration API, also weicht der Haptik-Helper auf einen Klick auf einen versteckten nativen Switch aus (`<input type="checkbox" switch>`). Das hat bis iOS 26.4 den System-Tick ausgelöst; ab iOS 26.5 tickt WebKit nur noch, wenn die Person selbst auf den Switch tippt, also trägt auf einem aktuellen iPhone die Füllung das Feedback allein. Desktops und Mausklicks bleiben still. Der [Guide zur Web Vibration API](https://wingo-ui.com/de/blog/web-vibration-api-haptic-feedback) zeigt, was auf einem iPhone noch tickt und wie Leute Haptik abschalten können.

## Kann shadcn/ui Mobile?

Teilweise. Stand Oktober 2026 beschränkt sich shadcn mobile auf eine Komponente und ein Muster. Die [Drawer-Doku](https://ui.shadcn.com/docs/components/drawer) gibt es in einer Base-UI-Version, die inzwischen den Drawer von Base UI statt Vaul nutzt, und in einer Radix-Version, die weiterhin [auf Vaul aufbaut](https://ui.shadcn.com/docs/components/radix/drawer). Beide haben einen Abschnitt „Responsive“, der auf dem Desktop einen `Dialog` und auf dem Smartphone einen `Drawer` rendert; das Beispiel entscheidet mit `useMediaQuery("(min-width: 768px)")` und teilt ein Formular zwischen beiden Zweigen. Die [Popover-Doku](https://ui.shadcn.com/docs/components/popover) beschreibt kein mobiles Verhalten, und Select und Dropdown Menu bleiben bei jeder Breite schwebende Panels, solange du nicht für jedes einen Wrapper schreibst.

Das passt zu einer Bibliothek, die als minimaler Ausgangspunkt ohne feste Vorgaben gedacht ist. shadcn/ui ist kostenlos und MIT-lizenziert, sein GitHub-Repository zeigt rund 125k Sterne (Stand Oktober 2026), und in der Doku wählst du zwischen Primitives von Base UI, Radix und React Aria. Wenn deine App vor allem auf Desktops läuft oder du jeden responsiven Wrapper selbst schreiben und besitzen willst, ist es die bessere Wahl. Unser [Vergleich der besten React-Komponentenbibliotheken für Mobile](https://wingo-ui.com/de/blog/best-react-component-library-for-mobile) prüft shadcn/ui und acht weitere bei 390px.

Wenn die meisten deiner Nutzer über das Smartphone kommen, summieren sich die Wrapper: Jedes Overlay braucht den Wechsel, jedes Control die Größe für grobe Zeiger, jede fixierte Leiste die Safe Area, und jedes davon muss das Hydration-Flackern vermeiden. Wingo UI trifft diese Entscheidungen in jeder Komponente, also sind die Standards schon mobile-first, und du schaltest sie pro Instanz ab.

## Welche Komponenten braucht eine React mobile web app?

Eine React mobile web app braucht meist Overlays, die zu Sheets werden, eine Tab Bar für die Hauptnavigation, eine Aktionsleiste unten auf Produkt- und Checkout-Seiten, Listen mit Swipe-Aktionen und eine Tabelle, die zu Karten wird. Hier ist das Set in Wingo UI (Registry 1.7.0, 326 Einträge, Stand Oktober 2026), mit dem, was jede Komponente auf dem Smartphone tut:

| Komponente | Aufgabe auf dem Smartphone | Zugang |
| --- | --- | --- |
| [Drawer](https://wingo-ui.com/components/drawer) | Das Bottom Sheet: Drag to Dismiss, Snap Points, Verschachtelung | Kostenlos |
| Popover, Select, Dropdown Menu, Dialog | Öffnen sich unter 768px als Bottom Sheets | Kostenlos |
| [Segmented Control](https://wingo-ui.com/components/segmented-control) | Wenige Optionen auf einem Thumb, den du ziehen kannst | Kostenlos |
| [Bottom Tab Bar](https://wingo-ui.com/components/bottom-tab-bar) | 3 bis 5 Hauptziele am Daumen | Pro |
| [Sticky CTA Bar](https://wingo-ui.com/components/sticky-cta-bar) | Preis und Hauptaktion, über der Tab Bar | Pro |
| [Pull To Refresh](https://wingo-ui.com/components/pull-to-refresh) | Eine Liste per Zug oder Button neu laden | Pro |
| [List](https://wingo-ui.com/components/list) | Gruppierte Zeilen mit Swipe-Aktionen und Drag zum Umsortieren | Pro |
| [Data Table](https://wingo-ui.com/components/data-table) | Zeilen als Karten, Long Press zum Auswählen | Pro |
| [Haptic Button](https://wingo-ui.com/components/haptic-button) | Druck-Feedback, Haptik und Halten zum Bestätigen | Pro |
| [Large Title Header](https://wingo-ui.com/components/large-title-header) | Ein großer iOS-Titel, der zu einer Leiste zusammenklappt | Pro |
| [Action Sheet](https://wingo-ui.com/components/action-sheet) | Eine kurze Liste von Aktionen zu einer Sache | Pro |
| [Picker Wheel](https://wingo-ui.com/components/picker-wheel) | iOS-Räder für Uhrzeiten und Daten | Pro |

Die ganze [Kategorie Mobile](https://wingo-ui.com/components/category/mobile) umfasst 15 Komponenten, alle nach diesen Regeln gebaut, mit Unterstützung für reduzierte Bewegung und gestalteten Light- und Dark-Themes.

## Wie testest du Mobile-First-React-Komponenten?

Teste bei 390 mal 844 und 360 mal 780 mit aktiviertem Touch und dann auf einem echten iPhone für die Teile, die Emulatoren falsch machen. Der Device Mode von Chrome stellt den Zeiger auf grob, das reicht, um `pointer-coarse`-Styles und die Sheets zu sehen. Die iOS-Tastatur, den Fokus-Zoom, den Home-Indikator und das Ein- und Ausfahren der Toolbar bildet er nicht nach, dafür brauchst du ein Smartphone.

Für automatische Checks kann Playwright pro Testdatei ein Touch-Smartphone emulieren:

```ts
import { expect, test } from "@playwright/test";

test.use({ viewport: { width: 390, height: 844 }, hasTouch: true, isMobile: true });

test("Filter öffnen auf dem Smartphone als Bottom Sheet", async ({ page }) => {
  await page.goto("/listings");
  await page.getByRole("button", { name: "Filter" }).tap();
  await expect(page.getByRole("dialog", { name: "Filter" })).toBeVisible();
});
```

Mach denselben Durchgang in Light und Dark und mit eingeschalteter reduzierter Bewegung. Ein Sheet-Griff oder eine Haarlinie mit geringer Deckkraft kann in einem Theme klar sein und im anderen verschwinden, und bei reduzierter Bewegung muss jedes Overlay trotzdem auf- und zugehen, nur mit einem kürzeren Slide oder einer Überblendung.

## Welche Guides vertiefen die einzelnen Regeln?

Jeder dieser Beiträge in [Mobile UI in React](https://wingo-ui.com/de/blog/category/mobile-ui) führt eine Regel oder eine Komponente weiter:

- [Responsiver shadcn-Dialog](https://wingo-ui.com/de/blog/shadcn-responsive-dialog-on-mobile): eine zentrierte Karte auf dem Desktop und ein Bottom Sheet auf Smartphones, mit einmal geschriebenem Inhalt.
- [React Bottom Sheet fürs Web](https://wingo-ui.com/de/blog/react-bottom-sheet-for-the-web): Snap Points, Drag to Dismiss, verschachtelte Sheets und die Tastatur auf vaul.
- [Minimale Touch-Target-Größe](https://wingo-ui.com/de/blog/minimum-touch-target-size-react-tailwind): die Zahlen von WCAG, iOS und Android und 44px auf Touch in Tailwind ohne aufgeblähten Desktop.
- [Tailwind Safe Area Insets](https://wingo-ui.com/de/blog/tailwind-safe-area-insets): Leisten unten, FABs und Toasts frei von Notch und Home-Indikator.
- [100vh auf Mobile](https://wingo-ui.com/de/blog/100vh-mobile-dvh-tailwind): wann dvh, svh oder lvh, und App Shells in voller Höhe, die nicht springen.
- [React Bottom Navigation Bar](https://wingo-ui.com/de/blog/react-bottom-navigation-bar): eine Tab Bar mit Badges, mittlerer Aktion und Ausblenden beim Scrollen.
- [React Pull to Refresh](https://wingo-ui.com/de/blog/react-pull-to-refresh): ein eigenes Aktualisieren in einer PWA, das nicht gegen das des Browsers kämpft.
- [Web Vibration API](https://wingo-ui.com/de/blog/web-vibration-api-haptic-feedback): das haptische Feedback, das eine Seite auf Android und iPhone auslösen kann.
- [React PWA Install Prompt](https://wingo-ui.com/de/blog/react-pwa-install-prompt): ein Installationshinweis für Chrome, Edge und iPhone, und wann du besser nicht fragst.
- [Responsive Tabellen auf Mobile](https://wingo-ui.com/de/blog/responsive-table-mobile-cards): wann Zeilen zu Karten werden, welche Spalten überleben, und Sortierung und Sammelaktionen ohne Spaltenköpfe.
- [React Segmented Control](https://wingo-ui.com/de/blog/react-segmented-control): der iOS-Thumb, den du ziehen kannst, Radio-Semantik, und wann Tabs oder eine Toggle Group besser passen.
- [React Wheel Picker](https://wingo-ui.com/de/blog/react-wheel-picker): iOS-Räder für Uhrzeiten und Daten auf einem nativ einrastenden Scroller, mit einem Eingabefeld auf dem Desktop.
- [React Swipe to Delete](https://wingo-ui.com/de/blog/react-swipe-to-delete): iOS-Swipe-Aktionen auf Listenzeilen, ein voller Swipe, der löscht, Rückgängig in einem Toast und ein Menü für alle, die nicht wischen können.
- [React Slide to Confirm](https://wingo-ui.com/de/blog/react-slide-to-confirm): wann ein Slider bei unumkehrbaren Aktionen einen Bestätigungsdialog schlägt, mit Tastaturweg und Statusansagen.
- [React Swipe Cards](https://wingo-ui.com/de/blog/react-swipe-cards): ein Kartenstapel im Tinder-Stil mit Neigung, Stempeln, Rückgängig und Buttons auf Motion Values.
- [Tailwind Touch Manipulation](https://wingo-ui.com/de/blog/tailwind-touch-manipulation): die touch-action-Utility, die jeder Button, Slider, jede Swipe-Zeile und jedes Sheet braucht.
- [iOS Safari, Tastatur und position fixed](https://wingo-ui.com/de/blog/ios-safari-keyboard-position-fixed): die Tastatur mit visualViewport messen und einen Composer oder ein Sheet darüber halten.
- [Eine Web-App, die sich nativ anfühlt](https://wingo-ui.com/de/blog/web-app-that-feels-native): 12 Details, von sofortigen Taps bis zu Action Sheets, großen Titeln und Haptik, mit denen sich eine React-Web-App auf dem Smartphone zu Hause fühlt.
- [React Action Sheet](https://wingo-ui.com/de/blog/react-action-sheet): eine Liste von Aktionen im iOS-Stil mit separatem Abbrechen, asynchronen Zeilen, die einen Spinner zeigen und sich erneut auslösen lassen, und einem kompakten Menü auf dem Desktop.
- [React Floating Action Button](https://wingo-ui.com/de/blog/react-floating-action-button): eine Hauptaktion in der Ecke, ein Speed Dial, ein Label, das beim Scrollen einklappt, und ein Abstand über Tab Bars und dem Home-Indikator.
- [React Swipeable Tabs](https://wingo-ui.com/de/blog/react-swipeable-tabs): Tab-Seiten auf einer CSS-Scroll-Snap-Spur, sodass ein seitliches Wischen den Tab wechselt und ein vertikaler Scroll nie.
- [React Collapsing Header](https://wingo-ui.com/de/blog/react-collapsing-header): der große iOS-Titel und ein Foto-Hero, gesteuert von einem Motion Value, ohne React-Render pro Scroll-Event.

## Wo solltest du anfangen?

Fang mit den Overlays an, denn sie sind der sichtbarste Fehler auf Mobile und am günstigsten zu beheben. Installier das kostenlose Popover mit `npx wingo-ui@latest add popover` (es bringt Drawer, Button und den Media-Query-Hook mit), setz es bei einem Panel ein, das Leute auf dem Smartphone öffnen, und probier es auf deinem eigenen Gerät aus. Tab Bar, Sticky CTA Bar, Pull to Refresh, Haptic Button und Data Table gehören zu [Wingo UI Pro](https://wingo-ui.com/de/pricing).

## Komponenten in diesem Artikel

- [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`
- [Popover](https://wingo-ui.com/components/popover) (Free): A React popover for rich content anchored to a trigger, like a quick filter, form or share panel, that becomes a bottom sheet on phones. Installation: `npx wingo-ui@latest add popover`
- [Bottom Tab Bar](https://wingo-ui.com/components/bottom-tab-bar) (Pro): A React bottom tab bar for phones: 3 to 5 tabs at the thumb with an animated pill, badges, a raised center action and hide on scroll. Installation: `npx wingo-ui@latest add bottom-tab-bar`
- [Pull To Refresh](https://wingo-ui.com/components/pull-to-refresh) (Pro): A React pull to refresh wrapper: pull a list past its top to reload it, with rubberband resistance, iOS spokes or a Material ring, haptics. Installation: `npx wingo-ui@latest add pull-to-refresh`
- [Sticky CTA Bar](https://wingo-ui.com/components/sticky-cta-bar) (Pro): A React sticky CTA bar for product, listing and checkout pages on phones: the price on one side, the main action on the other. Installation: `npx wingo-ui@latest add sticky-cta-bar`
- [Data Table](https://wingo-ui.com/components/data-table) (Pro): A React data table on TanStack Table: search, filters, sorting, bulk actions, pinning, resizing, virtualization and cards on phones. Installation: `npx wingo-ui@latest add data-table`
- [Haptic Button](https://wingo-ui.com/components/haptic-button) (Pro): A React haptic button that feels native: spring press, touch ripple, a haptic tick, a long-press action and a hold-to-confirm mode. Installation: `npx wingo-ui@latest add haptic-button`

## Häufige Fragen

### Was ist eine Mobile-First-React-Komponente?

Eine Komponente, die zuerst für Touch, einen schmalen Screen und eine Bildschirmtastatur entworfen ist und dann für Maus und breite Fenster erweitert wird. Auf dem Smartphone ändert sie nicht nur ihr Layout, sondern auch ihr Verhalten: Sie öffnet ein Bottom Sheet statt eines Popovers, behält 44px große Trefferflächen und hat keine Controls, die nur bei Hover erscheinen.

### Wie groß sollten Buttons in einer mobilen Web-App sein?

WCAG 2.2 verlangt 24 mal 24 CSS-Pixel auf Stufe AA und 44 mal 44 auf Stufe AAA. Apples Richtlinien nutzen 44 mal 44 Punkt als Standardgröße für iOS-Controls, Android empfiehlt 48 mal 48 dp. Eine praktische Regel fürs Web: 44px bei groben Zeigern und 8px Abstand zwischen den Zielen.

### Soll ich einen useIsMobile-Hook oder CSS Media Queries nutzen?

Nimm CSS-Breakpoints für alles, was beim ersten Paint sichtbar ist, denn der Server kennt die Bildschirmbreite nicht und ein Hook liefert dort nur eine Vermutung. Den Hook nimmst du für Verhalten, das erst nach einer Interaktion beginnt, etwa ein Bottom Sheet statt eines Popovers zu öffnen.

### Hat shadcn/ui ein Bottom Sheet für Mobile?

Ja, den Drawer. Stand Oktober 2026 baut seine Base-UI-Version auf dem Drawer von Base UI auf und seine Radix-Version auf Vaul, und die Doku zeigt ein responsives Beispiel, das auf dem Desktop einen Dialog und auf dem Smartphone einen Drawer rendert. Popover, Select und Dropdown Menu wechseln nicht von selbst zu einem Sheet.

### Warum funktionieren Hover-Effekte auf Smartphones nicht?

Touchscreens haben kein Hover, und in Tailwind CSS v4 greift die hover-Variante nur, wenn das primäre Eingabegerät hovern kann. Ein Control, das nur bei Hover erscheint, taucht auf dem Smartphone also nie auf. Zeig es auch bei Fokus und bei groben Zeigern, oder ersetz das Hover durch einen Tap oder einen Long Press.

### Welche mobilen Komponenten von Wingo UI sind kostenlos?

Drawer, Sheet, Popover, Dialog, Select, Dropdown Menu, Segmented Control, Button und der Hook useMediaQuery sind kostenlos, und Popover, Dialog, Select und Dropdown Menu öffnen sich unter 768px standardmäßig als Bottom Sheets. Die 15 Komponenten der Kategorie Mobile (Tab Bar, Sticky CTA Bar, Pull to Refresh, Haptic Button und mehr) und die Data Table gehören zu Pro.

---

Quelle: https://wingo-ui.com/de/blog/mobile-first-react-components
