Browser only

Use the components purely as UI, files stay in the browser, and your form or code decides what to do with them.

import { localAdapter } from "@uploadcn/core"

Sometimes you don't want an upload pipeline: a form that posts files to a server action, a desktop app, an offline draft, or a prototype. localAdapter "uploads" instantly without a network, and every component keeps working: previews, validation, cropping, compression, animations.

"use client"

import { localAdapter } from "@uploadcn/core"
import { toast } from "sonner"

import { Button } from "@/components/ui/button"
import {
  Upload,
  UploadDropzone,
  UploadDropzoneDescription,
  UploadDropzoneTitle,
  UploadQueue,
} from "@/components/ui/upload"

const adapter = localAdapter()

/**
 * No storage at all: files stay in the browser and the form submits them
 * like a regular <input type="file">, e.g. to a server action.
 */
export default function UiOnlyFormExample() {
  return (
    <form
      className="flex w-full max-w-md flex-col gap-3"
      onSubmit={(event) => {
        event.preventDefault()
        const files = new FormData(event.currentTarget).getAll(
          "attachments"
        ) as File[]
        toast.success(`Form submitted with ${files.length} files`, {
          description: files.map((file) => file.name).join(", "),
        })
      }}
    >
      <Upload adapter={adapter} name="attachments" submit="file">
        <UploadDropzone size="sm">
          <UploadDropzoneTitle>Attach files</UploadDropzoneTitle>
          <UploadDropzoneDescription>
            Nothing is uploaded, the form sends the files
          </UploadDropzoneDescription>
        </UploadDropzone>
        <UploadQueue size="sm" variant="muted" />
      </Upload>
      <Button type="submit" className="self-end">
        Submit form
      </Button>
    </form>
  )
}

Submit files with a form

Set name and submit="file" on <Upload>. The selected files are added to the form like a regular <input type="file">:

app/new-post/page.tsx
import { localAdapter } from "@uploadcn/core"

import { Upload, UploadDropzone, UploadQueue } from "@/components/ui/upload"

const adapter = localAdapter()

export default function NewPost() {
  async function createPost(formData: FormData) {
    "use server"
    const files = formData.getAll("attachments") as File[]
    // Save them wherever you like.
  }

  return (
    <form action={createPost}>
      <Upload adapter={adapter} name="attachments" submit="file">
        <UploadDropzone />
        <UploadQueue />
      </Upload>
      <button type="submit">Post</button>
    </form>
  )
}

Transforms apply

If you crop or compress images before upload, the form submits the processed file, so the server receives the smaller version.

Read files in code

The result of each item carries the File and a preview URL:

<FileUpload
  adapter={localAdapter()}
  onSuccess={(item) => {
    item.result.file // File
    item.result.url  // blob: URL for previews
  }}
/>

Keep files across reloads

With store: "indexeddb", files are also saved in the browser. Read them back later, even after a reload, with getLocalFile:

import { getLocalFile, localAdapter } from "@uploadcn/core"

const adapter = localAdapter({ store: "indexeddb" })

// later
const file = await getLocalFile(item.id)

Remove them with deleteLocalFile(id) when you're done.

On this page