Uploads

FileCard

A composable row for displaying a single file's upload status, progress, and actions.

FileCard pairs with Dropzone — map it over dropzone.fileStatuses to build a queue. There is no separate list component to install; a plain .map() is the queue.

Dependencies

  • lucide-react — Icons for status, retry, and remove actions.
  • @/components/shelf-ui/dropzoneFileCard reads the FileStatus and UploadStatus types from the Dropzone component.

Installation

terminal
npx shadcn@latest add @shelf-ui/file-card
terminal
pnpm dlx shadcn@latest add @shelf-ui/file-card
terminal
yarn dlx shadcn@latest add @shelf-ui/file-card
terminal
bunx shadcn@latest add @shelf-ui/file-card

Usage

FileCard is a compound component — the root provides context, and you choose which pieces to render and in what order. Map it over dropzone.fileStatuses to render the queue; there's no separate list component because the loop itself is the entire "queue."

Uploaded Files

1/3
quarterly-report.pdf2.4 MB·Uploading
mountain-view.jpg
mountain-view.jpg850.0 KB·Completed
contract-v2.docx1.2 MB·Failed
"use client";import { Trash2 } from "lucide-react";import { useState } from "react";import type { FileStatus } from "@/components/shelf-ui/dropzone";import {  FileCard,  FileCardActions,  FileCardCopyButton,  FileCardDownloadButton,  FileCardFileName,  FileCardFileSize,  FileCardPreview,  FileCardProgress,  FileCardStatusText,} from "@/components/shelf-ui/file-card";import { Badge } from "@/components/ui/badge";import { Button } from "@/components/ui/button";function useSampleFileStatuses(): FileStatus[] {  const [statuses] = useState<FileStatus[]>(() => [    {      id: "1",      file: new File(        [new ArrayBuffer(1024 * 1024 * 2.4)],        "quarterly-report.pdf",        { type: "application/pdf" }      ),      status: "pending",      tries: 1,    },    {      id: "2",      file: new File([new ArrayBuffer(1024 * 850)], "mountain-view.jpg", {        type: "image/jpeg",      }),      status: "success",      tries: 1,      result:        "https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?q=80&w=800",    },    {      id: "3",      file: new File([new ArrayBuffer(1024 * 1024 * 1.2)], "contract-v2.docx", {        type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",      }),      status: "error",      tries: 2,      error: "File type not allowed.",    },  ]);  return statuses;}export function FileCardBasicDemo() {  const fileStatuses = useSampleFileStatuses();  const successCount = fileStatuses.filter(    (s) => s.status === "success"  ).length;  return (    <div className="w-full max-w-md space-y-4">      {/* Header */}      <div className="flex items-center justify-between">        <div className="flex items-center gap-2">          <h3 className="font-semibold text-sm">Uploaded Files</h3>          <Badge className="h-5 px-1.5 text-[10px]" variant="secondary">            {successCount}/{fileStatuses.length}          </Badge>        </div>        <Button          className="h-7 gap-1.5 text-xs"          onClick={() => {}}          size="sm"          variant="ghost"        >          <Trash2 className="size-3" />          Clear all        </Button>      </div>      {/* File list */}      <div className="space-y-2">        {fileStatuses.map((status) => (          <FileCard            canRetry={status.tries < 3}            className="group transition-all duration-200 hover:border-primary/40 hover:shadow-sm"            fileStatus={status}            key={status.id}            onRemove={() => {}}            onRetry={() => {}}          >            <div className="flex w-full items-center gap-3">              <FileCardPreview showImageThumbnail />              {/* Name + status inline */}              <div className="flex min-w-0 flex-1 flex-col gap-0.5">                <FileCardFileName />                <span className="flex items-center gap-1.5">                  <FileCardFileSize />                  <span className="text-muted-foreground/40 text-xs">·</span>                  <FileCardStatusText />                </span>              </div>              <FileCardActions>                <FileCardCopyButton />                <FileCardDownloadButton />              </FileCardActions>            </div>            <FileCardProgress />          </FileCard>        ))}      </div>    </div>  );}

For large queues (dozens of files re-rendering frequently), wrap onRemove/onRetry in useCallback in the parent. FileCard's internal context is memoized, but inline arrow functions created fresh on every render still defeat that memoization. See Performance.

Composition

FileCard (root + context provider)
├── FileCardPreview         (thumbnail or type/status icon)
├── FileCardInfo             (classic layout: filename + size/error)
│   ├── FileCardFileName     (standalone filename)
│   └── FileCardFileSize     (standalone formatted size)
├── FileCardProgress         (progress bar — pending / success / error)
├── FileCardActions          (retry/remove button group)
│   ├── FileCardCopyButton     (copies result URL on success)
│   └── FileCardDownloadButton (download link on success)
├── FileCardStatusText       (inline status: "Uploading / Completed / Failed")
├── FileCardTypeIcon         (standalone file type icon: PDF, Video, Image, etc)
├── FileCardStatusIcon       (standalone status icon: Loader, Check, Alert)
├── FileCardRetryButton      (standalone retry button)
└── FileCardRemoveButton     (standalone remove button)

Every subcomponent is optional and order is up to you. Omit FileCardProgress for a compact row, drop FileCardPreview if thumbnails aren't useful for your file types, or use the granular components (FileCardFileName, FileCardFileSize, etc.) to build entirely custom layouts.

With a progress bar

The default queue row pairs FileCardPreview, FileCardInfo, FileCardProgress, and FileCardActions.

Uploaded Files

1/3
quarterly-report.pdf2.4 MB·Uploading
mountain-view.jpg
mountain-view.jpg850.0 KB·Completed
contract-v2.docx1.2 MB·Failed
"use client";import { Trash2 } from "lucide-react";import { useState } from "react";import type { FileStatus } from "@/components/shelf-ui/dropzone";import {  FileCard,  FileCardActions,  FileCardCopyButton,  FileCardDownloadButton,  FileCardFileName,  FileCardFileSize,  FileCardPreview,  FileCardProgress,  FileCardStatusText,} from "@/components/shelf-ui/file-card";import { Badge } from "@/components/ui/badge";import { Button } from "@/components/ui/button";function useSampleFileStatuses(): FileStatus[] {  const [statuses] = useState<FileStatus[]>(() => [    {      id: "1",      file: new File(        [new ArrayBuffer(1024 * 1024 * 2.4)],        "quarterly-report.pdf",        { type: "application/pdf" }      ),      status: "pending",      tries: 1,    },    {      id: "2",      file: new File([new ArrayBuffer(1024 * 850)], "mountain-view.jpg", {        type: "image/jpeg",      }),      status: "success",      tries: 1,      result:        "https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?q=80&w=800",    },    {      id: "3",      file: new File([new ArrayBuffer(1024 * 1024 * 1.2)], "contract-v2.docx", {        type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",      }),      status: "error",      tries: 2,      error: "File type not allowed.",    },  ]);  return statuses;}export function FileCardBasicDemo() {  const fileStatuses = useSampleFileStatuses();  const successCount = fileStatuses.filter(    (s) => s.status === "success"  ).length;  return (    <div className="w-full max-w-md space-y-4">      {/* Header */}      <div className="flex items-center justify-between">        <div className="flex items-center gap-2">          <h3 className="font-semibold text-sm">Uploaded Files</h3>          <Badge className="h-5 px-1.5 text-[10px]" variant="secondary">            {successCount}/{fileStatuses.length}          </Badge>        </div>        <Button          className="h-7 gap-1.5 text-xs"          onClick={() => {}}          size="sm"          variant="ghost"        >          <Trash2 className="size-3" />          Clear all        </Button>      </div>      {/* File list */}      <div className="space-y-2">        {fileStatuses.map((status) => (          <FileCard            canRetry={status.tries < 3}            className="group transition-all duration-200 hover:border-primary/40 hover:shadow-sm"            fileStatus={status}            key={status.id}            onRemove={() => {}}            onRetry={() => {}}          >            <div className="flex w-full items-center gap-3">              <FileCardPreview showImageThumbnail />              {/* Name + status inline */}              <div className="flex min-w-0 flex-1 flex-col gap-0.5">                <FileCardFileName />                <span className="flex items-center gap-1.5">                  <FileCardFileSize />                  <span className="text-muted-foreground/40 text-xs">·</span>                  <FileCardStatusText />                </span>              </div>              <FileCardActions>                <FileCardCopyButton />                <FileCardDownloadButton />              </FileCardActions>            </div>            <FileCardProgress />          </FileCard>        ))}      </div>    </div>  );}
<FileCard fileStatus={status} onRemove={...} onRetry={...} canRetry={...}>
  <div className="flex w-full items-center gap-3">
    <FileCardPreview />
    <FileCardInfo />
    <FileCardActions />
  </div>
  <FileCardProgress />
</FileCard>

Compact, no progress bar

A slimmer layout suitable for sidebars or tight spaces. This layout relies on granular subcomponents like FileCardFileName and FileCardStatusText to fit more metadata inline without needing multiple rows.

Attachments

3 files
resume.pdf1.8 MB·Completed
cover-letter.pdf400.0 KB·Completed
portfolio-2024.pdf8.5 MB·Completed
"use client";import { CheckCircle2, Download, ExternalLink, FileText } from "lucide-react";import { useState } from "react";import type { FileStatus } from "@/components/shelf-ui/dropzone";import {  FileCard,  FileCardActions,  FileCardFileName,  FileCardFileSize,  FileCardStatusText,} from "@/components/shelf-ui/file-card";import { Badge } from "@/components/ui/badge";import { Button } from "@/components/ui/button";function useSampleFileStatuses(): FileStatus[] {  const [statuses] = useState<FileStatus[]>(() => [    {      id: "1",      file: new File([new ArrayBuffer(1024 * 1024 * 1.8)], "resume.pdf", {        type: "application/pdf",      }),      status: "success",      tries: 1,      result: "https://example.com/resume.pdf",    },    {      id: "2",      file: new File([new ArrayBuffer(1024 * 400)], "cover-letter.pdf", {        type: "application/pdf",      }),      status: "success",      tries: 1,      result: "https://example.com/cover-letter.pdf",    },    {      id: "3",      file: new File(        [new ArrayBuffer(1024 * 1024 * 8.5)],        "portfolio-2024.pdf",        { type: "application/pdf" }      ),      status: "success",      tries: 1,      result: "https://example.com/portfolio.pdf",    },  ]);  return statuses;}// Compact layout with enhanced UI — file icons with subtle animations,// action buttons with hover states, and a polished document list appearanceexport function FileCardCompactDemo() {  const fileStatuses = useSampleFileStatuses();  return (    <div className="w-full max-w-md space-y-4">      {/* Header */}      <div className="flex items-center justify-between">        <div className="flex items-center gap-2">          <h3 className="font-semibold text-sm">Attachments</h3>          <Badge className="h-5 px-1.5 text-[10px]" variant="secondary">            {fileStatuses.length} files          </Badge>        </div>        <Button          className="h-7 gap-1.5 text-muted-foreground text-xs hover:text-foreground"          onClick={() => {}}          size="sm"          variant="ghost"        >          <Download className="size-3" />          Download all        </Button>      </div>      {/* Compact file list */}      <div className="space-y-2">        {fileStatuses.map((status) => (          <FileCard            className="group relative overflow-hidden transition-all duration-200 hover:border-primary/50 hover:shadow-sm"            fileStatus={status}            key={status.id}            onRemove={() => {}}          >            <div className="flex w-full items-center gap-3">              {/* Subtle gradient overlay on hover */}              <div className="pointer-events-none absolute inset-0 bg-gradient-to-r from-primary/0 via-primary/0 to-primary/5 opacity-0 transition-opacity duration-300 group-hover:opacity-100" />              {/* File icon with background */}              <div className="relative flex size-9 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-red-50 to-red-100/50 shadow-sm transition-transform duration-200 group-hover:scale-105 dark:from-red-950/30 dark:to-red-900/20">                <FileText className="size-4 text-red-500" />                {/* Success indicator */}                <div className="absolute -top-0.5 -right-0.5 flex size-3.5 items-center justify-center rounded-full bg-background shadow-sm">                  <CheckCircle2 className="size-2.5 text-emerald-500" />                </div>              </div>              {/* File info */}              <div className="flex min-w-0 flex-1 flex-col gap-0.5">                <FileCardFileName />                <span className="flex items-center gap-1.5">                  <FileCardFileSize />                  <span className="text-muted-foreground/40 text-xs">·</span>                  <FileCardStatusText />                </span>              </div>              {/* Actions with enhanced styling */}              <FileCardActions className="relative" hideRetry>                <Button                  aria-label="View file"                  className="size-7 opacity-0 transition-opacity duration-200 group-hover:opacity-100"                  onClick={() => {}}                  size="icon"                  variant="ghost"                >                  <ExternalLink className="size-3.5" />                </Button>              </FileCardActions>            </div>          </FileCard>        ))}      </div>    </div>  );}

Image thumbnails

FileCardPreview shows a generic status icon by default. Pass showImageThumbnail to render an actual image preview for image files. The underlying object URL is created and revoked automatically — no manual cleanup needed.

Image attachments

3 images
cover-image.svg529 B
gallery-tile.svg529 B
thumbnail.svgImage dimensions are too small.
"use client";import { useState } from "react";import type { FileStatus } from "@/components/shelf-ui/dropzone";import {  FileCard,  FileCardActions,  FileCardInfo,  FileCardPreview,} from "@/components/shelf-ui/file-card";import { Badge } from "@/components/ui/badge";function createSvgFile(name: string, colors: [string, string]) {  const [from, to] = colors;  const svg = `    <svg width="160" height="160" viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg">      <defs>        <linearGradient id="g" x1="0" x2="1" y1="0" y2="1">          <stop stop-color="${from}" />          <stop offset="1" stop-color="${to}" />        </linearGradient>      </defs>      <rect width="160" height="160" rx="24" fill="url(#g)" />      <circle cx="116" cy="42" r="18" fill="white" fill-opacity=".75" />      <path d="M22 124 58 82l28 30 18-20 34 32Z" fill="white" fill-opacity=".7" />    </svg>  `;  return new File([svg], name, { type: "image/svg+xml" });}function useImageStatuses(): FileStatus[] {  const [statuses] = useState<FileStatus[]>(() => [    {      id: "cover",      file: createSvgFile("cover-image.svg", ["#f59e0b", "#ef4444"]),      status: "success",      tries: 1,    },    {      id: "gallery",      file: createSvgFile("gallery-tile.svg", ["#14b8a6", "#2563eb"]),      status: "pending",      tries: 1,    },    {      error: "Image dimensions are too small.",      file: createSvgFile("thumbnail.svg", ["#a855f7", "#ec4899"]),      id: "thumbnail",      status: "error",      tries: 2,    },  ]);  return statuses;}export function FileCardImageThumbnailDemo() {  const fileStatuses = useImageStatuses();  return (    <div className="flex w-full max-w-md flex-col gap-3">      <div className="flex items-center justify-between">        <h3 className="font-semibold text-sm">Image attachments</h3>        <Badge variant="secondary">{fileStatuses.length} images</Badge>      </div>      <div className="flex flex-col gap-2">        {fileStatuses.map((status) => (          <FileCard            canRetry={status.tries < 3}            fileStatus={status}            key={status.id}            onRemove={() => {}}            onRetry={() => {}}          >            <div className="flex w-full items-center gap-3">              <FileCardPreview showImageThumbnail />              <FileCardInfo />              <FileCardActions />            </div>          </FileCard>        ))}      </div>    </div>  );}
<FileCard fileStatus={status} onRemove={...} onRetry={...} canRetry={...}>
  <div className="flex w-full items-center gap-3">
    <FileCardPreview showImageThumbnail />
    <FileCardInfo />
    <FileCardActions />
  </div>
</FileCard>

Custom preview (file-type icons, video posters, etc.)

For file types beyond images — PDFs, videos, archives — pass renderPreview to take full control of what's shown. Return null or undefined to fall back to showImageThumbnail or the default status icon for that file.

Mixed file types

Custom previews
brand-guidelines.pdf3.5 MB
launch-cut.mp445.0 MB
assets.zipArchive contains an unsupported file.
"use client";import { Archive, FileText, Film, ImageIcon } from "lucide-react";import { useState } from "react";import type { FileStatus, UploadStatus } from "@/components/shelf-ui/dropzone";import {  FileCard,  FileCardActions,  FileCardInfo,  FileCardPreview,} from "@/components/shelf-ui/file-card";import { Badge } from "@/components/ui/badge";function useMixedFileStatuses(): FileStatus[] {  const [statuses] = useState<FileStatus[]>(() => [    {      file: new File(        [new ArrayBuffer(1024 * 1024 * 3.5)],        "brand-guidelines.pdf",        {          type: "application/pdf",        }      ),      id: "brand-guidelines",      result: "/uploads/brand-guidelines.pdf",      status: "success",      tries: 1,    },    {      file: new File([new ArrayBuffer(1024 * 1024 * 45)], "launch-cut.mp4", {        type: "video/mp4",      }),      id: "launch-cut",      status: "pending",      tries: 1,    },    {      error: "Archive contains an unsupported file.",      file: new File([new ArrayBuffer(1024 * 1024 * 1.2)], "assets.zip", {        type: "application/zip",      }),      id: "assets",      status: "error",      tries: 2,    },  ]);  return statuses;}function FileTypePreview(file: File, status: UploadStatus) {  const iconClassName =    status === "error" ? "size-4 text-destructive" : "size-4 text-foreground";  if (file.type === "application/pdf") {    return <FileText className={iconClassName} />;  }  if (file.type.startsWith("video/")) {    return <Film className={iconClassName} />;  }  if (file.type.includes("zip")) {    return <Archive className={iconClassName} />;  }  return <ImageIcon className={iconClassName} />;}export function FileCardCustomPreviewDemo() {  const fileStatuses = useMixedFileStatuses();  return (    <div className="flex w-full max-w-md flex-col gap-3">      <div className="flex items-center justify-between">        <h3 className="font-semibold text-sm">Mixed file types</h3>        <Badge variant="secondary">Custom previews</Badge>      </div>      <div className="flex flex-col gap-2">        {fileStatuses.map((status) => (          <FileCard            canRetry={status.tries < 3}            fileStatus={status}            key={status.id}            onRemove={() => {}}            onRetry={() => {}}          >            <div className="flex w-full items-center gap-3">              <FileCardPreview                className="bg-muted/70"                renderPreview={FileTypePreview}              />              <FileCardInfo />              <FileCardActions />            </div>          </FileCard>        ))}      </div>    </div>  );}
<FileCardPreview
  renderPreview={(file, status) => {
    if (file.type === "application/pdf") return <FileTextIcon />;
    if (file.type.startsWith("video/")) return <FilmIcon />;
    return null;
  }}
/>

Use this recipe when one queue mixes many file types. If your queue is documents-only, the Patterns page shows the lighter option: omit FileCardPreview entirely.

Extra actions

FileCardActions accepts children, rendered after the built-in retry/remove buttons — useful for a "view file" link or "copy URL" button once a file has uploaded successfully.

Uploaded documents

2 ready
signed-contract.pdf2.1 MB
invoice-1042.pdf340.0 KB
"use client";import { Check, Clipboard, ExternalLink } from "lucide-react";import { useState } from "react";import type { FileStatus } from "@/components/shelf-ui/dropzone";import {  FileCard,  FileCardActions,  FileCardInfo,  FileCardPreview,} from "@/components/shelf-ui/file-card";import { Badge } from "@/components/ui/badge";import { Button } from "@/components/ui/button";function useUploadedFiles(): FileStatus<string>[] {  const [statuses] = useState<FileStatus<string>[]>(() => [    {      file: new File(        [new ArrayBuffer(1024 * 1024 * 2.1)],        "signed-contract.pdf",        {          type: "application/pdf",        }      ),      id: "signed-contract",      result: "https://example.com/files/signed-contract.pdf",      status: "success",      tries: 1,    },    {      file: new File([new ArrayBuffer(1024 * 340)], "invoice-1042.pdf", {        type: "application/pdf",      }),      id: "invoice",      result: "https://example.com/files/invoice-1042.pdf",      status: "success",      tries: 1,    },  ]);  return statuses;}export function FileCardExtraActionsDemo() {  const fileStatuses = useUploadedFiles();  const [copiedId, setCopiedId] = useState<string | null>(null);  return (    <div className="flex w-full max-w-md flex-col gap-3">      <div className="flex items-center justify-between">        <h3 className="font-semibold text-sm">Uploaded documents</h3>        <Badge>{fileStatuses.length} ready</Badge>      </div>      <div className="flex flex-col gap-2">        {fileStatuses.map((status) => (          <FileCard fileStatus={status} key={status.id} onRemove={() => {}}>            <div className="flex w-full items-center gap-3">              <FileCardPreview />              <FileCardInfo />              <FileCardActions hideRetry>                <Button                  aria-label={`Copy link for ${status.file.name}`}                  onClick={() => setCopiedId(status.id)}                  size="icon-sm"                  type="button"                  variant="ghost"                >                  {copiedId === status.id ? (                    <Check className="size-3.5" />                  ) : (                    <Clipboard className="size-3.5" />                  )}                </Button>                <Button                  aria-label={`Open ${status.file.name}`}                  size="icon-sm"                  type="button"                  variant="ghost"                >                  <ExternalLink className="size-3.5" />                </Button>              </FileCardActions>            </div>          </FileCard>        ))}      </div>    </div>  );}
<FileCardActions>
  {status.status === "success" && (
    <Button size="icon" variant="ghost" aria-label="Copy link" onClick={() => copyToClipboard(status.result)}>
      <LinkIcon className="h-4 w-4" />
    </Button>
  )}
</FileCardActions>

Standalone action buttons

If you need total control over the action layout, use FileCardRetryButton and FileCardRemoveButton directly anywhere inside FileCard. They read onRetry and onRemove from context and accept the same props as Shelf UI's Button.

Needs attention

2 failed
board-deck.pdfConnection timed out.
budget.xlsxUpload token expired.
"use client";import { AlertCircle, RotateCcw, X } from "lucide-react";import { useState } from "react";import type { FileStatus } from "@/components/shelf-ui/dropzone";import {  FileCard,  FileCardInfo,  FileCardRemoveButton,  FileCardRetryButton,} from "@/components/shelf-ui/file-card";import { Badge } from "@/components/ui/badge";function useFailedFiles(): FileStatus[] {  const [statuses] = useState<FileStatus[]>(() => [    {      error: "Connection timed out.",      file: new File([new ArrayBuffer(1024 * 1024 * 15.2)], "board-deck.pdf", {        type: "application/pdf",      }),      id: "board-deck",      status: "error",      tries: 1,    },    {      error: "Upload token expired.",      file: new File([new ArrayBuffer(1024 * 510)], "budget.xlsx", {        type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",      }),      id: "budget",      status: "error",      tries: 2,    },  ]);  return statuses;}export function FileCardStandaloneActionsDemo() {  const fileStatuses = useFailedFiles();  return (    <div className="flex w-full max-w-md flex-col gap-3">      <div className="flex items-center justify-between">        <h3 className="font-semibold text-sm">Needs attention</h3>        <Badge variant="destructive">{fileStatuses.length} failed</Badge>      </div>      <div className="flex flex-col gap-2">        {fileStatuses.map((status) => (          <FileCard            canRetry={status.tries < 3}            fileStatus={status}            key={status.id}            onRemove={() => {}}            onRetry={() => {}}          >            <div className="flex w-full items-center gap-3">              <div className="flex size-10 shrink-0 items-center justify-center rounded-md bg-destructive/10">                <AlertCircle className="size-4 text-destructive" />              </div>              <FileCardInfo maxNameLength={34} />              <div className="ml-auto flex shrink-0 items-center gap-1">                <FileCardRetryButton size="icon-sm">                  <RotateCcw className="size-3.5" />                </FileCardRetryButton>                <FileCardRemoveButton size="icon-sm">                  <X className="size-3.5" />                </FileCardRemoveButton>              </div>            </div>          </FileCard>        ))}      </div>    </div>  );}
<FileCard fileStatus={status} onRemove={...} onRetry={...} canRetry={...}>
  <div className="flex w-full items-center gap-3">
    <FileCardInfo />
    <div className="ml-auto flex shrink-0 items-center gap-1">
      {status.status === "error" && <FileCardRetryButton size="icon-sm" />}
      <FileCardRemoveButton size="icon-sm" />
    </div>
  </div>
</FileCard>

API Reference

FileCard

PropTypeRequiredDescription
fileStatusFileStatusYesA single entry from dropzone.fileStatuses.
onRemove() => voidNoCalled when the remove button is clicked. Omit to hide the remove action entirely.
onRetry() => voidNoCalled when the retry button is clicked. Only shown when status === "error" and canRetry is true.
canRetrybooleanNoPass dropzone.canRetry(status.id). Controls whether the retry button renders.

FileCardPreview

PropTypeDefaultDescription
showImageThumbnailbooleanfalseRender an <img> thumbnail for image files instead of the default status icon. Object URL lifecycle is managed automatically.
renderPreview(file: File, status: UploadStatus) => ReactNodeFull control over the preview content. Return null/undefined to use the built-in fallback for that file.

FileCardInfo

PropTypeDefaultDescription
maxNameLengthnumber28Characters before the filename is truncated with an ellipsis (extension preserved).

Shows the formatted file size by default, or the error message when status === "error".

FileCardProgress

No props beyond standard div attributes. Reads status from context — renders an indeterminate animated bar while "pending", a filled bar on "success", and a destructive-colored bar on "error".

FileCardFileName

PropTypeDefaultDescription
maxLengthnumber28Characters before the filename is truncated with an ellipsis (extension preserved).

FileCardFileSize

PropTypeDefaultDescription
Standalone formatted file size text. No props beyond standard span attributes.

FileCardStatusText

PropTypeDefaultDescription
Inline status text ("Uploading...", "Completed", or the error message). No props beyond standard span attributes.

FileCardTypeIcon / FileCardStatusIcon

ComponentDescription
FileCardTypeIconStandalone file type icon (PDF, Video, Image, Archive, etc) based on the file type.
FileCardStatusIconStandalone status icon (Loader, Check, Alert) based on the upload status.

FileCardCopyButton / FileCardDownloadButton

ComponentDescription
FileCardCopyButtonCopies the URL to the clipboard. Automatically hides if status.result is empty.
FileCardDownloadButtonProvides a direct download link. Automatically hides if status.result is empty.

FileCardActions

PropTypeDefaultDescription
hideRetrybooleanfalseHide the retry button even when the file is retryable.
hideRemovebooleanfalseHide the remove button.
childrenReactNodeExtra actions rendered after retry/remove.

FileCardRetryButton / FileCardRemoveButton

PropTypeDefaultDescription
...propsButtonPropsStandalone action buttons for custom layouts. Both accept the same props as the Shelf UI Button component.

Utilities

ExportDescription
useFileCardContextReads the current FileCard context for custom subcomponents.
useFileObjectUrlCreates and revokes an object URL for a File; useful for custom previews outside FileCardPreview.

Accessibility

  • Retry and remove buttons have aria-labels ("Retry upload", "Remove file") since they're icon-only.
  • FileCardProgress uses role="progressbar" with aria-valuetext set to "indeterminate", "100%", or "error" depending on status.
  • Error messages in FileCardInfo are plain text, so they're picked up by the same ARIA live region wiring as DropzoneMessage when both are on the page.

Performance

  • Object URLs are handled internally. FileCardPreview uses useFileObjectUrl to create the URL via useEffect and revoke it automatically on unmount or whenever the file reference changes. You don't need to call URL.createObjectURL or URL.revokeObjectURL yourself for showImageThumbnail.
  • The URL is only created when it's actually needed — if showImageThumbnail is off, or the file isn't an image, or renderPreview returns custom content, no object URL is created at all.
  • FileCardInfo's derived strings are memoized against file.name/file.size, so re-renders triggered by unrelated state (e.g. a sibling row's status changing) don't re-run the truncation or byte-formatting logic.
  • Context memoization has a limit. FileCard's context value is wrapped in useMemo, but that only prevents re-renders if onRemove/onRetry are stable references. If you pass () => dropzone.onRemove(status.id) inline inside a .map(), that's a new function every render. For small queues this is invisible; for large or frequently-updating queues, wrap the dropzone's callbacks once with useCallback (or curry an id-bound function with useMemo) in the parent component.
  • Large lists — if you're rendering hundreds of files, consider a virtualization library (e.g. react-window) around the .map(), the same guidance as in the Dropzone docs.

Last Updated on Jun 22, 2026

On this page