Command Palette

Search for a command...

File Upload

A file upload component with drag and drop, previewing, and progress tracking.

Drag & drop files here

Or click to browse (max 2 files, up to 5MB each)

File upload
"use client";

import * as React from "react";
import { LuUpload, LuX } from "react-icons/lu";
import { toast } from "sonner";
import { css } from "styled-system/css";
import { styled } from "styled-system/jsx";
import { Button } from "@/components/ui/button";
import {
  FileUpload,
  FileUploadDropzone,
  FileUploadItem,
  FileUploadItemDelete,
  FileUploadItemMetadata,
  FileUploadItemPreview,
  FileUploadList,
  FileUploadTrigger,
} from "@/components/ui/file-upload";

export default function FileUploadDemo() {
  const [files, setFiles] = React.useState<File[]>([]);

  const onFileReject = React.useCallback((file: File, message: string) => {
    toast(message, {
      description: `"${file.name.length > 20 ? `${file.name.slice(0, 20)}...` : file.name}" has been rejected`,
    });
  }, []);

  return (
    <FileUpload
      maxFiles={2}
      maxSize={5 * 1024 * 1024}
      css={{ w: "full", maxW: "md" }}
      value={files}
      onValueChange={setFiles}
      onFileReject={onFileReject}
      multiple
    >
      <FileUploadDropzone>
        <styled.div
          css={{
            display: "flex",
            flexDir: "column",
            alignItems: "center",
            gap: "1",
            textAlign: "center",
          }}
        >
          <styled.div
            css={{
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
              rounded: "full",
              borderWidth: "1px",
              p: "2.5",
            }}
          >
            <LuUpload className={css({ w: "6", h: "6", color: "muted.fg" })} />
          </styled.div>
          <styled.p css={{ fontWeight: "medium", textStyle: "sm" }}>
            Drag & drop files here
          </styled.p>
          <styled.p css={{ color: "muted.fg", textStyle: "xs" }}>
            Or click to browse (max 2 files, up to 5MB each)
          </styled.p>
        </styled.div>
        <FileUploadTrigger asChild>
          <Button variant="outline" size="sm" css={{ mt: "2", w: "fit" }}>
            Browse files
          </Button>
        </FileUploadTrigger>
      </FileUploadDropzone>
      <FileUploadList>
        {files.map((file, index) => (
          <FileUploadItem key={index} value={file}>
            <FileUploadItemPreview />
            <FileUploadItemMetadata />
            <FileUploadItemDelete asChild>
              <Button variant="ghost" size="icon" css={{ w: "7", h: "7" }}>
                <LuX />
              </Button>
            </FileUploadItemDelete>
          </FileUploadItem>
        ))}
      </FileUploadList>
    </FileUpload>
  );
}

Installation

npx nore-ui-cli@latest add file-upload

Usage

import {
  FileUpload,
  FileUploadDropzone,
  FileUploadItem,
  FileUploadItemDelete,
  FileUploadItemMetadata,
  FileUploadItemPreview,
  FileUploadTrigger,
} from "@/components/ui/file-upload";
<FileUpload>
  <FileUploadDropzone />
  <FileUploadTrigger />
  <FileUploadList>
    <FileUploadItem>
      <FileUploadItemPreview />
      <FileUploadItemMetadata />
      <FileUploadItemProgress />
      <FileUploadItemDelete />
    </FileUploadItem>
  </FileUploadList>
</FileUpload>

Examples

With Validation

Validate files with the onFileValidate prop on the root component based on type, size, and custom rules. This will override the default file rejection message.

Drag & drop files here

Or click to browse (max 2 files)

File upload
"use client";

import * as React from "react";
import { LuUpload, LuX } from "react-icons/lu";
import { toast } from "sonner";
import { css } from "styled-system/css";
import { styled } from "styled-system/jsx";
import { Button } from "@/components/ui/button";
import {
  FileUpload,
  FileUploadDropzone,
  FileUploadItem,
  FileUploadItemDelete,
  FileUploadItemMetadata,
  FileUploadItemPreview,
  FileUploadList,
  FileUploadTrigger,
} from "@/components/ui/file-upload";

