A lightweight, headless React file upload library with progress tracking, chunked uploads, and cloud storage support.
- Headless - Full control over UI with hooks-based API
- Drag & Drop - Built-in drag-and-drop support with
getDropProps() - Progress Tracking - Real-time upload progress for each file
- Chunked Uploads - Split large files into chunks for reliable uploads
- Resumable Uploads - tus protocol support for resumable uploads
- Cloud Storage - Pre-built helpers for S3, GCS, and Azure Blob
- Image Processing - Built-in compression and preview generation
- Cross-Platform - Works with React DOM and React Native
- SSR Safe - Compatible with Next.js and other SSR frameworks
- TypeScript - Full TypeScript support with comprehensive types
- Tiny Bundle - Core is ~10KB gzipped, tree-shakeable
npm install @samithahansaka/dropup
# or
yarn add @samithahansaka/dropup
# or
pnpm add @samithahansaka/dropupimport { useDropup } from '@samithahansaka/dropup'
function FileUploader() {
const { files, actions, state, getDropProps, getInputProps } = useDropup({
accept: 'image/*',
maxSize: 10 * 1024 * 1024, // 10MB
multiple: true,
upload: {
url: '/api/upload',
},
})
return (
<div>
<div
{...getDropProps()}
style={{
border: state.isDragActive ? '2px dashed blue' : '2px dashed gray',
padding: 40,
textAlign: 'center',
}}
>
<input {...getInputProps()} />
{state.isDragActive ? (
<p>Drop files here...</p>
) : (
<p>Drag & drop files here, or click to select</p>
)}
</div>
{files.map((file) => (
<div key={file.id}>
<span>{file.name}</span>
<span>{file.progress}%</span>
<span>{file.status}</span>
<button onClick={() => actions.remove(file.id)}>Remove</button>
</div>
))}
<button onClick={() => actions.upload()} disabled={state.isUploading}>
Upload All
</button>
</div>
)
}The main hook for file upload functionality.
| Option | Type | Default | Description |
|---|---|---|---|
accept |
string | string[] |
- | Accepted file types (e.g., 'image/*', ['.pdf', '.doc']) |
maxSize |
number |
- | Maximum file size in bytes |
minSize |
number |
- | Minimum file size in bytes |
maxFiles |
number |
- | Maximum number of files |
multiple |
boolean |
false |
Allow multiple file selection |
disabled |
boolean |
false |
Disable the dropzone |
autoUpload |
boolean |
false |
Automatically upload files when added |
upload |
UploadOptions |
- | Upload configuration |
onFilesAdded |
(files: DropupFile[]) => void |
- | Callback when files are added |
onUploadComplete |
(file: DropupFile) => void |
- | Callback when a file upload completes |
onUploadError |
(file: DropupFile, error: Error) => void |
- | Callback when a file upload fails |
onValidationError |
(errors: ValidationError[]) => void |
- | Callback for validation errors |
interface UploadOptions {
url: string // Upload endpoint
method?: 'POST' | 'PUT' // HTTP method (default: POST)
headers?: Record<string, string> | (() => Promise<Record<string, string>>)
fieldName?: string // Form field name (default: 'file')
formData?: Record<string, string | Blob> // Additional form data
withCredentials?: boolean // Include cookies
timeout?: number // Request timeout in ms
}interface UseDropupReturn {
files: DropupFile[] // Array of files
actions: DropupActions // Action methods
state: ComputedState // Computed state
getDropProps: () => DropZoneProps // Props for drop zone element
getInputProps: () => InputProps // Props for hidden input
openFileDialog: () => void // Programmatically open file dialog
}| Action | Description |
|---|---|
upload() |
Upload all pending files |
uploadFile(id) |
Upload a specific file |
cancel() |
Cancel all uploads |
cancelFile(id) |
Cancel a specific file upload |
remove(id) |
Remove a file from the list |
reset() |
Reset all state |
retry(id) |
Retry a failed upload |
retryAll() |
Retry all failed uploads |
| Property | Type | Description |
|---|---|---|
isUploading |
boolean |
Whether any file is uploading |
isDragActive |
boolean |
Whether files are being dragged over |
isDragAccept |
boolean |
Whether dragged files are accepted |
isDragReject |
boolean |
Whether dragged files are rejected |
progress |
number |
Overall upload progress (0-100) |
status |
'idle' | 'uploading' | 'complete' | 'error' |
Overall status |
error |
DropupError | null |
Last error |
counts |
StatusCounts |
File counts by status |
For large files, use chunked uploads:
import { useDropup, createChunkedUploader } from '@samithahansaka/dropup'
const chunkedUploader = createChunkedUploader({
url: '/api/upload',
chunkSize: 5 * 1024 * 1024, // 5MB chunks
parallelChunks: 3,
})
const { files, actions } = useDropup({
upload: chunkedUploader,
})For resumable uploads using the tus protocol:
npm install tus-js-clientimport { useDropup } from '@samithahansaka/dropup'
import { useTusUploader } from '@samithahansaka/dropup/tus'
const tusUploader = useTusUploader({
endpoint: 'https://tusd.example.com/files/',
chunkSize: 5 * 1024 * 1024,
})
const { files, actions } = useDropup({
upload: tusUploader,
})import { useDropup } from '@samithahansaka/dropup'
import { createS3Uploader } from '@samithahansaka/dropup/cloud/s3'
const s3Uploader = createS3Uploader({
getPresignedUrl: async (file) => {
const response = await fetch('/api/s3-presign', {
method: 'POST',
body: JSON.stringify({ filename: file.name, contentType: file.type }),
})
return response.json()
},
})
const { files, actions } = useDropup({
upload: s3Uploader,
})import { createGCSUploader } from '@samithahansaka/dropup/cloud/gcs'
const gcsUploader = createGCSUploader({
getPresignedUrl: async (file) => {
const response = await fetch('/api/gcs-presign', {
method: 'POST',
body: JSON.stringify({ filename: file.name }),
})
return response.json()
},
})import { createAzureUploader } from '@samithahansaka/dropup/cloud/azure'
const azureUploader = createAzureUploader({
getPresignedUrl: async (file) => {
const response = await fetch('/api/azure-presign', {
method: 'POST',
body: JSON.stringify({ filename: file.name }),
})
return response.json()
},
})import { useDropup } from '@samithahansaka/dropup'
import { compressImage, generatePreview, fixImageOrientation } from '@samithahansaka/dropup/image'
const { files } = useDropup({
accept: 'image/*',
onFilesAdded: async (newFiles) => {
for (const file of newFiles) {
// Generate preview
const preview = await generatePreview(file.file, { maxWidth: 200 })
// Compress before upload
const compressed = await compressImage(file.file, {
maxWidth: 1920,
maxHeight: 1080,
quality: 0.8,
})
}
},
})import { useDropup, commonRules } from '@samithahansaka/dropup'
const { files } = useDropup({
accept: 'image/*',
maxSize: 5 * 1024 * 1024,
customRules: [
commonRules.safeFileName,
commonRules.extensionMatchesMime,
{
name: 'minDimensions',
validate: async (file) => {
// Custom async validation
const img = await createImageBitmap(file)
if (img.width < 100 || img.height < 100) {
return 'Image must be at least 100x100 pixels'
}
return true
},
},
],
})import { useDropup } from '@samithahansaka/dropup/native'
import * as ImagePicker from 'expo-image-picker'
function NativeUploader() {
const { files, actions } = useDropup({
upload: { url: 'https://api.example.com/upload' },
})
const pickImage = async () => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
})
if (!result.canceled) {
actions.addFiles(result.assets.map(asset => ({
uri: asset.uri,
name: asset.fileName || 'image.jpg',
type: asset.mimeType || 'image/jpeg',
size: asset.fileSize || 0,
})))
}
}
return (
<View>
<Button title="Pick Image" onPress={pickImage} />
{files.map(file => (
<Text key={file.id}>{file.name} - {file.progress}%</Text>
))}
</View>
)
}interface DropupFile {
id: string // Unique identifier
name: string // File name
size: number // File size in bytes
type: string // MIME type
status: FileStatus // 'idle' | 'pending' | 'uploading' | 'paused' | 'complete' | 'error'
progress: number // Upload progress (0-100)
file: File // Original File object
preview?: string // Preview URL (for images)
uploadedUrl?: string // URL after upload
error?: DropupError // Error if failed
meta?: Record<string, unknown> // Custom metadata
}| Entry Point | Size (gzipped) |
|---|---|
dropup |
~10KB |
dropup/tus |
~1.5KB |
dropup/image |
~5KB |
dropup/cloud/s3 |
~700B |
dropup/cloud/gcs |
~650B |
dropup/cloud/azure |
~700B |
- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
- React Native (iOS & Android)
dropup is written in TypeScript and provides comprehensive type definitions:
import type {
DropupFile,
UseDropupOptions,
UseDropupReturn,
UploadOptions,
ValidationError,
DropupError,
} from '@samithahansaka/dropup'Contributions are welcome! Please read our Contributing Guide for details.
MIT © Samitha Widanage
- tus-js-client - tus resumable upload protocol
- react-dropzone - Simple drag-and-drop