Skip to main content
A production-ready file upload component with drag-and-drop support, file validation, upload progress, and complete Convex storage integration.

Installation

npx shadcn@latest add https://convex-ui.vercel.app/r/dropzone-react
This installs:
  • Dropzone component with drag-and-drop UI
  • useConvexUpload hook for upload logic
  • File management backend functions
  • Convex storage integration
  • Convex client setup

What’s Included

Drag & Drop

Intuitive drag-and-drop interface with visual feedback

File Validation

Size limits, type restrictions, and error handling

Upload Progress

Visual feedback during upload with success/error states

Convex Storage

Direct upload to Convex with automatic metadata tracking

Setup

1
Deploy Convex Backend
2
npx convex dev
3
Configure Environment
4
VITE_CONVEX_URL=https://your-deployment.convex.cloud
5
Add Provider
6
Ensure ConvexClientProvider wraps your app (automatically included).

Component

Dropzone

File upload component with drag-and-drop interface.
import { Dropzone } from "@/components/dropzone";
import { useConvexUpload } from "@/hooks/use-convex-upload";

function FileUploadPage() {
  const upload = useConvexUpload({
    maxFileSize: 5 * 1024 * 1024, // 5MB
    maxFiles: 3,
    allowedMimeTypes: ['image/png', 'image/jpeg', 'application/pdf'],
  });
  
  return (
    <div className="container max-w-2xl mx-auto py-8">
      <h1 className="text-2xl font-bold mb-6">Upload Files</h1>
      <Dropzone upload={upload} />
    </div>
  );
}

Props

upload
UseConvexUploadReturn
required
Upload state and handlers from useConvexUpload hook.
className
string
Additional CSS classes for the dropzone container.

useConvexUpload Hook

Manages file upload state and logic.
import { useConvexUpload } from '@/hooks/use-convex-upload';

const upload = useConvexUpload({
  allowedMimeTypes: ['image/*', 'video/*'],
  maxFileSize: 10 * 1024 * 1024, // 10MB
  maxFiles: 5,
});

Options

allowedMimeTypes
string[]
default:[]
Allowed MIME types (e.g., ['image/png', 'image/jpeg']). Wildcards supported ('image/*'). Empty array allows all types.
maxFileSize
number
default:null
Maximum file size in bytes (e.g., 5 * 1024 * 1024 for 5MB).
maxFiles
number
default:1
Maximum number of files allowed per upload.

Return Value

{
  // State
  files: FileWithPreview[],
  loading: boolean,
  errors: { name: string; message: string }[],
  successes: string[],
  isSuccess: boolean,
  
  // Actions
  setFiles: (files: FileWithPreview[]) => void,
  onUpload: () => Promise<void>,
  
  // Config
  maxFileSize: number,
  maxFiles: number,
  allowedMimeTypes: string[],
  
  // Dropzone props
  getRootProps: () => object,
  getInputProps: () => object,
  isDragActive: boolean,
  open: () => void,
}

Usage Examples

import { Dropzone } from '@/components/dropzone';
import { useConvexUpload } from '@/hooks/use-convex-upload';

function ImageUploader() {
  const upload = useConvexUpload({
    allowedMimeTypes: ['image/png', 'image/jpeg', 'image/webp'],
    maxFileSize: 5 * 1024 * 1024, // 5MB
    maxFiles: 1,
  });
  
  return (
    <div className="max-w-md mx-auto">
      <h2>Upload Profile Picture</h2>
      <Dropzone upload={upload} />
    </div>
  );
}

Features

Drag and Drop

Intuitive drag-and-drop interface:
  • Hover effect when dragging files over dropzone
  • Click to browse files
  • Visual feedback during drag

File Preview

Shows preview for each file:
  • Image thumbnails for image files
  • File type icons for other files
  • File name and size
  • Upload status (loading, success, error)

Validation

Client-side validation before upload:
  • File type checking via MIME types
  • File size limits
  • Maximum file count
  • Clear error messages

Error Handling

Comprehensive error states:
  • Per-file validation errors
  • Upload errors (network, server)
  • Visual error indicators
  • Retry failed uploads

Backend Functions

generateUploadUrl

Generate a temporary upload URL.
const uploadUrl = await generateUploadUrl();
Returns: string - Temporary URL for file upload Security Note: This is a public mutation. For production, consider adding rate limiting or authentication.

saveFile

Save file metadata after upload.
await saveFile({
  storageId: "kg2...",
  name: "document.pdf",
  type: "application/pdf",
  size: 1024000,
  sessionId: "session-123" // Optional
});
Arguments:
storageId
Id<'_storage'>
required
Storage ID returned from Convex upload
name
string
required
Original filename (max 255 chars)
type
string
required
MIME type of the file
size
number
required
File size in bytes (max 50MB)
sessionId
string
Session ID for demo mode (optional)
Returns: Id<"files"> - The created file record ID