export default function FileUploadValidation() {
  const [files, setFiles] = React.useState<File[]>([]);

  const onFileValidate = React.useCallback(
    (file: File): string | null => {
      // Validate max files
      if (files.length >= 2) {
        return "You can only upload up to 2 files";
      }

      // Validate file type (only images)
      if (!file.type.startsWith("image/")) {
        return "Only image files are allowed";
      }

      // Validate file size (max 2MB)
      const MAX_SIZE = 2 * 1024 * 1024; // 2MB
      if (file.size > MAX_SIZE) {
        return `File size must be less than ${MAX_SIZE / (1024 * 1024)}MB`;
      }

      return null;
    },
    [files]
  );

  const onFileReject = React.useCallback((file: File, message: string) => {
    toast(message, {
      description: `"${file.name.length > 20 ? `${file.name.slice(0, 20)}...` : file.name}" has been rejected`,
    });
  }, []);

  return (
    <FileUpload
      value={files}
      onValueChange={setFiles}
      onFileValidate={onFileValidate}
      onFileReject={onFileReject}
      accept="image/*"
      maxFiles={2}
      css={{ w: "full", maxW: "md" }}
      multiple
    >
      <FileUploadDropzone>
        <styled.div css={{ display: "flex", flexDir: "column", alignItems: "center", gap: "1" }}>
          <styled.div
            css={{
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
              rounded: "full",
              borderWidth: "1px",
              p: "2.5",
            }}
          >
            <LuUpload className={css({ w: "6", h: "6", color: "muted.fg" })} />
          </styled.div>
          <styled.p css={{ fontWeight: "medium", textStyle: "sm" }}>
            Drag & drop files here
          </styled.p>
          <styled.p css={{ color: "muted.fg", textStyle: "xs" }}>
            Or click to browse (max 2 files)
          </styled.p>
        </styled.div>
        <FileUploadTrigger asChild>
          <Button variant="outline" size="sm" css={{ mt: "2", w: "fit" }}>
            Browse files
          </Button>
        </FileUploadTrigger>
      </FileUploadDropzone>
      <FileUploadList>
        {files.map((file) => (
          <FileUploadItem key={file.name} value={file}>
            <FileUploadItemPreview />
            <FileUploadItemMetadata />
            <FileUploadItemDelete asChild>
              <Button variant="ghost" size="icon" css={{ w: "7", h: "7" }}>
                <LuX />
              </Button>
            </FileUploadItemDelete>
          </FileUploadItem>
        ))}
      </FileUploadList>
    </FileUpload>
  );
}

Direct Upload

Upload files directly with the onUpload prop on the root component.

Drag & drop files here

Or click to browse (max 2 files)

File upload
"use client";

import * as React from "react";
import { LuUpload, LuX } from "react-icons/lu";
import { toast } from "sonner";
import { css } from "styled-system/css";
import { styled } from "styled-system/jsx";
import { Button } from "@/components/ui/button";
import {
  FileUpload,
  FileUploadDropzone,
  FileUploadItem,
  FileUploadItemDelete,
  FileUploadItemMetadata,
  FileUploadItemPreview,
  FileUploadItemProgress,
  FileUploadList,
  FileUploadTrigger,
  type FileUploadProps,
} from "@/components/ui/file-upload";

