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/dropzone—FileCardreads theFileStatusandUploadStatustypes from the Dropzone component.
Installation
npx shadcn@latest add @shelf-ui/file-cardpnpm dlx shadcn@latest add @shelf-ui/file-cardyarn dlx shadcn@latest add @shelf-ui/file-cardbunx shadcn@latest add @shelf-ui/file-cardUsage
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."
"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.
"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"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"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"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"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"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
| Prop | Type | Required | Description |
|---|---|---|---|
fileStatus | FileStatus | Yes | A single entry from dropzone.fileStatuses. |
onRemove | () => void | No | Called when the remove button is clicked. Omit to hide the remove action entirely. |
onRetry | () => void | No | Called when the retry button is clicked. Only shown when status === "error" and canRetry is true. |
canRetry | boolean | No | Pass dropzone.canRetry(status.id). Controls whether the retry button renders. |
FileCardPreview
| Prop | Type | Default | Description |
|---|---|---|---|
showImageThumbnail | boolean | false | Render an <img> thumbnail for image files instead of the default status icon. Object URL lifecycle is managed automatically. |
renderPreview | (file: File, status: UploadStatus) => ReactNode | — | Full control over the preview content. Return null/undefined to use the built-in fallback for that file. |
FileCardInfo
| Prop | Type | Default | Description |
|---|---|---|---|
maxNameLength | number | 28 | Characters 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
| Prop | Type | Default | Description |
|---|---|---|---|
maxLength | number | 28 | Characters before the filename is truncated with an ellipsis (extension preserved). |
FileCardFileSize
| Prop | Type | Default | Description |
|---|---|---|---|
| — | — | — | Standalone formatted file size text. No props beyond standard span attributes. |
FileCardStatusText
| Prop | Type | Default | Description |
|---|---|---|---|
| — | — | — | Inline status text ("Uploading...", "Completed", or the error message). No props beyond standard span attributes. |
FileCardTypeIcon / FileCardStatusIcon
| Component | Description |
|---|---|
FileCardTypeIcon | Standalone file type icon (PDF, Video, Image, Archive, etc) based on the file type. |
FileCardStatusIcon | Standalone status icon (Loader, Check, Alert) based on the upload status. |
FileCardCopyButton / FileCardDownloadButton
| Component | Description |
|---|---|
FileCardCopyButton | Copies the URL to the clipboard. Automatically hides if status.result is empty. |
FileCardDownloadButton | Provides a direct download link. Automatically hides if status.result is empty. |
FileCardActions
| Prop | Type | Default | Description |
|---|---|---|---|
hideRetry | boolean | false | Hide the retry button even when the file is retryable. |
hideRemove | boolean | false | Hide the remove button. |
children | ReactNode | — | Extra actions rendered after retry/remove. |
FileCardRetryButton / FileCardRemoveButton
| Prop | Type | Default | Description |
|---|---|---|---|
...props | ButtonProps | — | Standalone action buttons for custom layouts. Both accept the same props as the Shelf UI Button component. |
Utilities
| Export | Description |
|---|---|
useFileCardContext | Reads the current FileCard context for custom subcomponents. |
useFileObjectUrl | Creates 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. FileCardProgressusesrole="progressbar"witharia-valuetextset to"indeterminate","100%", or"error"depending on status.- Error messages in
FileCardInfoare plain text, so they're picked up by the same ARIA live region wiring asDropzoneMessagewhen both are on the page.
Performance
- Object URLs are handled internally.
FileCardPreviewusesuseFileObjectUrlto create the URL viauseEffectand revoke it automatically on unmount or whenever thefilereference changes. You don't need to callURL.createObjectURLorURL.revokeObjectURLyourself forshowImageThumbnail. - The URL is only created when it's actually needed — if
showImageThumbnailis off, or the file isn't an image, orrenderPreviewreturns custom content, no object URL is created at all. FileCardInfo's derived strings are memoized againstfile.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 inuseMemo, but that only prevents re-renders ifonRemove/onRetryare 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 withuseCallback(or curry an id-bound function withuseMemo) 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