Guides
Custom Upload UI
Build custom upload interfaces with the useClawdy hook
Overview
While Clawdy provides ready-made <UploadButton /> and <UploadDropzone /> components, you may want full control over your upload UI. The useClawdy hook gives you all the building blocks to create a completely custom experience.
The useClawdy Hook
Import the hook from @clawdyup/react and pass it the name of your file route endpoint:
import { useClawdy } from "@clawdyup/react";
function MyUploader() {
const { startUpload, isUploading } = useClawdy("profileImage");
return (
<div>
<input
type="file"
onChange={async (e) => {
const files = Array.from(e.target.files ?? []);
await startUpload(files);
}}
/>
{isUploading && <p>Uploading...</p>}
</div>
);
}Return Values
The hook returns the following:
| Property | Type | Description |
|---|---|---|
startUpload | (files: File[]) => Promise<UploadResult[]> | Begins the upload for the given files |
isUploading | boolean | Whether an upload is currently in progress |
Building a Custom File Picker
Here is a complete example of a custom file picker with a styled button:
"use client";
import { useState } from "react";
import { useClawdy } from "@clawdyup/react";
export function CustomFilePicker() {
const [files, setFiles] = useState<File[]>([]);
const { startUpload, isUploading } = useClawdy("imageUploader");
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
setFiles(Array.from(e.target.files));
}
};
const handleUpload = async () => {
if (files.length === 0) return;
const result = await startUpload(files);
console.log("Uploaded files:", result);
setFiles([]);
};
return (
<div className="flex flex-col gap-4">
<label className="cursor-pointer rounded-md border border-dashed border-gray-300 p-4 text-center hover:border-gray-500">
<span>{files.length > 0 ? `${files.length} file(s) selected` : "Choose files"}</span>
<input
type="file"
multiple
className="hidden"
onChange={handleFileChange}
/>
</label>
{files.length > 0 && (
<ul className="text-sm text-gray-600">
{files.map((file) => (
<li key={file.name}>{file.name} ({(file.size / 1024).toFixed(1)} KB)</li>
))}
</ul>
)}
<button
onClick={handleUpload}
disabled={isUploading || files.length === 0}
className="rounded-md bg-blue-600 px-4 py-2 text-white disabled:opacity-50"
>
{isUploading ? "Uploading..." : "Upload"}
</button>
</div>
);
}Progress Tracking
Track upload progress by passing an onUploadProgress callback:
const { startUpload, isUploading } = useClawdy("imageUploader", {
onUploadProgress: (progress) => {
console.log(`Upload is ${progress}% complete`);
setProgress(progress);
},
});Use this to render a progress bar:
"use client";
import { useState } from "react";
import { useClawdy } from "@clawdyup/react";
export function UploaderWithProgress() {
const [progress, setProgress] = useState(0);
const { startUpload, isUploading } = useClawdy("imageUploader", {
onUploadProgress: (p) => setProgress(p),
});
return (
<div>
<input
type="file"
onChange={async (e) => {
const files = Array.from(e.target.files ?? []);
setProgress(0);
await startUpload(files);
}}
/>
{isUploading && (
<div className="h-2 w-full rounded-full bg-gray-200">
<div
className="h-2 rounded-full bg-blue-600 transition-all"
style={{ width: `${progress}%` }}
/>
</div>
)}
</div>
);
}Error Handling
Handle upload errors with the onUploadError callback:
const { startUpload, isUploading } = useClawdy("imageUploader", {
onUploadError: (error) => {
alert(`Upload failed: ${error.message}`);
},
onUploadProgress: (progress) => {
setProgress(progress);
},
});Custom Drag-and-Drop UI
Here is a full example of a custom drag-and-drop upload zone:
"use client";
import { useState, useCallback } from "react";
import { useClawdy } from "@clawdyup/react";
export function CustomDropzone() {
const [isDragging, setIsDragging] = useState(false);
const [progress, setProgress] = useState(0);
const [uploadedFiles, setUploadedFiles] = useState<{ url: string; name: string }[]>([]);
const { startUpload, isUploading } = useClawdy("imageUploader", {
onUploadProgress: (p) => setProgress(p),
onUploadError: (error) => {
alert(`Upload failed: ${error.message}`);
},
});
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault();
setIsDragging(true);
}, []);
const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
}, []);
const handleDrop = useCallback(
async (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
const files = Array.from(e.dataTransfer.files);
if (files.length === 0) return;
setProgress(0);
const result = await startUpload(files);
if (result) {
setUploadedFiles((prev) => [
...prev,
...result.map((f) => ({ url: f.url, name: f.name })),
]);
}
},
[startUpload],
);
return (
<div className="flex flex-col gap-4">
<div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={`flex h-48 items-center justify-center rounded-lg border-2 border-dashed transition-colors ${
isDragging
? "border-blue-500 bg-blue-50"
: "border-gray-300 bg-gray-50"
}`}
>
{isUploading ? (
<div className="text-center">
<p className="text-sm text-gray-600">Uploading... {progress}%</p>
<div className="mt-2 h-2 w-48 rounded-full bg-gray-200">
<div
className="h-2 rounded-full bg-blue-600 transition-all"
style={{ width: `${progress}%` }}
/>
</div>
</div>
) : (
<p className="text-sm text-gray-500">
Drag and drop files here
</p>
)}
</div>
{uploadedFiles.length > 0 && (
<ul className="space-y-1 text-sm">
{uploadedFiles.map((file) => (
<li key={file.url}>
<a href={file.url} className="text-blue-600 underline">
{file.name}
</a>
</li>
))}
</ul>
)}
</div>
);
}