Clawdy
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:

PropertyTypeDescription
startUpload(files: File[]) => Promise<UploadResult[]>Begins the upload for the given files
isUploadingbooleanWhether 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>
  );
}

On this page