Upload Sources
upup collects files from nine sources. One prop — sources — decides which of
them appear in the panel, and each one is a self-contained view inside the same
fixed-height uploader.
Live demo — drag a file in. Demo mode: nothing leaves your browser.
The sources prop
sources is an array of source ids. It drives the chip grid on the idle panel:
every id you list gets a chip, in the order below (the registry order, not the
order you pass), and nothing else is rendered.
| Id | Chip label | What it opens |
|---|---|---|
local | My Device | The OS file picker (no in-panel view) |
googleDrive | Google Drive | Google Drive browser |
oneDrive | OneDrive | OneDrive browser |
dropbox | Dropbox | Dropbox browser |
box | Box | Box browser |
url | Link | URL import form |
camera | Camera | Live webcam capture |
microphone | Audio | Microphone recorder |
screen | Screen Capture | Screen recorder |
When sources is omitted the default set is the five that need no OAuth
credentials: local, url, camera, microphone, screen. Cloud drives are
opt-in because they also need a cloudDrives config (client mode) or a
configured server (server mode).
<UpupUploader
uploadEndpoint="/api/upload-token"
sources={['local', 'camera', 'googleDrive']}
/>Ids are normalized against the known set and anything unrecognized is dropped silently — a typo removes a chip rather than throwing.
Clicking My Device opens the OS picker immediately. Every other chip swaps the panel to that source's view, which carries a header with the source name and a Back button that returns to the chip grid.
Chip density
The chip grid is capped at a fixed width, so it fits four regular chips per row. Up to 8 sources the chips render at the regular size (rows of 4). At 9 sources — the full set — they switch to the compact size, which fits five per row. Nothing else changes: same chips, same labels, same order.
In mini mode the chip grid is not rendered at all; the panel shows a single
browse button.
Local files
local covers four ways to get files off the machine: the browse button, the
file picker behind the My Device chip, drag-and-drop, and clipboard paste.
Browse
The idle panel's browse files link and the My Device chip both open the same
hidden file input. It carries your allowedFileTypes as its accept attribute
and is multiple whenever maxFiles is greater than 1.
Drag and drop
Drag-and-drop is on by default; disableDragDrop turns it off while leaving
browse and the chips working. Dropping is also suppressed automatically while a
source view is open or while an upload is in flight.
One special case: dropping files onto an open cloud-drive picker is rejected with a toast rather than silently ignored — that view browses a remote drive and cannot accept OS drops.
Paste
Clipboard paste is off by default. Set enablePaste to accept files pasted with
Ctrl+V / Cmd+V:
<UpupUploader uploadEndpoint="/api/upload-token" enablePaste />The listener sits on the uploader panel, so the paste has to land in the panel
(it or something inside it must have focus) — there is no document-wide
listener. Clipboard images usually arrive nameless or as image.png; those are
renamed to pasted-<timestamp>.<ext> so a batch of screenshots doesn't collapse
into one repeated filename. A file that already has a real name keeps it.
Paste has its own switch: disableDragDrop does not disable it.
Folders
Folder upload is configured with folderUpload, and both of its flags default
to false:
<UpupUploader
uploadEndpoint="/api/upload-token"
folderUpload={{ allowDrop: true, showSelectFolderButton: true }}
/>allowDrop lets a dropped folder be traversed recursively — every file inside
is collected, and each one is tagged with its relativePath inside the dropped
folder. With allowDrop left off, a dropped folder is skipped: the uploader
raises a warning through onWarn and emits folder-drop-blocked with the
number of loose files it still accepted from the same drop. If the drop was
folders only, nothing is added.
showSelectFolderButton adds a select a folder action next to the browse
link. It uses the browser's directory picker where available and falls back to a
webkitdirectory file input elsewhere; either way the selected files carry
their relativePath.
Camera
The camera view mounts a live webcam preview via getUserMedia. The browser
owns the permission prompt — upup does not pre-check it.
The flow is capture-then-confirm: Capture freezes a still from the stream and
shows it as a preview with a small delete button to retake, and Add image
turns that still into a File and returns you to the chip grid. Captured stills
get a generated filename (the data URL carries no name), with the extension
derived from the image MIME type. A switch camera button flips between the
environment- and user-facing cameras; the view starts on the environment camera.
Permission behavior, as implemented: the camera view has no in-panel error state. If the user denies access, the preview area simply stays empty and Capture produces nothing — no capture, no event, no message. If you need to surface a denial to the user, do it outside the uploader.
Microphone
The microphone view records audio with getUserMedia plus MediaRecorder.
Idle shows a mic button; recording shows a live waveform rendered from the
stream, a pulsing indicator, and an m:ss timer. Stop Recording ends the
capture and gives you a standard audio player to review the take, plus
Discard (throw it away, return to idle) and Add Recording (add it as
recording-<timestamp>.webm, or .ogg where WebM is unavailable, and return to
the chip grid).
If permission is denied or no device is available, the view is replaced by the message "Microphone access denied. Please allow microphone access and try again." There is no retry button here — leave the view with Back and re-enter it to try again.
Screen capture
The screen view records the screen with getDisplayMedia, requesting video and
audio. The browser's own picker chooses which screen, window, or tab is
shared — upup cannot preselect it.
While recording, the panel shows a live preview of the shared surface with a
REC chip and timer over it. Stopping works two ways: the Stop Recording
button, or the browser's own "stop sharing" control — ending the track from
outside stops the recording too, so the two never drift apart. Then it's the
same review step as audio: play it back, Discard, or Add Recording to add
screen-recording-<timestamp>.webm.
If the user cancels the share dialog or denies the permission, the view is replaced by "Screen sharing was cancelled or denied. Please try again." with a Try Again button that reopens the picker.
Recorded video and the live preview are sized to fit the panel rather than stretch it — the uploader panel is a fixed-height container, so tall or wide media is letterboxed, never clipped.
Import from a URL
The url view takes a URL and fetches it from the browser, with the page's
own origin. It is a plain fetch() — there is no server-side proxy, and this is
identical in client mode and server mode.
That makes CORS the deciding factor: the remote server must allow the browser to read the response. A URL that opens fine in a browser tab will still fail here unless its server sends permissive CORS headers. A cross-origin host that doesn't opt in is not importable this way; proxy it through your own backend and hand the uploader a same-origin URL instead.
The filename is derived in order of preference: the response's
Content-Disposition filename, then the last path segment of the URL, then a
generated UUID with an extension inferred from the content type. A non-2xx
response surfaces as an error message through onError
(Failed to fetch URL: <status>).
Cloud drives
Google Drive, OneDrive, Dropbox, and Box each render a file browser with
sign-in, folder navigation, multi-select, and search where the provider supports
it. Which credentials they need — and whether OAuth tokens live in the browser
or on your server — depends
entirely on the upload mode, so their setup lives with the mode docs rather than
here: start with Client Mode vs Server Mode, then
Server Mode Setup if you are proxying
through @upupjs/server.
Source events
Beyond the callback props, each source emits events on the core event bus:
| Event | Payload | Fired when |
|---|---|---|
source-click | { sourceId } | A chip is clicked |
source-view-cancel | { sourceId } | A source view is closed with Back |
browse-files | — | The browse link opens the file picker |
folder-select | { count } | A folder is picked (count is 0 on the input fallback) |
drag-over | — | A drag enters the panel |
drag-leave | — | A drag leaves the panel |
drop | { files } | Files are dropped |
folder-drop-blocked | { acceptedFiles } | A folder was dropped with allowDrop off |
paste | { files } | Files are pasted |
camera-capture | { dataUrl } | A still is captured |
camera-confirm | { file } | The captured still is added |
url-submit | { url } | The URL form is submitted |
url-fetch | { file } | A URL fetch succeeds |
url-fetch-cancel | { url } | An in-flight URL fetch is aborted |
Two notes on the URL events. url-fetch also fires when a camera still is
confirmed — the camera reuses the same fetch helper to turn its data URL into a
File. And url-fetch-cancel fires on abort, which today happens when the view
unmounts mid-fetch (leaving via Back), not from a user-facing cancel button.
The microphone and screen-capture views emit no source-specific events; their recordings surface through the normal file-added path.
Reaching these events
These fire on the uploader's internal core, and the UpupUploader component
does not currently expose it — the ref surface is useUpload() only.
Subscribe with core.on(...) where you own the core: React's
useUpupUpload() headless hook returns both on and core, and the
Vanilla createUploader() handle does the same. See Headless
Usage.
Next steps
- Client Mode vs Server Mode — where uploads and drive API calls actually run.
- Theming — restyle the chip grid and each source view through tokens and slots.
- Headless Usage — drive the sources yourself and subscribe to the events above.