# React File Upload: Drag and Drop, Kamera, Fortschritt

> So baust du einen React File Upload mit Drag and Drop, Einfügen und Handykamera, der Typ und Größe prüft und jede Datei mit Vorschau und Fortschritt zeigt.

- Autor: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Veröffentlicht: 16. Okt. 2026
- Kategorie: [Komponenten-Guides](https://wingo-ui.com/de/blog/category/components)
- Lesezeit: 12 Min.
- Kanonische URL: https://wingo-ui.com/de/blog/react-file-upload
- Englische Fassung: https://wingo-ui.com/blog/react-file-upload

## Kurz gesagt

Ein guter React File Upload nimmt Dateien auf vier Wegen an (Drag and Drop, die Dateiauswahl, Einfügen und die Handykamera), prüft jede Datei vor dem Upload und listet abgelehnte Dateien mit einem Grund auf, statt sie still zu verwerfen. Den Fortschritt misst du mit XMLHttpRequest, weil dessen upload-Objekt Progress-Events feuert und fetch keine hat; über ein AbortSignal stoppen Abbrechen und Entfernen den Request dann wirklich. Der File Upload von Wingo UI erledigt all das mit einer einzigen upload-Funktion als Prop, und der Server muss Typ und Größe trotzdem noch einmal prüfen.

Ein File Input wirkt fertig, sobald sich die Dateiauswahl öffnet. Dann kommen die echten Wünsche: Dateien vom Desktop hineinziehen, einen Screenshot einfügen, auf dem Handy ein Foto machen, den Fortschritt jeder Datei sehen, den Upload der falschen abbrechen. Ein React File Upload muss alle vier Eingabewege abdecken, jede Datei prüfen, bevor sie den Browser verlässt, und klar sagen, warum eine Datei abgelehnt wurde. Dieser Guide zeigt, was dazugehört, wie Fortschritt und Abbrechen unter der Haube funktionieren, und baut ein komplettes Beispiel mit einem Route Handler in Next.js, inklusive der Größenlimits für den Body, an denen Uploads in Produktion scheitern. Die Beispiele nutzen die [File-Upload-Komponente](https://wingo-ui.com/components/file-upload) von Wingo UI, der Bibliothek, die wir bauen. Wir sind also nicht neutral; für Fakten von außen verlinken wir die Quellen.

## Was muss ein React File Upload können?

Vier Eingabewege, drei Prüfungen und einen Status pro Datei. Das ist die Liste, nach der wir gebaut haben, und ein fairer Test für jede Bibliothek oder eine selbst geschriebene Lösung:

- **Drag and Drop.** Eine Datei, die irgendwo über die Seite gezogen wird, soll die Dropzone hervorheben, und ein Drop daneben soll nichts tun. Sonst öffnet der Browser die Datei, und das Formular ist weg.
- **Durchsuchen.** Ein Klick, Enter oder Leertaste auf der Dropzone öffnet die native Dateiauswahl. Drag and Drop ist das Extra. Die Dateiauswahl ist die Basis, die überall funktioniert.
- **Einfügen.** Strg+V oder Cmd+V mit dem Fokus in der Komponente fügt kopierte Dateien und Screenshots hinzu.
- **Kamera.** Auf dem Handy ein Button „Foto aufnehmen“, der direkt die Kamera öffnet.
- **Prüfungen.** Erlaubte Typen, ein Größenlimit und eine maximale Anzahl, dazu deine eigenen Regeln, mit einem sichtbaren Grund für jede abgelehnte Datei.
- **Status.** Eine Vorschau für Bilder, ein Fortschrittsbalken pro Datei, Abbrechen, Wiederholen und Entfernen.

Installier die Komponente und gib ihr eine accept-Liste:

```bash
npx wingo-ui@latest add file-upload
```

```tsx
import { FileUpload } from "@/components/ui/file-upload";

export function Documents() {
  return (
    <FileUpload
      label="Unterlagen zur Immobilie"
      description="PDFs oder Bilder, je bis 10 MB"
      accept="image/*,.pdf"
      maxFiles={8}
    />
  );
}
```

Ohne `upload`-Funktion bleiben die Dateien einfach in der Liste liegen, ohne hochzuladen. Genau das willst du bei einem Formular, das sie nativ absendet (mehr dazu weiter unten).

> Live-Demo (File Upload): Zieh ein PDF oder ein Foto vom Desktop über die Seite, fokussier dann die Dropzone und füg einen Screenshot ein: Jede Datei bekommt eine Zeile mit ihrer Größe und einem Fortschrittsbalken. Probier sie unter [File Upload](https://wingo-ui.com/components/file-upload) aus und installier sie mit `npx wingo-ui@latest add file-upload`.

## Wie funktioniert Drag and Drop in einem React File Upload?

Der Browser feuert `dragenter`, `dragover`, `dragleave` und `drop`, und die abgelegten Dateien kommen in `event.dataTransfer` an. An drei Details scheitert selbst geschriebener Code für React File Upload Drag and Drop immer wieder. Erstens muss `dragover` `preventDefault()` aufrufen, sonst feuert das Drop-Event nie. Zweitens öffnet der Browser bei einem Drop außerhalb deiner Zone die Datei und verlässt die Seite. Drittens feuern `dragenter` und `dragleave` bei jedem Kindelement, über das der Zeiger fährt, erneut, deshalb flackert ein einfacher Boolean. Zähl die Enter- und Leave-Events mit und nimm die Hervorhebung erst zurück, wenn der Zähler wieder bei null ist.

Unsere Komponente löst die ersten beiden Punkte direkt am `window`: Ein Drag mit Dateien hebt jede Dropzone auf der Seite mit „Drop to upload“ hervor, und ein Drop daneben wird abgefangen. Vorher prüft sie, ob `dataTransfer.types` `"Files"` enthält. Wer Text oder einen Link über die Seite zieht, löst also nichts aus. Ordner überspringt sie ebenfalls. Ein abgelegter Ordner erscheint in `dataTransfer.items` als Eintrag, bei dem `webkitGetAsEntry()` `isDirectory` meldet, und `getAsFile()` liefert dir nicht seinen Inhalt. Die Komponente lässt ihn weg, statt eine kaputte Zeile anzulegen.

## Wie zeigst du bei einem React File Upload einen Fortschrittsbalken?

Für einen Upload mit Fortschrittsbalken nimmst du `XMLHttpRequest`. Sein `upload`-Objekt feuert `progress`-Events mit `loaded` und `total` Bytes. `fetch` hat kein Progress-Event für den Upload, ein Upload mit fetch kann also nur einen Spinner zeigen. [Die MDN-Seite zu XMLHttpRequest.upload](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/upload) (geprüft im Oktober 2026) nennt noch zwei Fallen. Listener müssen vor `send()` registriert sein, sonst feuern keine Upload-Events, und Browser brauchen sie oft schon vor `open()`, also setz sie zuerst. Außerdem ist ein Cross-Origin-Request mit Upload-Listener kein einfacher Request („simple request“) mehr, und das kostet einen CORS-Preflight.

Die Komponente nimmt eine einzige Funktion, `upload(file, { signal, onProgress })`. Du rufst `onProgress` mit einem Anteil von 0 bis 1 auf und verbindest `signal` mit dem Request, damit Abbrechen, Entfernen und Unmount den Request stoppen. Speicher das als `upload-file.ts`; die späteren Beispiele importieren es:

```ts
import type { UploadHandler } from "@/components/ui/file-upload";

export const uploadFile: UploadHandler = (file, { signal, onProgress }) =>
  new Promise<{ url: string }>((resolve, reject) => {
    const request = new XMLHttpRequest();
    // Listener vor open(): manche Browser verwerfen sonst die Upload-Events
    request.upload.onprogress = (event) => {
      if (event.lengthComputable) onProgress(event.loaded / event.total);
    };
    request.onload = () => {
      // responseType "json" liefert bei einem kaputten Body null, statt zu werfen
      if (request.status < 300 && request.response) resolve(request.response);
      else reject(new Error(request.response?.error ?? "Der Upload ist fehlgeschlagen"));
    };
    request.onerror = () => reject(new Error("Keine Verbindung"));
    request.onabort = () => reject(new DOMException("Abgebrochen", "AbortError"));
    signal.addEventListener("abort", () => request.abort(), { once: true });
    request.open("POST", "/api/upload");
    request.responseType = "json";
    const body = new FormData();
    body.append("file", file);
    request.send(body);
  });
```

Der `onload`-Handler ist wichtiger, als er aussieht. Ein `JSON.parse` an dieser Stelle wirft innerhalb eines Event-Handlers, wenn der Server mit einer HTML-Fehlerseite antwortet. Das Promise bleibt dann für immer offen, und der Spinner in der Zeile dreht sich endlos. Mit `responseType = "json"` ist ein kaputter Body `null`, und die eigene `error`-Meldung des Servers landet in der Zeile.

Fortschritts-Updates sind auf ein Rendern pro 100ms und Datei gedrosselt, damit eine schnelle Verbindung die Liste nicht hunderte Male pro Sekunde neu rendert. Standardmäßig laden höchstens drei Dateien gleichzeitig hoch (`maxConcurrent`), der Rest wartet, bis er dran ist. Der Wert, mit dem das Promise erfüllt wird, landet in der `response` der Datei. Wird das Promise abgelehnt, bleibt die Datei mit ihrer Fehlermeldung und einem Button zum Wiederholen in der Liste. Setz `autoUpload={false}`, wenn deine Nutzer die Liste erst prüfen sollen: Jede Zeile bekommt dann einen Upload-Button und die Liste einen Button „Upload all“.

## Wie gehst du mit Einfügen und der Handykamera um?

Wer in einem React File Upload einen Screenshot einfügen will, liest `event.clipboardData.files`. Diese Liste enthält kopierte Dateien und Screenshots, die direkt in der Zwischenablage gelandet sind. Der Haken ist, wo du den Listener anhängst. Ein `onPaste` auf der Dropzone verpasst ein Einfügen, während der Fokus auf dem Entfernen-Button einer Zeile sitzt. Deshalb lauscht die Komponente am document und reagiert nur, wenn `document.activeElement` in ihrem Root liegt. `preventDefault()` ruft sie nur auf, wenn die Zwischenablage Dateien enthält. So lässt sich Text in ein anderes Eingabefeld auf der Seite weiter ganz normal einfügen. `paste` ist standardmäßig an; mit `paste={false}` schaltest du es ab.

Die Kamera ist ein zweites, verstecktes Input-Element mit `capture="environment"` und `accept="image/*"`. Laut [der MDN-Seite zum capture-Attribut](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/capture) fordert `environment` die nach außen gerichtete Kamera an und `user` die Frontkamera, und ein Desktop-Browser zeigt stattdessen meist die normale Dateiauswahl. Deshalb ergänzt `camera` seinen Button „Take photo“ nur auf Touchscreens:

```tsx
<FileUpload variant="grid" accept="image/*" maxFiles={12} camera upload={uploadFile} />
```

Das Grid-Layout ist für Fotos gebaut. Die erste Kachel ist das Titelbild, und du sortierst um, indem du ziehst, auf Touchscreens nach 250ms Gedrückthalten, damit die Seite weiter scrollbar bleibt, oder mit Leertaste und den Pfeiltasten. Warum diese Verzögerung wichtig ist, erklärt unser Beitrag über [dnd-kit Drag auf Touchscreens](https://wingo-ui.com/de/blog/dnd-kit-mobile-touch-drag).

> Live-Demo (File Upload): Füg ein paar Fotos hinzu und zieh dann eine Kachel an die erste Stelle, um sie zum Titelbild zu machen; auf dem Handy hältst du die Kachel vor dem Ziehen kurz gedrückt. Probier sie unter [File Upload](https://wingo-ui.com/components/file-upload) aus und installier sie mit `npx wingo-ui@latest add file-upload`.

## Wie validierst du Dateien vor dem Upload?

Für die üblichen Regeln nimmst du `accept`, `maxSize` und `maxFiles`, für eigene `validate`. `maxSize` steht standardmäßig auf 10 MB; setz es auf `0`, wenn du wirklich kein Limit willst. Jede abgelehnte Datei steht unter der Dropzone mit einer Meldung („You can add up to 6 files“) und geht mit einem Code für den Grund (`"type"`, `"size"`, `"count"` oder `"custom"`) an `onReject`. Nichts verschwindet also ohne Erklärung. Die Prüfungen laufen pro Datei in dieser Reihenfolge: Typ, dann Größe, dann dein `validate`, dann die Anzahl. Eine Datei, die an zwei Regeln scheitert, bekommt also nur die erste Meldung. `accept` vergleicht Endungen ohne Rücksicht auf Groß- und Kleinschreibung und versteht MIME-Wildcards wie `image/*`. `acceptsFile(file, accept)` führt dieselbe Prüfung außerhalb der Komponente aus, und `formatFileSize(bytes, locale)` formatiert Größen für deine Meldungen.

Versteh das alles als Feedback. `accept` filtert nur die Dateiauswahl, und jeder kann beliebige Dateien an deinen Endpoint schicken, also prüft der Server noch einmal. Beachte auch: `file.type` kommt auf dem Client aus der Dateiendung und auf dem Server aus dem Multipart-Header. Beides sind Behauptungen des Absenders. Wenn der Typ für die Sicherheit zählt, lies auf dem Server die ersten Bytes der Datei.

## Wie lädst du Dateien zu einem Route Handler in Next.js hoch?

Schick jede Datei an einen Route Handler, prüf sie dort noch einmal und speicher sie unter einem Namen, den du selbst erzeugst. Der Client: Dokumente, eine Prüfung auf Duplikate, ein Limit von 10 MB und ein Callback für abgelehnte Dateien.

```tsx
"use client";

import { useState } from "react";
import { FileUpload, formatFileSize, type UploadFile } from "@/components/ui/file-upload";
import { uploadFile } from "./upload-file";

export function ClaimDocuments() {
  const [files, setFiles] = useState<UploadFile[]>([]);

  return (
    <FileUpload
      label="Unterlagen zum Schadensfall"
      description="PDFs oder Fotos, je bis 10 MB"
      accept="image/*,.pdf"
      maxSize={10 * 1024 * 1024}
      maxFiles={6}
      camera
      value={files}
      onValueChange={setFiles}
      upload={uploadFile}
      validate={(file) =>
        files.some((f) => f.name === file.name && f.size === file.size)
          ? `${file.name} (${formatFileSize(file.size, "de-DE")}) ist schon in der Liste`
          : null
      }
      onReject={(rejections) => console.warn(rejections.map((r) => r.message))}
      locale="de-DE"
    />
  );
}
```

Der Server: ein Route Handler in `app/api/upload/route.ts`, der die Prüfungen wiederholt, bevor er irgendetwas speichert.

```ts
import { saveToStorage } from "@/lib/storage"; // dein eigener Helper für S3, R2 oder die Festplatte

const MAX_SIZE = 10 * 1024 * 1024;
// erlaubte Typen und die Endung, mit der jeder gespeichert wird
const EXTENSIONS: Record<string, string> = {
  "application/pdf": "pdf",
  "image/jpeg": "jpg",
  "image/png": "png",
  "image/webp": "webp",
  "image/heic": "heic",
};

export async function POST(request: Request) {
  const form = await request.formData();
  const file = form.get("file");
  if (!(file instanceof File)) return Response.json({ error: "Keine Datei" }, { status: 400 });
  if (file.size > MAX_SIZE) return Response.json({ error: "Datei zu groß" }, { status: 413 });
  const extension = EXTENSIONS[file.type];
  if (!extension) return Response.json({ error: "Dateityp nicht erlaubt" }, { status: 415 });

  const bytes = new Uint8Array(await file.arrayBuffer());
  const key = `${crypto.randomUUID()}.${extension}`;
  await saveToStorage(key, bytes);
  return Response.json({ url: `/uploads/${key}` });
}
```

Der Key nutzt nie `file.name`. Dieser Name ist, was auch immer der Client geschickt hat, und `../../app.config.js` darf nie in einem Pfad auf der Festplatte landen; die Endung kommt stattdessen aus dem erlaubten Typ. Der Body `{ error }` ist genau das, was die upload-Funktion von oben in der fehlgeschlagenen Zeile anzeigt.

Ein Route Handler ist der richtige Ort für den Upload-Endpoint. Laut [der Next.js-Doku zu Server Actions](https://nextjs.org/docs/app/guides/server-actions) (geprüft im Oktober 2026) sind Requests an Actions standardmäßig auf 1MB begrenzt (`serverActions.bodySizeLimit` hebt das Limit an), und eine Action liefert ohnehin keinen Upload-Fortschritt.

### Warum scheitern Uploads über 4,5 MB auf Vercel?

Das Hosting setzt ein eigenes Limit. [Vercels Seite zu den Limits von Functions](https://vercel.com/docs/functions/limitations) (geprüft im Oktober 2026) begrenzt den Request-Body einer Vercel Function auf 4,5 MB und beantwortet größere Bodys mit einem 413 `FUNCTION_PAYLOAD_TOO_LARGE`. Die Route oben mit ihren 10 MB läuft also lokal und scheitert dort in Produktion. Die Lösung: Halt die Datei von der Function fern. Die Route gibt eine vorsignierte URL deines Storage zurück (S3 und R2 bieten das beide an), und die upload-Funktion schickt die Datei direkt dorthin. Fortschritt und Abbrechen funktionieren weiter, denn es ist immer noch ein `XMLHttpRequest`:

```ts
import type { UploadHandler } from "@/components/ui/file-upload";

export const uploadDirect: UploadHandler = async (file, { signal, onProgress }) => {
  // deine Route: prüft Typ und Größe, gibt eine vorsignierte PUT-URL und die öffentliche URL zurück
  const response = await fetch("/api/upload-url", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ name: file.name, type: file.type, size: file.size }),
    signal,
  });
  if (!response.ok) throw new Error("Der Upload wurde abgelehnt");
  const { uploadUrl, url } = (await response.json()) as { uploadUrl: string; url: string };

  await new Promise<void>((resolve, reject) => {
    const request = new XMLHttpRequest();
    request.upload.onprogress = (event) => {
      if (event.lengthComputable) onProgress(event.loaded / event.total);
    };
    request.onload = () => (request.status < 300 ? resolve() : reject(new Error("Der Upload ist fehlgeschlagen")));
    request.onerror = () => reject(new Error("Keine Verbindung"));
    request.onabort = () => reject(new DOMException("Abgebrochen", "AbortError"));
    signal.addEventListener("abort", () => request.abort(), { once: true });
    request.open("PUT", uploadUrl);
    request.setRequestHeader("Content-Type", file.type);
    request.send(file);
  });
  return { url };
};
```

Der Bucket braucht eine CORS-Regel, die `PUT` und den Header `Content-Type` von deinem Origin erlaubt. Durch den Upload-Listener ist der Request kein einfacher Request mehr, also schickt der Browser vorher einen Preflight. Fehlt die Regel, siehst du nur den Fehler „Keine Verbindung“ aus `onerror`, und die eigentliche Ursache steht nur in der Browser-Konsole. Wenn deine vorsignierte URL einen Content-Type mitsigniert, schick genau diesen, sonst lehnt der Storage die Signatur ab.

### Kannst du die Dateien auch mit einem normalen Formular absenden?

Gib der Komponente einen `name` und lass `upload` weg. Sie hält einen versteckten File Input, dessen Dateien über `DataTransfer` der Liste folgen. Ein nativer Formular-Submit oder `new FormData(form)` schickt also genau die Dateien, die gerade angezeigt werden, und `required` blockiert ein leeres Absenden. Unser [Guide zu React-Formularkomponenten](https://wingo-ui.com/de/blog/react-form-components-guide) behandelt die Anatomie des Felds drumherum: Label, Beschreibung und die animierte Fehlermeldung.

## Wie zeigst du Dateien an, die schon hochgeladen sind?

Übergib sie als `defaultValue` oder `value` mit einer `url` und `status: "done"`, dann erscheinen sie als fertige Zeilen. Wenn die Dateien auf einem eigenen Screen liegen, etwa in einem Kundenordner oder als E-Mail-Anhänge, passt die [File-List-Komponente](https://wingo-ui.com/components/file-list) besser: Zeilen, Vorschaukacheln oder Chips, Download und Löschen mit Bestätigungsdialog, Auswahl und Fortschritt für Dateien, die noch hochladen. Das `UploadFile[]`, das File Upload meldet, passt unverändert in ihre Prop `files`.

## Wie schneidest du ein Foto vor dem Upload zu?

Setz den [Image Cropper](https://wingo-ui.com/components/image-cropper) zwischen Auswahl und Upload. Er nimmt ein `File`-Objekt als `src`, bietet Presets für das Seitenverhältnis, Zoom per Pinch oder Mausrad und Drehung, und gibt dir über `setApi` eine API, deren `toBlob` ein JPEG, PNG oder WebP exportiert:

```tsx
"use client";

import { useState } from "react";
import { Button } from "@/components/ui/button";
import { ImageCropper, type ImageCropperApi } from "@/components/ui/image-cropper";

export function CoverPhoto({ photo, onSave }: { photo: File; onSave: (file: File) => void }) {
  const [api, setApi] = useState<ImageCropperApi | null>(null);

  const save = async () => {
    if (!api) return;
    const blob = await api.toBlob({ type: "image/jpeg", quality: 0.9, maxSize: 1600 });
    onSave(new File([blob], "cover.jpg", { type: "image/jpeg" }));
  };

  return (
    <div className="flex max-w-xl flex-col gap-4">
      <ImageCropper src={photo} defaultAspect={4 / 3} setApi={setApi} />
      <Button onClick={save}>Zuschnitt speichern</Button>
    </div>
  );
}
```

`maxSize` begrenzt die längste Seite und skaliert nie hoch, ein Handyfoto mit 12 Megapixeln geht also nicht in voller Größe über die Leitung. Der Cropper exportiert über ein Canvas, deshalb enthält das Ergebnis keine EXIF-Daten. Damit verschwindet auch der GPS-Standort, den ein Handy in Fotos schreibt. Für ein Profilfoto kombiniert [Avatar Upload](https://wingo-ui.com/components/avatar-upload) Auswahl, runden Zuschnitt und Upload schon in einer Komponente. Der Guide zum [React Image Cropper](https://wingo-ui.com/de/blog/react-image-cropper) behandelt Zoom, Drehung, die Exportgröße und diesen Avatar-Ablauf genauer.

> Live-Demo (Image Cropper): Zieh das Foto unter dem Rahmen zurecht, zoom per Pinch oder Scrollen, stell dann das Verhältnis auf 1:1 und exportier den Zuschnitt. Probier sie unter [Image Cropper](https://wingo-ui.com/components/image-cropper) aus und installier sie mit `npx wingo-ui@latest add image-cropper`.

## Ist der File Upload per Tastatur und auf dem Handy barrierefrei?

Ja. Die Dropzone ist ein echtes fokussierbares Element: Tab erreicht sie, Enter oder Leertaste öffnet die Dateiauswahl, und ein Klick aufs Label fokussiert sie, ohne die Auswahl zu öffnen. Eine Live-Region im Modus „polite“ sagt hochgeladene, fehlgeschlagene, abgelehnte und entfernte Dateien an, dazu jedes Umsortieren im Grid. Auf dem Handy steht in der Dropzone „Tap to choose files“, die Entfernen-Buttons auf Fotos sind immer sichtbar, statt erst beim Hover zu erscheinen, und jede Aktion hat eine Trefferfläche von 44px, die Größe, die unser [Guide zu Touch-Zielen](https://wingo-ui.com/de/blog/minimum-touch-target-size-react-tailwind) erklärt.

## Solltest du stattdessen einen shadcn File Upload nehmen?

Stand Oktober 2026 hat shadcn/ui keine eigene Dropzone. Seine [Attachment-Komponente](https://ui.shadcn.com/docs/components/attachment) zeigt eine Datei mit ihrem Upload-Status und Aktionen an, aber Dateiauswahl, Drop-Handling und die Berechnung des Fortschritts baust du selbst. Ein shadcn File Upload heißt also meist: ein Item aus einer Community-Registry. [Der File Upload von Dice UI](https://diceui.com/docs/components/file-upload) ist ein zusammensetzbares Set aus Teilen (Dropzone, Trigger, Liste, Vorschau pro Item, Fortschritt pro Item) mit Validierung und wird mit `npx shadcn@latest add @diceui/file-upload` installiert. Wenn du diese Teile selbst zusammensetzen willst, ist das eine gute Wahl. Der File Upload von Wingo UI ist ein einziges Element, das du über Props konfigurierst, mit vier Layouts (list, grid, avatar, compact), Einfügen, dem Kamera-Button, Umsortieren im Grid, einer Upload-Warteschlange und einem Fallback für native Formulare, alles eingebaut.

## Nächster Schritt

Installier alles mit `npx wingo-ui@latest add file-upload file-list image-cropper`. Alle drei gehören zu Wingo UI Pro ([Preise](https://wingo-ui.com/de/pricing)); die [Doku zu File Upload](https://wingo-ui.com/components/file-upload) hat den Playground und jede Prop. Weitere Komponenten-Guides findest du in der [Kategorie Komponenten](https://wingo-ui.com/de/blog/category/components).

## Komponenten in diesem Artikel

- [File Upload](https://wingo-ui.com/components/file-upload) (Pro): A React file upload with drag and drop, paste and camera capture: validation, previews, per-file progress, and list, grid or avatar layouts. Installation: `npx wingo-ui@latest add file-upload`
- [File List](https://wingo-ui.com/components/file-list) (Pro): A React file list for attachments and documents: rows, preview tiles or chips with type icons, thumbnails, upload progress and file actions. Installation: `npx wingo-ui@latest add file-list`
- [Image Cropper](https://wingo-ui.com/components/image-cropper) (Pro): A React image cropper for photos before upload: aspect presets, pinch or wheel zoom, rotation, a thirds grid and a JPEG, PNG or WebP export. Installation: `npx wingo-ui@latest add image-cropper`

## Häufige Fragen

### Wie zeige ich in React den Upload-Fortschritt an?

Schick die Datei mit XMLHttpRequest und werte request.upload.onprogress aus, das die geladenen und die gesamten Bytes meldet. fetch hat kein Progress-Event für den Upload, ein Upload mit fetch kann also nur einen Spinner zeigen. Registriere den Listener vor open(), denn laut MDN lassen Browser Upload-Events oft aus, wenn der Listener erst später dazukommt.

### Wie füge ich Bilder in einen React File Upload ein?

Reagier auf das paste-Event und lies event.clipboardData.files, dort liegen kopierte Dateien und Screenshots. Häng den Listener ans document und reagiere nur, wenn document.activeElement in deiner Komponente liegt. So klappt das Einfügen, egal ob der Fokus auf der Dropzone oder auf einem Button in einer Zeile sitzt.

### Wie öffne ich auf dem Handy die Kamera aus einem File Input?

Setz capture="environment" für die Rückkamera oder capture="user" für die Frontkamera, zusammen mit accept="image/*". Laut MDN zeigen Desktop-Browser stattdessen meist die normale Dateiauswahl, biete die Kamera auf Touchscreens also als zusätzlichen Button an.

### Reicht das accept-Attribut, um Uploads zu validieren?

Nein. accept filtert nur die Dateiauswahl; eine hineingezogene oder eingefügte Datei umgeht es, und jeder kann beliebige Dateien an deinen Endpoint schicken. Prüf Typ und Größe in der Komponente für schnelles Feedback und noch einmal auf dem Server, bevor du die Datei speicherst.

### Gibt es eine File-Upload-Komponente für shadcn?

Stand Oktober 2026 hat shadcn/ui eine Attachment-Komponente, die Dateien und ihren Upload-Status anzeigt, aber keine Dropzone und keine Dateiauswahl. Community-Registries wie Dice UI füllen die Lücke, und der File Upload von Wingo UI lässt sich genauso als Quellcode installieren, mit npx wingo-ui@latest add file-upload.

### Warum scheitern große Uploads auf Vercel?

Vercel Functions nehmen Request-Bodys bis 4,5 MB an und beantworten alles darüber mit einem Fehler 413. Für größere Dateien gibt deine Route eine vorsignierte Storage-URL zurück, und der Browser schickt die Datei direkt in den Storage.

---

Quelle: https://wingo-ui.com/de/blog/react-file-upload
