輸入元件

File Dropzone

拖放上傳區:磁吸虛線邊框朝游標偏移、跑馬燈流動,檔案列表附環形進度。

把這個效果裝進你的專案

在專案資料夾執行指令,CLI 會加入原始碼與依賴套件,再依下方使用範例引用元件。

npx webberui@latest add file-dropzone

還沒有專案,或安裝遇到問題?看快速開始 →

File Dropzone 是用 React、Tailwind CSS 與 Motion 做的拖放上傳(drag and drop file upload)元件:拖曳檔案懸停時虛線邊框朝游標偏移並流動,下方檔案列表以 SVG 環形進度顯示每個檔案的上傳狀態。

Playground

即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。

0.12
編輯中的呼叫預設屬性
<FileDropzone/>

完整範例包含預覽的排版與資料。先安裝元件,再把範例檔貼入你的專案。

適用情境

  • 後台的資料匯入頁,例如上傳 CSV、Excel 或發票 PDF。
  • 會員資料的大頭貼或證件照上傳,限制單一檔案與圖片格式。
  • 雲端硬碟、素材庫這類以上傳為主要動作的頁面。
  • 表單裡的附件欄位,需要顯示每個檔案的進度、成功與失敗。
  • 不適合:放在聊天輸入框上方、需要縮圖 chip 與拖曳排序的附件列,改用 AI Attachment Tray。

安裝

npx webberui@latest add https://webberui.com/r/file-dropzone.json

或在 components.json 設定 registries 後,改用 @webberui/file-dropzone 安裝。

使用

import { FileDropzone } from "@/components/ui/file-dropzone";

<FileDropzone
  accept="image/*,.pdf"
  maxFiles={4}
  maxSize={10 * 1024 * 1024}
  onFilesAdded={(files) => console.log(files)}
/>

接上真實上傳

未提供 upload 時元件以模擬進度展示動畫;接上後端時傳入非同步函式並回報進度:

<FileDropzone
  upload={async (file, onProgress) => {
    await uploadToServer(file, {
      onUploadProgress: (e) => onProgress(e.loaded / e.total),
    });
  }}
/>

resolve 顯示成功打勾,reject 顯示失敗狀態。

範例

上傳到預先簽章的網址

先向後端取得 presigned URL,再用 XMLHttpRequest 直接上傳到物件儲存,並把 loaded / total 交給 onProgress。

import { FileDropzone } from "@/components/ui/file-dropzone";

async function uploadToStorage(file: File, onProgress: (p: number) => void) {
  const res = await fetch("/api/upload-url", {
    method: "POST",
    body: JSON.stringify({ name: file.name, type: file.type }),
  });
  const { url } = await res.json();

  await new Promise<void>((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.upload.onprogress = (e) => e.lengthComputable && onProgress(e.loaded / e.total);
    xhr.onload = () => (xhr.status < 300 ? resolve() : reject(new Error(String(xhr.status))));
    xhr.onerror = () => reject(new Error("network"));
    xhr.open("PUT", url);
    xhr.setRequestHeader("Content-Type", file.type);
    xhr.send(file);
  });
}

<FileDropzone accept=".csv,.xlsx" maxSize={20 * 1024 * 1024} upload={uploadToStorage} />

單一大頭貼上傳

multiple={false} 時新檔案會取代舊檔案;搭配 onReject 告訴使用者為什麼檔案沒有被接受。

import * as React from "react";
import { FileDropzone, type FileDropzoneRejectReason } from "@/components/ui/file-dropzone";

const reasons: Record<FileDropzoneRejectReason, string> = {
  type: "請上傳 JPG 或 PNG 圖片",
  size: "圖片需小於 2 MB",
  count: "只能上傳一張圖片",
};

export function AvatarUpload() {
  const [error, setError] = React.useState<string | null>(null);

  return (
    <div className="max-w-sm">
      <FileDropzone
        label="拖放大頭貼,或點擊選取"
        accept="image/jpeg,image/png"
        multiple={false}
        maxSize={2 * 1024 * 1024}
        onFilesAdded={() => setError(null)}
        onReject={(_, reason) => setError(reasons[reason])}
        upload={(file, onProgress) => uploadAvatar(file, onProgress)}
      />
      {error && <p className="mt-2 text-sm text-red-600">{error}</p>}
    </div>
  );
}

關掉磁吸效果

strength={0} 讓邊框在拖曳時保持不動,適合放在密集的表單版面裡。

<FileDropzone strength={0} label="拖放合約 PDF" accept=".pdf" />

鄰近磁吸與跟隨光暈