export default function FileUploadDirectUpload() {
  const [files, setFiles] = React.useState<File[]>([]);

  const onUpload: NonNullable<FileUploadProps["onUpload"]> = React.useCallback(
    async (files, { onProgress, onSuccess, onError }) => {
      try {
        // Process each file individually
        const uploadPromises = files.map(async (file) => {
          try {
            // Simulate file upload with progress
            const totalChunks = 10;
            let uploadedChunks = 0;

            // Simulate chunk upload with delays
            for (let i = 0; i < totalChunks; i++) {
              // Simulate network delay (100-300ms per chunk)
              await new Promise((resolve) => setTimeout(resolve, Math.random() * 200 + 100));

              // Update progress for this specific file
              uploadedChunks++;
              const progress = (uploadedChunks / totalChunks) * 100;
              onProgress(file, progress);
            }

            // Simulate server processing delay
            await new Promise((resolve) => setTimeout(resolve, 500));
            onSuccess(file);
          } catch (error) {
            onError(file, error instanceof Error ? error : new Error("Upload failed"));
          }
        });

        // Wait for all uploads to complete
        await Promise.all(uploadPromises);
      } catch (error) {
        // This handles any error that might occur outside the individual upload processes
        console.error("Unexpected error during upload:", error);
      }
    },
    []
  );

  const onFileReject = React.useCallback((file: File, message: string) => {
    toast(message, {
      description: `"${file.name.length > 20 ? `${file.name.slice(0, 20)}...` : file.name}" has been rejected`,
    });
  }, []);

  return (
    <FileUpload
      value={files}
      onValueChange={setFiles}
      onUpload={onUpload}
      onFileReject={onFileReject}
      maxFiles={2}
      css={{ w: "full", maxW: "md" }}
      multiple
    >
      <FileUploadDropzone>
        <styled.div
          css={{
            display: "flex",
            flexDir: "column",
            alignItems: "center",
            gap: "1",
            textAlign: "center",
          }}
        >
          <styled.div
            css={{
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
              rounded: "full",
              borderWidth: "1px",
              p: "2.5",
            }}
          >
            <LuUpload className={css({ w: "6", h: "6", color: "muted.fg" })} />
          </styled.div>
          <styled.p css={{ fontWeight: "medium", textStyle: "sm" }}>
            Drag & drop files here
          </styled.p>
          <styled.p css={{ color: "muted.fg", textStyle: "xs" }}>
            Or click to browse (max 2 files)
          </styled.p>
        </styled.div>
        <FileUploadTrigger asChild>
          <Button variant="outline" size="sm" css={{ mt: "2", w: "fit" }}>
            Browse files
          </Button>
        </FileUploadTrigger>
      </FileUploadDropzone>
      <FileUploadList>
        {files.map((file, index) => (
          <FileUploadItem key={index} value={file} css={{ flexDir: "column" }}>
            <styled.div css={{ display: "flex", w: "full", alignItems: "center", gap: "2" }}>
              <FileUploadItemPreview />
              <FileUploadItemMetadata />
              <FileUploadItemDelete asChild>
                <Button variant="ghost" size="icon" css={{ w: "7", h: "7" }}>
                  <LuX />
                </Button>
              </FileUploadItemDelete>
            </styled.div>
            <FileUploadItemProgress />
          </FileUploadItem>
        ))}
      </FileUploadList>
    </FileUpload>
  );
}

Circular Progress

Render a circular progress indicator instead of a linear one by enabling the circular prop on the FileUploadItemProgress component.

Drag & drop files here

Or click to browse (max 10 files, up to 5MB each)

File upload
"use client";

import * as React from "react";
import { LuUpload, LuX } from "react-icons/lu";
import { toast } from "sonner";
import { css } from "styled-system/css";
import { styled } from "styled-system/jsx";
import { Button } from "@/components/ui/button";
import {
  FileUpload,
  FileUploadDropzone,
  FileUploadItem,
  FileUploadItemDelete,
  FileUploadItemMetadata,
  FileUploadItemPreview,
  FileUploadItemProgress,
  FileUploadList,
  FileUploadTrigger,
} from "@/components/ui/file-upload";

