jattac.libs.web.zest-file-upload
v0.3.1
Published
A production-ready React file upload component with drag-drop, camera capture, image crop, compression, progress tracking, and mobile-optimised UI. CSS is auto-injected — no separate import required.
Maintainers
Readme
ZestFileUpload
A production-ready React file upload component with drag-and-drop, camera capture, image crop, compression, progress tracking, multi-file parallel uploads, and a mobile-optimised UI. CSS is auto-injected — no separate stylesheet import required.
Installation
npm install jattac.libs.web.zest-file-uploadPeer dependencies
npm install react react-dom react-iconsQuick Start
import { ZestFileUpload } from "jattac.libs.web.zest-file-upload";
function App() {
return (
<ZestFileUpload
label="Upload a file"
onFileSelect={async (file) => {
if (!file) return;
const formData = new FormData();
formData.append("file", file);
await fetch("/api/upload", { method: "POST", body: formData });
}}
/>
);
}Features
- Drag and drop — full drop-zone with visual feedback
- File picker — click to browse
- Camera capture — take a photo directly (mobile and desktop)
- Image crop — built-in crop UI after camera capture
- Image compression — auto-compress camera photos before upload
- Progress bar — caller-controlled upload progress
- Multi-file mode — parallel real-time uploads with per-file status
- Validation — file type and size enforcement
- Error handling — inline validation errors and upload errors with retry
- Accessibility — keyboard navigation, ARIA roles, live regions
- i18n ready — every label is overridable
- Custom icons — swap any icon with your own
- SSR safe — works with Next.js and other SSR frameworks
- Zero CSS import — styles are injected automatically
Props
Core props
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | — | Required. Label shown above the upload zone |
| onFileSelect | (file: File \| null) => Promise<void> | — | Required. Called when a file is selected (single mode) or per-file fallback in multi mode |
| disabled | boolean | false | Disables the component |
| accept | string | — | Accepted file types (e.g. "image/*", ".pdf,.docx", "image/png,image/jpeg") |
| maxFileSize | number | — | Max file size in bytes |
Upload feedback
| Prop | Type | Default | Description |
|---|---|---|---|
| progressPercentage | number | 0 | Upload progress 0–100 (single mode) |
| hideCompletionMessage | boolean | false | Hide the "Upload complete" message |
| successTimeout | number | 5000 | Ms before success state resets to idle |
| onError | (error: string) => void | — | Called on validation errors |
Camera
| Prop | Type | Default | Description |
|---|---|---|---|
| capture | "user" \| "environment" | — | Preferred camera facing mode |
Multi-file mode
| Prop | Type | Default | Description |
|---|---|---|---|
| multiple | boolean | false | Enable multi-file mode |
| onFilesSelect | (files: File[]) => Promise<void> | — | Batch callback — called once with all valid files. If omitted, onFileSelect is called concurrently per file |
| filesProgress | Record<FileEntryId, number> | — | Per-file progress 0–100, keyed by FileEntryId |
| maxFiles | number | — | Maximum number of files in the queue |
| multiLabels | Partial<MultiFileUploadLabels> | — | Override multi-mode UI labels |
Customisation
| Prop | Type | Default | Description |
|---|---|---|---|
| labels | Partial<FileUploadLabels> | — | Override any UI label string |
| icons | Partial<FileUploadIcons> | — | Override any icon with a custom ReactNode |
| errorBoundary | boolean | false | Wrap with a React error boundary |
Ref methods
const ref = useRef<FileUploadRef>(null);
ref.current.clearFile(); // Reset single-mode state + call onFileSelect(null)
ref.current.clearAll(); // Clear the multi-file queue (no-op in single mode)Examples
Single file upload with progress
import { useState } from "react";
import { ZestFileUpload } from "jattac.libs.web.zest-file-upload";
function UploadWithProgress() {
const [progress, setProgress] = useState(0);
const handleUpload = async (file: File | null) => {
if (!file) return;
const formData = new FormData();
formData.append("file", file);
await new Promise<void>((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
setProgress(Math.round((e.loaded / e.total) * 100));
}
};
xhr.onload = () => (xhr.status >= 200 && xhr.status < 300 ? resolve() : reject());
xhr.onerror = reject;
xhr.open("POST", "/api/upload");
xhr.send(formData);
});
setProgress(0);
};
return (
<ZestFileUpload
label="Upload document"
onFileSelect={handleUpload}
progressPercentage={progress}
accept=".pdf,.docx,.xlsx"
maxFileSize={10 * 1024 * 1024} // 10 MB
/>
);
}Validate file type and size
<ZestFileUpload
label="Upload image"
onFileSelect={async (file) => {
if (!file) return;
await uploadToServer(file);
}}
accept="image/png,image/jpeg,image/webp"
maxFileSize={5 * 1024 * 1024} // 5 MB
onError={(message) => console.error("Validation failed:", message)}
/>Camera capture with image crop
<ZestFileUpload
label="Take a photo"
onFileSelect={async (file) => {
if (!file) return;
// file is already compressed (JPEG, max 1920×1080 by default)
await uploadPhoto(file);
}}
accept="image/*"
capture="environment" // prefer rear camera
/>Programmatic clear via ref
import { useRef } from "react";
import { ZestFileUpload, FileUploadRef } from "jattac.libs.web.zest-file-upload";
function FormWithUpload() {
const uploadRef = useRef<FileUploadRef>(null);
const handleFormReset = () => {
uploadRef.current?.clearFile();
};
return (
<form onReset={handleFormReset}>
<ZestFileUpload
ref={uploadRef}
label="Attachment"
onFileSelect={async (file) => { /* ... */ }}
/>
<button type="reset">Clear</button>
</form>
);
}Multi-file upload — batch callback
All files are uploaded in a single call. Ideal when your API accepts an array or multipart/form-data with multiple fields.
import { ZestFileUpload } from "jattac.libs.web.zest-file-upload";
function BatchUpload() {
const handleFiles = async (files: File[]) => {
const formData = new FormData();
files.forEach((file) => formData.append("files", file));
const res = await fetch("/api/upload/batch", {
method: "POST",
body: formData,
});
if (!res.ok) throw new Error("Upload failed");
};
return (
<ZestFileUpload
label="Upload documents"
multiple
onFileSelect={async () => {}} // required prop — unused when onFilesSelect is provided
onFilesSelect={handleFiles}
accept=".pdf,.docx"
maxFiles={10}
maxFileSize={20 * 1024 * 1024} // 20 MB per file
/>
);
}Multi-file upload — concurrent per-file with real-time progress
Each file uploads in parallel. Progress is tracked per-file via FileEntryId.
import { useState } from "react";
import { ZestFileUpload, FileEntryId } from "jattac.libs.web.zest-file-upload";
function ParallelUpload() {
const [progress, setProgress] = useState<Record<FileEntryId, number>>({});
// Called concurrently for each file when onFilesSelect is not provided
const handleFile = async (file: File) => {
// Note: in multi mode without onFilesSelect, onFileSelect receives individual files
// You won't have the FileEntryId here — use onFilesSelect for per-file progress
await uploadFile(file);
};
return (
<ZestFileUpload
label="Upload files"
multiple
onFileSelect={handleFile}
accept="image/*"
maxFiles={20}
/>
);
}Tip: For per-file progress bars, use
onFilesSelect+filesProgresstogether (see next example).
Multi-file upload — per-file progress bars
import { useState } from "react";
import {
ZestFileUpload,
FileEntryId,
} from "jattac.libs.web.zest-file-upload";
function UploadWithPerFileProgress() {
const [progress, setProgress] = useState<Record<FileEntryId, number>>({});
const handleFiles = async (files: File[]) => {
// Upload each file independently with XHR for progress events
await Promise.allSettled(
files.map(async (file, i) => {
// In a real app you'd receive FileEntryId from the queue —
// here we use a simplified index-based key for illustration.
// Wire real IDs by using a custom upload manager.
const formData = new FormData();
formData.append("file", file);
await fetch("/api/upload", { method: "POST", body: formData });
})
);
};
return (
<ZestFileUpload
label="Upload images"
multiple
onFileSelect={async () => {}}
onFilesSelect={handleFiles}
filesProgress={progress}
accept="image/*"
maxFiles={5}
/>
);
}Multi-file with ref — clear all programmatically
import { useRef } from "react";
import { ZestFileUpload, FileUploadRef } from "jattac.libs.web.zest-file-upload";
function ClearableMultiUpload() {
const ref = useRef<FileUploadRef>(null);
return (
<>
<ZestFileUpload
ref={ref}
label="Upload files"
multiple
onFileSelect={async (file) => { await upload(file!); }}
maxFiles={8}
/>
<button onClick={() => ref.current?.clearAll()}>
Reset queue
</button>
</>
);
}Custom labels (i18n)
<ZestFileUpload
label="Télécharger un fichier"
onFileSelect={async (file) => { /* ... */ }}
labels={{
browseFiles: "Parcourir",
takePhoto: "Prendre une photo",
uploadComplete: "Téléchargement terminé !",
uploadFailed: "Échec du téléchargement. Veuillez réessayer.",
uploading: "Téléchargement en cours...",
noFileSelected: "Aucun fichier. Cliquez ou déposez un fichier ici.",
invalidFileType: "Type de fichier non valide",
}}
multiLabels={{
filesQueued: "fichiers en attente",
clearAll: "Tout effacer",
pending: "En attente",
retryFile: "Réessayer",
removeFile: "Supprimer",
}}
/>Custom icons
import { Upload, Camera, X } from "lucide-react"; // any icon library
<ZestFileUpload
label="Upload"
onFileSelect={async (file) => { /* ... */ }}
icons={{
browse: <Upload size={18} />,
camera: <Camera size={18} />,
close: <X size={16} />,
}}
/>Disabled state
<ZestFileUpload
label="Upload (locked)"
onFileSelect={async () => {}}
disabled={isSubmitting}
/>With error boundary
<ZestFileUpload
label="Upload"
onFileSelect={async (file) => { /* ... */ }}
errorBoundary
/>Next.js (App Router)
Mark the parent as a Client Component — the file picker and camera APIs are browser-only.
"use client";
import { ZestFileUpload } from "jattac.libs.web.zest-file-upload";
export default function UploadPage() {
return (
<ZestFileUpload
label="Upload file"
onFileSelect={async (file) => {
if (!file) return;
// call a server action or fetch route handler
const formData = new FormData();
formData.append("file", file);
await fetch("/api/upload", { method: "POST", body: formData });
}}
/>
);
}Type Reference
// Per-file entry in multi mode
interface FileEntry {
id: FileEntryId; // stable UUID
file: File;
status: FileEntryStatus; // "pending" | "uploading" | "complete" | "error"
error: string | null;
}
type FileEntryId = string;
type FileEntryStatus = "pending" | "uploading" | "complete" | "error";
// Multi-mode label overrides
interface MultiFileUploadLabels {
addMoreFiles: string;
removeFile: string;
filesQueued: string;
clearAll: string;
pending: string;
retryFile: string;
}
// All label overrides
interface FileUploadLabels {
browseFiles: string;
takePhoto: string;
uploadComplete: string;
uploadFailed: string;
uploading: string;
noFileSelected: string;
invalidFileType: string;
cameraError: string;
retake: string;
usePhoto: string;
cancel: string;
switchCamera: string;
toggleFlash: string;
cropTitle: string;
cropSave: string;
cropCancel: string;
}
// Icon overrides
interface FileUploadIcons {
camera: React.ReactNode;
browse: React.ReactNode;
close: React.ReactNode;
flashOn: React.ReactNode;
flashOff: React.ReactNode;
switchCamera: React.ReactNode;
retake: React.ReactNode;
confirm: React.ReactNode;
}
// Ref handle
interface FileUploadRef {
clearFile: () => void; // resets single-mode state + calls onFileSelect(null)
clearAll: () => void; // clears multi-mode queue; no-op in single mode
}Changelog
0.2.0
- Multi-file mode (
multipleprop) with parallel real-time uploads viaPromise.allSettled - Per-file status queue with animated rows (slide-in, shimmer progress, green pop on complete, shake on error)
onFilesSelectbatch callbackfilesProgressfor per-file progress barsmaxFilescapmultiLabelsfor multi-mode i18nclearAll()ref method- Camera photos in multi mode enqueue and upload independently
0.1.0
- Initial release: single-file upload, drag-drop, camera capture, image crop, compression, progress bar, validation, custom labels/icons, SSR support
License
MIT © Jattac
