Upload
Click or drag to select files; displays the file list and upload progress. Supports a real upload channel (action XHR / custom-request escape hatch), before-upload validation & transform, failure retry and cancel.
Basic Usage
Basic (select files)
Multiple & Limit
Multiple + max
max limits the maximum number of selectable files; accept filters file types. Files exceeding max are rejected and an oas-exceed-limit event is emitted.
Auto Upload
Auto upload (simulated progress)
With auto-upload, adding a file automatically starts uploading. When neither action nor custom-request is set, progress is simulated (built-in timer fallback, no network request); the progress bar reuses oas-progress.
Drag & Drop
Drag & drop upload
Drag files directly into the dashed area and release to add them (multiple supported); the area highlights while dragging. When disabled, the drag shows a not-allowed cursor and drops are ignored.
Picture Wall
list-type controls the list style: list (default, text rows), picture (rows with small thumbnails), picture-card (card thumbnail wall).
picture-card
Picture wall (picture-card)
Image files render thumbnails (URL.createObjectURL); non-image files show a file icon and name. Click a thumbnail to open the preview overlay (close with Esc or by clicking the mask); remove via the top-right ×, and hover shows preview/remove actions. A circular avatar card can be achieved with the CSS variable --oas-upload-card-radius: 50%.
On touch screens (pointer: coarse) there is no hover to rely on: the top-right remove × stays visible, the action overlay (preview/retry/cancel) stays visible on error/uploading cards, and the remove × / action buttons get a 44px hit target.
Remove, preview & max rejection
Picture wall · remove/preview/exceed
A 4th file is rejected by max="3" and triggers oas-exceed-limit with a warning; clicking a thumbnail triggers oas-preview.
picture rows with thumbnails
Rows with thumbnails (picture)
In list / picture modes, clicking the file name also opens the preview overlay (the name is a focusable button).
At the max
Reached max (pre-filled with 3)
Pre-filled to max="3"; further selections are rejected via oas-exceed-limit.
Disabled
Disabled
Real Upload Channel (action)
With action set, the built-in XHR channel is used: name (form file field, default file), data (extra form fields JSON), headers (request headers JSON), method (default POST) and with-credentials are passed through; progress/success/failure come from real XHR events.
action channel (with failure/retry demo)
The docs site has no real upload endpoint — /api/upload returns 404, which conveniently demonstrates the failure state: the row turns red with a retry button (⟳); clicking retry re-sends the request; while uploading you can click × to cancel (emits oas-cancel). Success emits oas-success (detail: { file, response }, where response is parsed JSON or raw text); failure emits oas-error (detail: { file, response, status }).
Custom Request Channel (custom-request)
Chunked uploads, object-storage direct upload, WebSocket and other non-standard channels are taken over by the custom-request function property (the component does not care about request strategy):
custom-request takes over the request
el.customRequest = ({ file, name, action, onProgress, onSuccess, onError }) => {
// send the request yourself; callback pacing is controlled by the host
onProgress({ percent: 60 })
onSuccess({ url: 'https://example.com/f.png' }) // or onError({ status: 500 })
return { abort: () => {/* cancel */} } // optional: a cancel button appears while uploading
}Manual Upload
Without auto-upload the component is in manual mode: after selecting files, call submit() (an alias of startUpload()) to upload them together.
Manual upload (submit together)
Before Upload (validation & transform)
The before-upload function property: return false to reject a file; return a new File to transform (rename/compress); async Promise is supported.
before-upload validation & transform
Image Crop (crop)
crop enables pre-upload image cropping. Activation conditions: list-type is picture / picture-card and accept includes image types — selecting / dropping / pasting an image file then opens a built-in lightweight crop dialog (self-drawn overlay, zero dependencies) instead of queueing directly:
- Move: drag the stage's empty area to pan the image; drag the crop frame to move it (arrow keys nudge when focused, Shift to speed up)
- Resize the frame: drag the bottom-right handle; set a fixed aspect ratio with
crop-aspect(e.g.1:1,16:9; invalid/missing = free ratio) - Zoom the image: mouse wheel on the stage, or the zoom in/out buttons (100% = cover baseline, up to 800%)
- Confirm: an offscreen canvas exports the cropped region at the source's natural resolution; the result file is queued in place of the original and
oas-cropis dispatched (detail: { file, blob }) - Cancel / Esc: closes without queueing; with multi-select, files are cropped one by one (cancel skips the current one and continues), non-image files queue directly
crop before upload (picture-card + 1:1)
Combined with before-upload, the hook runs first (validation/transform) and the transformed result enters cropping; queuing still respects max / max-size / replace (a cropped result may be smaller than the original — size limits are judged at queue time).
File Size Limit (max-size)
max-size accepts bytes or a unit form (512KB / 2MB / 1GB); oversized files are rejected with oas-exceed-limit (detail.type === 'size', including the maxSize in bytes).
max-size (100KB)
Replace on Exceed
With replace, exceeding the count limit no longer rejects: the oldest file is removed to make room (for max=1 / single-select this is "new replaces old" — no more "delete first, then select" for avatar-like scenarios).
replace (max=1, selecting a new file replaces the old)
Directory & Paste Upload
directory enables directory upload (sets webkitdirectory on the input; the list shows relative paths). paste enables paste upload (Ctrl+V clipboard files directly on the component — great for screenshots; off by default).
Directory upload (directory)
Paste upload (paste)
Trigger, Tip & Item Customization
slot="trigger": replaces the drop zone content (click/drag semantics stay on the zone container — the zone itself is the focusablerole="button"control, so do not nest interactive elements (buttons/links) inside the trigger content (nested-interactive is an axe violation); presentational content only)tipattribute ortemplate[slot="tip"]: secondary hint text below the zone (attribute wins)template[slot="item"]: cloned into everylist/picturerow;[data-item-name]/[data-item-size]bind the file name and size automatically
trigger / tip slots
item slot (custom list rows)
Echo Back & Hiding the List
- The
files/defaultFilesproperties accept{ name, url, size? }echo records (status done, preview uses the remote url) — the initial-value semantics for edit scenarios show-file-list="false"hides the built-in list, leaving only the trigger area (premise for host-drawn previews)
Echo back (defaultFiles)
Avatar (show-file-list=false + custom trigger + before-upload validation)
Events
Change events
Listen to oas-change / oas-remove / oas-upload / oas-progress:
oas-upload: progress,detail: { file, percent, status }(statusis onlyuploading/done; failures are not reported here but viaoas-error)oas-progress: upload progress callback (PRD P2),detail: { file, percent }— internal progress already drives the UI; this event adds the outbound channel (emitted by real / mock / custom-request channels alike, with 100 on success)oas-success/oas-error: upload success/failure (real channel),detail: { file, response, status? }oas-retry/oas-cancel: retry started / upload cancelled,detail: { file }oas-exceed-limit: rejection,detail: { files, max, total }(count) or{ files, type: 'size', maxSize, total }(size)oas-remove: file removed,detail: { file, index, replaced? }(replaced: truemeans removed by thereplacesemantics)oas-crop: crop confirmed,detail: { file, blob }(fileis the original file,blobthe offscreen canvas crop result)
Methods: submit() (manual upload), startUpload() (equivalent), abort(file?) (cancel one/all in-flight uploads).
oas-progress feedback
Third-party Composition Scenarios
These scenarios are not part of the component API; compose them via existing channels:
// Large-file chunking / object-storage direct upload: take over via custom-request
// (chunking strategy is a business decision)
el.customRequest = async ({ file, onProgress, onSuccess, onError }) => {
const chunks = sliceFile(file, 5 * 1024 * 1024) // chunk it yourself
for (let i = 0; i < chunks.length; i++) {
await uploadChunk(chunks[i], i)
onProgress({ percent: ((i + 1) / chunks.length) * 100 })
}
onSuccess(await assembleChunks(file))
}- Avatar cropping: covered by the built-in
cropchannel (see "Image Crop"); to use a third-party cropper instead, return the cropped newFilefrombefore-upload - List drag sorting: customize rows via the
itemslot + reorderfileswith a third-party dnd library - Circular avatar card: CSS variable
--oas-upload-card-radius: 50%(no separate enum)
required
required (native validation chain)
required drives the native validation chain (form-associated): when unfilled, checkValidity() returns false (valueMissing) and native form submission is blocked; it recovers to :valid once filled.
The current form value is available through the public value property for read/write: el.value returns the submittable File array ({name,url} echo records carry no file body and are not included; an empty array when there are no files); el.value = [file] replaces the whole file list (same controlled channel as the files property, no events dispatched).
API
oas-upload
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
accept | Accepted file types | string | — |
action | Upload endpoint URL (real XHR channel; without it and customRequest, progress is simulated) | string | — |
auto-upload | Auto-simulate upload after adding | boolean | — |
crop | Pre-upload image cropping (picture/picture-card + image accept): picked images open the built-in crop dialog first; the canvas crop result is queued on confirm | boolean | — |
crop-aspect | Fixed crop aspect ratio (e.g. 1:1, 16:9); invalid/missing = free ratio | string | — |
data | Extra form fields (JSON string, submitted with the file) | — | — |
directory | Directory upload (recursive folder intake) | boolean | — |
disabled | Disabled | boolean | — |
headers | Extra request headers (JSON string) | — | — |
list-type | List style: list (default) / picture (rows with thumbnails) / picture-card (card thumbnail wall) | string | list |
max | Maximum number of files | string | 0 |
max-size | Per-file size limit (bytes or 2KB/1MB/1GB; over limit fires oas-exceed size form) | string | — |
method | HTTP method (default POST) | string | POST |
multiple | Multiple selection | boolean | — |
name | Form field name for the file (default file) | string | — |
paste | Paste upload (off by default; clipboard files enter the list when on) | boolean | — |
replace | Replace semantics at max-count=1 (new file replaces the old instead of being rejected) | boolean | — |
required | Required marker (drives the native valueMissing validation chain; not passed through to the inner control) | boolean | — |
show-file-list | File list visibility ("false" hides it, keeping only the drop zone) | string | true |
tip | Drop-zone tip text (same as template[slot="tip"] for rich content) | string | — |
with-credentials | Cross-origin credentials | boolean | — |
Property (JS property; see each row for attribute reflection)
| Property | Description | Type | Default |
|---|---|---|---|
files | Controlled file list (property channel; assigning re-renders) | Array<File | UploadEchoFile> | [] |
value | Current value: the submittable File[] (file-body entries only; echo records without a body are excluded) | File[] | null | [] |
Events
| Event | Description |
|---|---|
oas-cancel | Upload cancelled, detail: { file } (cancel button on the item) |
oas-change | File list change, detail: { files } |
oas-crop | Crop confirmed, detail: { file, blob } (file = original file, blob = offscreen canvas result) |
oas-error | Upload failed, detail: { file, response?, status? } (retry button appears) |
oas-exceed | [Compat alias] Same as oas-exceed-limit; will be removed later, detail: { files: rejected, max, total: next.length } | { files: sizeRejected, type: 'size', maxSize, total: next.length } |
oas-exceed-limit | Adding files rejected due to limit (canonical, aligned with checkbox-group/select/toggle-group), detail: { files, max, total }; emitted for both count and size rejections (size rejections also carry type: 'size' and maxSize) |
oas-preview | Preview overlay opened, detail: { file, url } |
oas-progress | Upload progress { file, percent }, emitted across all channels with 100 on success, detail: { file, percent: cur.percent } | { file, percent: 100 } | { file: f, percent: st.percent } |
oas-remove | File removed, detail: { file, index } |
oas-retry | Retry clicked, detail: { file } (re-upload after failure) |
oas-success | Upload succeeded, detail: { file, response } |
oas-upload | Upload progress, detail: { file, percent, status } |
Slots
| Name | Description |
|---|---|
template[slot="item"] | Custom file row/card ([data-item-name]/[data-item-size] bindings) |
template[slot="tip"] | Drop-zone tip rich content (tip attribute wins) |
template[slot="trigger"] | Self-drawn trigger-area template (native slot does not render template elements — cloned by the component; clicking the zone still opens the file picker) |
trigger | Replace the drop-zone content (zone semantics kept) |
CSS Variables
| CSS Variable | Default |
|---|---|
--oas-upload-card-radius | var(--oas-radius-md) |
⚠️ Three limitations of
template[slot="trigger"]: ① the template content is cloned into the shadow DOM — host-page stylesheets do not apply to cloned content (use inline styles / oas components / CSS variables instead); ② the clone is a static snapshot taken at component update() — template changes after connect do not auto-refresh (write the template before mounting the component, or nudge any attribute afterwards to trigger a re-sync); ③cloneNodedoes not carry event listeners —@click/addEventListenerbound on the template nodes are lost in the clone; use component events (oas-change, etc.) or delegate on the cloned zone container for interactivity.
Keyboard: Enter / Space on the drop zone opens the file picker; remove buttons are focusable; the preview overlay closes with Esc.