export default function FileUploadCircularProgress() {
  const [files, setFiles] = React.useState<File[]>([]);

  const onUpload = React.useCallback(
    async (
      files: File[],
      {
        onProgress,
        onSuccess,
        onError,
      }: {
        onProgress: (file: File, progress: number) => void;
        onSuccess: (file: File) => void;
        onError: (file: File, error: Error) => void;
      }
    ) => {
      try {
        // Process each file individually
        const uploadPromises = files.map(async (file) => {
          try {
            // Simulate file upload with progress
            const totalChunks = 10;
            let uploadedChunks = 0;

            // Simulate chunk upload with delays
            for (let i = 0; i < totalChunks; i++) {
              // Simulate network delay (100-300ms per chunk)
              await new Promise((resolve) => setTimeout(resolve, Math.random() * 200 + 100));

              // Update progress for this specific file
              uploadedChunks++;
              const progress = (uploadedChunks / totalChunks) * 100;
              onProgress(file, progress);
            }

            // Simulate server processing delay
            await new Promise((resolve) => setTimeout(resolve, 500));
            onSuccess(file);
          } catch (error) {
            onError(file, error instanceof Error ? error : new Error("Upload failed"));
          }
        });

        // Wait for all uploads to complete
        await Promise.all(uploadPromises);
      } catch (error) {
        // This handles any error that might occur outside the individual upload processes
        console.error("Unexpected error during upload:", error);
      }
    },
    []
  );

  const onFileReject = React.useCallback((file: File, message: string) => {
    toast(message, {
      description: `"${file.name.length > 20 ? `${file.name.slice(0, 20)}...` : file.name}" has been rejected`,
    });
  }, []);

  return (
    <FileUpload
      value={files}
      onValueChange={setFiles}
      maxFiles={10}
      maxSize={5 * 1024 * 1024}
      css={{ w: "full", maxW: "md" }}
      onUpload={onUpload}
      onFileReject={onFileReject}
      multiple
    >
      <FileUploadDropzone>
        <styled.div
          css={{
            display: "flex",
            flexDir: "column",
            alignItems: "center",
            gap: "1",
            textAlign: "center",
          }}
        >
          <styled.div
            css={{
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
              rounded: "full",
              borderWidth: "1px",
              p: "2.5",
            }}
          >
            <LuUpload className={css({ w: "6", h: "6", color: "muted.fg" })} />
          </styled.div>
          <styled.p css={{ fontWeight: "medium", textStyle: "sm" }}>
            Drag & drop files here
          </styled.p>
          <styled.p css={{ color: "muted.fg", textStyle: "xs" }}>
            Or click to browse (max 10 files, up to 5MB each)
          </styled.p>
        </styled.div>
        <FileUploadTrigger asChild>
          <Button variant="outline" size="sm" css={{ mt: "2", w: "fit" }}>
            Browse files
          </Button>
        </FileUploadTrigger>
      </FileUploadDropzone>
      <FileUploadList orientation="horizontal">
        {files.map((file, index) => (
          <FileUploadItem key={index} value={file} css={{ p: "0" }}>
            <FileUploadItemPreview
              css={{
                w: "20",
                h: "20",
                "& > svg": {
                  w: "12",
                  h: "12",
                },
              }}
            >
              <FileUploadItemProgress variant="circular" size={40} />
            </FileUploadItemPreview>
            <FileUploadItemMetadata css={{ srOnly: true }} />
            <FileUploadItemDelete asChild>
              <Button
                variant="secondary"
                size="icon"
                css={{ pos: "absolute", top: "-1", right: "-1", w: "5", h: "5", rounded: "full" }}
              >
                <LuX className={css({ w: "3", h: "3" })} />
              </Button>
            </FileUploadItemDelete>
          </FileUploadItem>
        ))}
      </FileUploadList>
    </FileUpload>
  );
}

Fill Progress

Render a fill progress indicator instead of a linear one by enabling the fill prop on the FileUploadItemProgress component.

Drag & drop files here

Or click to browse (max 10 files, up to 5MB each)

File upload
"use client";

import * as React from "react";
import { LuUpload, LuX } from "react-icons/lu";
import { toast } from "sonner";
import { css } from "styled-system/css";
import { styled } from "styled-system/jsx";
import { Button } from "@/components/ui/button";
import {
  FileUpload,
  FileUploadDropzone,
  FileUploadItem,
  FileUploadItemDelete,
  FileUploadItemMetadata,
  FileUploadItemPreview,
  FileUploadItemProgress,
  FileUploadList,
  FileUploadTrigger,
} from "@/components/ui/file-upload";