magnet="proximity" 改在整個視窗監聽拖曳:檔案拖到放置區邊緣 proximityRadius(預設 180px)內就開始吸引,距離越近位移越大(最多 10px,朝游標方向),邊框縮放從 1 漸進到 1.01,進入放置區時到 1.025;同時有一圈模糊光暈跟著游標,200ms 淡入。適合整頁只有一個上傳目標、希望使用者「拖過去就知道丟哪」的頁面。

<FileDropzone
  magnet="proximity"
  proximityRadius={220}
  accept="image/*"
  rejectMessage={(file, reason) =>
    reason === "type" ? `${file.name} 不是圖片` : `${file.name} 無法加入`
  }
/>

Props

Prop型別預設值說明
onFilesAdded(files: File[]) => void—檔案通過驗證並加入清單時觸發
upload(file: File, onProgress: (p: number) => void) => Promise<void>—自訂上傳邏輯,onProgress 回報 0–1;未提供時模擬進度
onRemove(file: File) => void—從清單移除檔案時觸發
onReject(file: File, reason: "type" | "size" | "count") => void—檔案未通過驗證時觸發
acceptstring—接受的檔案型別(同原生 accept),拖放也會套用驗證
multiplebooleantrue允許多檔;false 時新檔案取代舊檔案
maxFilesnumber—清單檔案數量上限
maxSizenumber—單一檔案大小上限(bytes)
strengthnumber0.12磁吸強度,邊框偏移量 = 游標相對中心距離 × strength(只作用於 center 模式)
magnet"center" | "proximity""center"磁吸模式:center 懸停時才偏移;proximity 拖曳檔案靠近就吸引並顯示跟隨光暈
proximityRadiusnumber180proximity 模式的感應半徑(px),距放置區邊緣在此範圍內線性增強吸引
rejectMessage(file: File, reason: "type" | "size" | "count") => string—自訂驗證失敗時播報給螢幕閱讀器的文字;未提供時使用內建訊息
labelstring"拖放檔案到這裡,或點擊選取"放置區主要文字
disabledbooleanfalse停用整個放置區

細節

  • 磁吸邊框:拖曳懸停時,虛線邊框以 spring 朝游標方向偏移(上限 10px)並輕微放大,同時虛線以跑馬燈方式流動
  • 拖曳進出子元素會連發 dragenter/dragleave,內部以深度計數避免邊框狀態閃爍
  • 進度環以 SVG stroke-dashoffset 搭配 spring 平滑推進,完成後彈出打勾、失敗顯示警示
  • accept 驗證支援副檔名(.pdf)、萬用字元(image/*)與完整 MIME,拖放與點選皆套用
  • 檔案選取後會清空 input value,同一檔案可重複選取
  • proximity 模式在 window 監聽 dragover,量測游標到放置區矩形邊緣的最短距離;吸引力在感應半徑內線性變化,位移與縮放以 stiffness: 280、damping: 24、mass: 0.65 的 spring 跟隨,拖出視窗、放下或取消拖曳時回到原位

可及性

  • 放置區為 role="button",可 Tab 聚焦,Enter/Space 開啟檔案選取器
  • 進度環帶 role="progressbar" 與 aria-valuenow,成功/失敗圖示有對應 aria-label
  • 上傳中的檔案數量透過 aria-live="polite" 播報給螢幕閱讀器
  • 驗證失敗(型別/大小/數量)與上傳失敗會透過另一個 aria-live="assertive" 區域立即播報,文字可用 rejectMessage 自訂
  • 使用者系統開啟「減少動態效果」時,磁吸偏移、跑馬燈與進出場動畫停用,進度環直接跳至目前值;proximity 模式不掛 window 監聽器、不顯示光暈

常見問題

沒有傳 upload 時會發生什麼事?

元件會用計時器模擬上傳進度,方便預覽動畫,不會真的送出任何請求。正式使用時請傳入 upload,resolve 顯示成功打勾,reject 顯示失敗。

可以在 Next.js App Router 使用嗎?

可以。原始碼開頭已有 "use client",在 Server Component 頁面直接 import 即可;upload 函式裡可以呼叫你的 Route Handler 或 Server Action。

accept 怎麼驗證?

支援副檔名(.pdf)、萬用字元(image/*)與完整 MIME(image/png),以逗號分隔。驗證依序檢查型別、大小(maxSize,單位 bytes)與數量(maxFiles),點選與拖放都會套用,未通過時呼叫 onReject。

鍵盤與螢幕閱讀器可以使用嗎?

可以。放置區是 role="button",Tab 聚焦後按 Enter 或 Space 開啟檔案選取器;進度環帶 role="progressbar",上傳中的檔案數量會透過 aria-live 播報。系統開啟「減少動態效果」時,磁吸、跑馬燈與進出場動畫都會停用。

需要哪些依賴?

motion、lucide-react、clsx 與 tailwind-merge。不用 CLI 也可以,從「手動」分頁複製原始碼即可。