---
title: File Preview
description: A lazy preview of a single stored file — image, PDF or text — with metadata, resolving bytes only when it mounts.
---

Previews one stored file by key (or its resolved [`FileInfo`](/docs/api/stored-file)). Images prefer a direct [`url()`](/docs/ui/client/react#downloading-vs-linking), falling back to the gateway download proxy when the adapter can't sign; PDFs are downloaded and rendered from a `blob:` URL; text is fetched and shown inline. Bytes load only when the component mounts.

<Component path="file-preview/examples/file-preview-basic" />

## Installation

<ComponentInstall name="file-preview" />

## Usage

```tsx lineNumbers
import { useFiles } from "files-sdk/react";

import { FilePreview } from "@/components/files-sdk/file-preview";

export function Preview({ fileKey }: { fileKey: string }) {
  const files = useFiles({ endpoint: "/api/files" });

  return <FilePreview file={fileKey} files={files} />;
}
```

The footer shows the key, size, type and etag. Pass either a key string (the component `head()`s it for metadata) or an already-resolved `FileInfo` (a `list()` item, say) to skip that round-trip.

## Files without a real content type

Objects stored as `application/octet-stream` (or with no type at all) are common when something other than a browser wrote them. `FilePreview` previews those by their key's extension instead: `.pdf`, raster images (`.png`, `.jpg`, `.gif`, `.webp`, `.avif`, `.bmp`) and text (`.txt`, `.md`, `.csv`, `.log`, `.json`). An extension-less key is checked for the PDF signature with a 5-byte range read, on adapters that support range reads, and previewed as a PDF when it matches. The footer still shows the stored type.

To store the right type in the first place, add the [`contentType()`](/docs/plugins/content-type) plugin on the server. It sniffs the bytes of each upload that goes through your `Files` instance and replaces a generic `application/octet-stream` with the detected type.

## Why PDFs use a `blob:` URL

The [gateway](/docs/ui/server/gateway) forces `Content-Disposition: attachment` on both `url()` and the download proxy — its stored-XSS guard against user-uploaded HTML and script-bearing SVGs. Browsers honor that header even for a document loaded inside an `<object>` tag, so a gateway URL would trigger a download instead of an inline PDF render. `FilePreview` sidesteps this by fetching the bytes through `download()` and previewing a `blob:` URL, which carries no headers — no server configuration needed, and the XSS guard stays intact. If you'd rather serve PDFs inline directly (e.g. for a full-page viewer), return `disposition: "inline"` from your [`authorize`](/docs/ui/server/authorization) scope for the keys and operations where that's safe.

## Custom viewers

Pass `renderPreview` to replace the built-in preview with your own viewer component — a PDF, DOCX or CSV viewer, for example. When it's set, a `src` is resolved for every non-text type (not just images and PDFs), so viewers for formats the built-in preview can't render still get a URL:

```tsx lineNumbers
<FilePreview
  file={fileKey}
  files={files}
  renderPreview={({ type, src, text }) => {
    if (type === "application/pdf" && src) {
      return <PDFViewer className="h-[640px]" src={src} />;
    }
    if (type === "text/csv" && text) {
      return <CsvTable data={text} />;
    }
    return <p className="text-muted-foreground text-sm">No preview</p>;
  }}
/>
```

The callback receives the resolved `FileInfo` plus the same `type`, `src` and `text` the built-in preview would use. `type` is the stored type without parameters (`text/csv`, not `text/csv; charset=utf-8`), or the inferred one for [files without a real content type](#files-without-a-real-content-type), so branch on it rather than `file.contentType`. PDFs arrive as a `blob:` URL; other types as a signed or proxy URL.

## Props

<AutoTypeTable
  path="registry/files-sdk/file-preview/file-preview.tsx"
  name="FilePreviewProps"
/>
