File Preview
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 a resolved StoredFile). Images prefer a direct url(), 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.
"use client";
import { UploadIcon } from "lucide-react";
import type { ChangeEvent } from "react";
import { useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import { demoFiles } from "@/lib/demo-files";
import { FilePreview } from "@/registry/files-sdk/file-preview/file-preview";
const Example = () => {
const files = demoFiles;
const inputRef = useRef<HTMLInputElement>(null);
// Start on a seeded image so the preview is populated; uploading swaps it out.
const [key, setKey] = useState<string>("photos/sunset.jpg");
const handleChange = async (event: ChangeEvent<HTMLInputElement>) => {
// Capture the element now — React nulls `currentTarget` after the handler's
// synchronous phase, so it's gone by the time the upload below resolves.
const input = event.currentTarget;
const file = input.files?.[0];
if (!file) {
return;
}
const result = await files.upload(`demo/${file.name}`, file, {
contentType: file.type,
});
setKey(result.key);
input.value = "";
};
return (
<div className="flex flex-col gap-4">
<div>
<Button
onClick={() => inputRef.current?.click()}
type="button"
variant="outline"
>
<UploadIcon />
Choose a file
</Button>
<input
accept="image/*,text/*,application/pdf"
aria-label="Choose a file to preview"
className="hidden"
onChange={(event) => void handleChange(event)}
ref={inputRef}
type="file"
/>
</div>
<FilePreview file={key} files={files} />
</div>
);
};
export default Example;
Installation
npx shadcn@latest add https://files-sdk.dev/r/file-preview.jsonUsage
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 StoredFile to skip that round-trip.
Why PDFs use a blob: URL
The 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 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:
<FilePreview
file={fileKey}
files={files}
renderPreview={({ file, src, text }) => {
if (file.type === "application/pdf" && src) {
return <PDFViewer className="h-[640px]" src={src} />;
}
if (file.type === "text/csv" && text) {
return <CsvTable data={text} />;
}
return <p className="text-muted-foreground text-sm">No preview</p>;
}}
/>
The callback receives the resolved StoredFile plus the same src and text the built-in preview would use. PDFs arrive as a blob: URL; other types as a signed or proxy URL.
Props
FilePreviewProps: Cannot read properties of undefined (reading 'ESNext')