File Dropzone
拖放上傳區:磁吸虛線邊框朝游標偏移、跑馬燈流動,檔案列表附環形進度。
把這個效果裝進你的專案
在專案資料夾執行指令,CLI 會加入原始碼與依賴套件,再依下方使用範例引用元件。
npx webberui@latest add file-dropzoneFile Dropzone 是用 React、Tailwind CSS 與 Motion 做的拖放上傳(drag and drop file upload)元件:拖曳檔案懸停時虛線邊框朝游標偏移並流動,下方檔案列表以 SVG 環形進度顯示每個檔案的上傳狀態。
Playground
即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。
<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 | — | 檔案未通過驗證時觸發 |
accept | string | — | 接受的檔案型別(同原生 accept),拖放也會套用驗證 |
multiple | boolean | true | 允許多檔;false 時新檔案取代舊檔案 |
maxFiles | number | — | 清單檔案數量上限 |
maxSize | number | — | 單一檔案大小上限(bytes) |
strength | number | 0.12 | 磁吸強度,邊框偏移量 = 游標相對中心距離 × strength(只作用於 center 模式) |
magnet | "center" | "proximity" | "center" | 磁吸模式:center 懸停時才偏移;proximity 拖曳檔案靠近就吸引並顯示跟隨光暈 |
proximityRadius | number | 180 | proximity 模式的感應半徑(px),距放置區邊緣在此範圍內線性增強吸引 |
rejectMessage | (file: File, reason: "type" | "size" | "count") => string | — | 自訂驗證失敗時播報給螢幕閱讀器的文字;未提供時使用內建訊息 |
label | string | "拖放檔案到這裡,或點擊選取" | 放置區主要文字 |
disabled | boolean | false | 停用整個放置區 |
細節
- 磁吸邊框:拖曳懸停時,虛線邊框以 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 也可以,從「手動」分頁複製原始碼即可。