export default function FileUploadFillProgress() {
  const [files, setFiles] = React.useState<File[]>([]);

  const onUpload = React.useCallback(
    async (
      files: File[],
      {
        onProgress,
        onSuccess,
        onError,
      }: {
        onProgress: (file: File, progress: number) => void;
        onSuccess: (file: File) => void;
        onError: (file: File, error: Error) => void;
      }
    ) => {
      try {
        // Process each file individually
        const uploadPromises = files.map(async (file) => {
          try {
            // Simulate file upload with progress
            const totalChunks = 10;
            let uploadedChunks = 0;

            // Simulate chunk upload with delays
            for (let i = 0; i < totalChunks; i++) {
              // Simulate network delay (100-300ms per chunk)
              await new Promise((resolve) => setTimeout(resolve, Math.random() * 200 + 100));

              // Update progress for this specific file
              uploadedChunks++;
              const progress = (uploadedChunks / totalChunks) * 100;
              onProgress(file, progress);
            }

            // Simulate server processing delay
            await new Promise((resolve) => setTimeout(resolve, 500));
            onSuccess(file);
          } catch (error) {
            onError(file, error instanceof Error ? error : new Error("Upload failed"));
          }
        });

        // Wait for all uploads to complete
        await Promise.all(uploadPromises);
      } catch (error) {
        // This handles any error that might occur outside the individual upload processes
        console.error("Unexpected error during upload:", error);
      }
    },
    []
  );

  const onFileReject = React.useCallback((file: File, message: string) => {
    toast(message, {
      description: `"${file.name.length > 20 ? `${file.name.slice(0, 20)}...` : file.name}" has been rejected`,
    });
  }, []);

  return (
    <FileUpload
      value={files}
      onValueChange={setFiles}
      maxFiles={10}
      maxSize={5 * 1024 * 1024}
      css={{ w: "full", maxW: "md" }}
      onUpload={onUpload}
      onFileReject={onFileReject}
      multiple
    >
      <FileUploadDropzone>
        <styled.div
          css={{
            display: "flex",
            flexDir: "column",
            alignItems: "center",
            gap: "1",
            textAlign: "center",
          }}
        >
          <styled.div
            css={{
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
              rounded: "full",
              borderWidth: "1px",
              p: "2.5",
            }}
          >
            <LuUpload className={css({ w: "6", h: "6", color: "muted.fg" })} />
          </styled.div>
          <styled.p css={{ fontWeight: "medium", textStyle: "sm" }}>
            Drag & drop files here
          </styled.p>
          <styled.p css={{ color: "muted.fg", textStyle: "xs" }}>
            Or click to browse (max 10 files, up to 5MB each)
          </styled.p>
        </styled.div>
        <FileUploadTrigger asChild>
          <Button variant="outline" size="sm" css={{ mt: "2", w: "fit" }}>
            Browse files
          </Button>
        </FileUploadTrigger>
      </FileUploadDropzone>
      <FileUploadList orientation="horizontal">
        {files.map((file, index) => (
          <FileUploadItem key={index} value={file} css={{ p: "0" }}>
            <FileUploadItemPreview css={{ w: "20", h: "20" }}>
              <FileUploadItemProgress variant="fill" />
            </FileUploadItemPreview>
            <FileUploadItemMetadata css={{ srOnly: true }} />
            <FileUploadItemDelete asChild>
              <Button
                variant="secondary"
                size="icon"
                css={{
                  position: "absolute",
                  top: "-1",
                  right: "-1",
                  w: "5",
                  h: "5",
                  rounded: "full",
                }}
              >
                <LuX className={css({ w: "3", h: "3" })} />
              </Button>
            </FileUploadItemDelete>
          </FileUploadItem>
        ))}
      </FileUploadList>
    </FileUpload>
  );
}

API Reference

FileUpload

The main container component for the file upload functionality.

PropTypeDefault
cssSystemStyleObject
value?File[]
defaultValue?File[]
onValueChange?(files: File[]) => void
onAccept?(files: File[]) => void
onFileAccept?(file: File) => void
onFileReject?(file: File, message: string) => void
onFileValidate?(file: File) => string | null | undefined
onUpload?(files: File[], options: { onProgress: (file: File, progress: number) => void; onSuccess: (file: File) => void; onError: (file: File, error: Error) => void }) => Promise<void> | void
accept?string
maxFiles?number
maxSize?number
dir?"ltr" | "rtl"
label?string
name?string
asChild?boolean
disabled?boolean
invalid?boolean
multiple?boolean
required?boolean

FileUploadDropzone

A container for drag and drop functionality.

PropTypeDefault
cssSystemStyleObject
asChild?boolean

FileUploadTrigger

A button that opens the file selection dialog.

PropTypeDefault
cssSystemStyleObject
asChild?boolean

FileUploadList

A container for displaying uploaded files.

PropTypeDefault
cssSystemStyleObject
orientation?"horizontal" | "vertical""vertical"
forceMount?boolean
asChild?boolean

FileUploadItem

Individual file item component.

PropTypeDefault
cssSystemStyleObject
valueFile
asChild?boolean

FileUploadItemPreview

Displays a preview of the file, showing an image for image files or an appropriate icon for other file types.

PropTypeDefault
cssSystemStyleObject
render?(file: File, fallback: () => ReactNode) => ReactNode
asChild?boolean

FileUploadItemMetadata

Displays file information such as name, size, and error messages.

PropTypeDefault
cssSystemStyleObject
size?"default" | "sm""default"
asChild?boolean

FileUploadItemProgress

Shows the upload progress for a file.

PropTypeDefault
cssSystemStyleObject
variant?"fill" | "circular" | "linear""linear"
size?number40
forceMount?boolean
asChild?boolean

FileUploadItemDelete

A button to remove a file from the list.

PropTypeDefault
cssSystemStyleObject
asChild?boolean

FileUploadClear

A button to clear all files from the list.

PropTypeDefault
cssSystemStyleObject
forceMount?boolean
asChild?boolean