list

List files for current user or session.
const files = useQuery(api.files.list, { 
  sessionId: "session-123" 
});
Returns:
Array<{
  _id: Id<"files">,
  _creationTime: number,
  storageId: Id<"_storage">,
  userId?: Id<"users">,
  sessionId?: string,
  name: string,
  type: string,
  size: number,
  url: string | null
}>

getUrl

Get download URL for a file.
const url = useQuery(api.files.getUrl, { 
  storageId: "kg2..." 
});

remove

Delete a file (storage + metadata).
await remove({
  fileId: "j97...",
  sessionId: "session-123" // Required for demo mode
});

Upload Flow

1
User Selects Files
2
User drags files or clicks to browse. Files are validated client-side.
3
Generate Upload URL
4
For each valid file, request a temporary upload URL from Convex:
5
const uploadUrl = await generateUploadUrl();
6
Upload to Storage
7
Upload file directly to Convex storage:
8
const response = await fetch(uploadUrl, {
  method: "POST",
  headers: { "Content-Type": file.type },
  body: file,
});
const { storageId } = await response.json();
9
Save Metadata
10
Save file information to database:
11
await saveFile({
  storageId,
  name: file.name,
  type: file.type,
  size: file.size,
});
12
Show Success
13
Update UI with success state and show uploaded files.

File Types

Supported MIME Types

Common MIME type examples:
// Images
'image/png'
'image/jpeg'
'image/gif'
'image/webp'
'image/*' // All images

// Documents
'application/pdf'
'application/msword'
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
'text/plain'

// Videos
'video/mp4'
'video/webm'
'video/*' // All videos

// Audio
'audio/mpeg'
'audio/wav'
'audio/*' // All audio

File Icons

The component shows different icons based on file type:
  • Images: ImageIcon + thumbnail preview
  • Videos: FileVideoIcon
  • Audio: FileAudioIcon
  • Text/Documents: FileTextIcon
  • Other: FileIcon

Validation Errors

Common validation error codes:
  • file-invalid-type: File type not allowed
  • file-too-large: File exceeds size limit
  • too-many-files: Too many files selected

Customization

Custom Dropzone Style

<Dropzone 
  upload={upload}
  className="border-4 border-dashed border-primary rounded-xl"
/>

Custom File Preview

Modify the file card rendering in components/dropzone.tsx:
<Card className="...">
  <CardContent className="flex items-center gap-4 p-4">
    {/* Your custom preview layout */}
  </CardContent>
</Card>

Custom Upload Button

<Button 
  onClick={upload.onUpload}
  disabled={!canUpload}
  className="w-full bg-gradient-to-r from-blue-500 to-purple-500"
>
  Upload Files
</Button>

Schema

The files table schema:
convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  files: defineTable({
    storageId: v.id("_storage"),
    userId: v.optional(v.id("users")),
    sessionId: v.optional(v.string()),
    name: v.string(),
    type: v.string(),
    size: v.number(),
  })
    .index("by_user", ["userId"])
    .index("by_session", ["sessionId"]),
});

Security

The generateUploadUrl mutation is public by default. For production apps:
  • Add rate limiting to prevent abuse
  • Require authentication
  • Implement file type/size validation on the server
  • Consider virus scanning for uploaded files
Security features included:
  • File size validation (max 50MB backend)
  • Filename sanitization
  • User/session ownership tracking
  • Storage cleanup on file deletion

Demo Mode

Works without authentication:
  • Each session gets a unique ID
  • Files associated with session
  • Session ID NOT in localStorage (iframe-safe)

Performance

Direct Upload

Files upload directly to Convex storage (no intermediate server)

Parallel Processing

Multiple files upload in parallel

Retry Logic

Failed uploads can be retried without re-selecting files

Efficient Storage

Convex storage optimized for file delivery

Troubleshooting

Check browser console for errors. Verify VITE_CONVEX_URL is set and npx convex dev is running.
The backend enforces a 50MB limit. Adjust maxFileSize in your hook to match or be lower.
Make sure allowedMimeTypes matches the files you’re uploading. Use wildcards like 'image/*' for flexibility.
Image previews are generated using URL.createObjectURL(). Check that the file is a valid image type.

Next Steps

Password Auth

Authenticate users before uploading

Realtime Chat

Share files in chat messages

Current User Avatar

Upload profile pictures

Build docs developers (and LLMs) love