File Upload
Upload files via drag-and-drop or file browser. Supports single and multiple files with progress indicators, previews, and validation.
Installation
File Upload uses Standard’s base styles and JavaScript for drag-and-drop interactions.
<link rel="stylesheet" href="/css/standard.css">
<script src="/js/standard.js" defer></script>
Usage
Basic Drop Zone
Click to upload or drag and drop
PNG, JPG, GIF up to 10MB
<div class="FileUpload">
<div class="FileUpload-dropzone">
<svg class="FileUpload-icon" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
</svg>
<p class="FileUpload-text">
<span class="FileUpload-link">Click to upload</span> or drag and drop
</p>
<p class="FileUpload-hint">PNG, JPG, GIF up to 10MB</p>
<input type="file" class="FileUpload-input" />
</div>
</div>
Multiple Files
Add multiple to accept multiple files.
Click to upload or drag and drop
Up to 5 files, 10MB each
<div class="FileUpload">
<div class="FileUpload-dropzone">
<svg class="FileUpload-icon" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
</svg>
<p class="FileUpload-text">
<span class="FileUpload-link">Click to upload</span> or drag and drop
</p>
<p class="FileUpload-hint">Up to 5 files, 10MB each</p>
<input type="file" class="FileUpload-input" multiple />
</div>
</div>
Variants
Compact
Smaller drop zone for inline usage.
<div class="FileUpload FileUpload--compact">
<div class="FileUpload-dropzone">
<svg class="FileUpload-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
</svg>
<span class="FileUpload-text">
<span class="FileUpload-link">Upload file</span>
</span>
<input type="file" class="FileUpload-input" />
</div>
</div>
With Border
More prominent border style.
Click to upload or drag and drop
SVG, PNG, JPG or GIF (max. 800x400px)
<div class="FileUpload FileUpload--bordered">
<div class="FileUpload-dropzone">
<svg class="FileUpload-icon" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
</svg>
<p class="FileUpload-text">
<span class="FileUpload-link">Click to upload</span> or drag and drop
</p>
<p class="FileUpload-hint">SVG, PNG, JPG or GIF (max. 800x400px)</p>
<input type="file" class="FileUpload-input" accept="image/*" />
</div>
</div>
Image Preview
Shows thumbnail preview for uploaded images.
Change image
PNG, JPG up to 5MB
<div class="FileUpload FileUpload--preview">
<div class="FileUpload-dropzone">
<div class="FileUpload-preview">
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120' viewBox='0 0 120 120'%3E%3Crect fill='%23e5e7eb' width='120' height='120'/%3E%3Cpath fill='%239ca3af' d='M45 50h30v20H45z'/%3E%3Ccircle fill='%239ca3af' cx='52' cy='45' r='5'/%3E%3C/svg%3E" alt="Preview" class="FileUpload-thumbnail" />
</div>
<div class="FileUpload-content">
<p class="FileUpload-text">
<span class="FileUpload-link">Change image</span>
</p>
<p class="FileUpload-hint">PNG, JPG up to 5MB</p>
</div>
<input type="file" class="FileUpload-input" accept="image/*" />
</div>
</div>
File List
Display uploaded files with actions.
Basic File List
Add more files
-
document.pdf 2.4 MB
-
photo.jpg 1.2 MB
<div class="FileUpload">
<div class="FileUpload-dropzone">
<svg class="FileUpload-icon" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
</svg>
<p class="FileUpload-text"><span class="FileUpload-link">Add more files</span></p>
<input type="file" class="FileUpload-input" multiple />
</div>
<ul class="FileUpload-list">
<li class="FileUpload-item">
<svg class="FileUpload-itemIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
</svg>
<div class="FileUpload-itemInfo">
<span class="FileUpload-itemName">document.pdf</span>
<span class="FileUpload-itemSize">2.4 MB</span>
</div>
<button class="FileUpload-itemRemove" aria-label="Remove file">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</li>
<li class="FileUpload-item">
<svg class="FileUpload-itemIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<polyline points="21 15 16 10 5 21"/>
</svg>
<div class="FileUpload-itemInfo">
<span class="FileUpload-itemName">photo.jpg</span>
<span class="FileUpload-itemSize">1.2 MB</span>
</div>
<button class="FileUpload-itemRemove" aria-label="Remove file">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</li>
</ul>
</div>
With Progress
Show upload progress for each file.
-
large-video.mp465% · 12.4 MB of 19.1 MB
-
report.pdf Complete · 4.2 MB
-
corrupted.zip Upload failed · File corrupted
<ul class="FileUpload-list">
<li class="FileUpload-item FileUpload-item--uploading">
<svg class="FileUpload-itemIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
</svg>
<div class="FileUpload-itemInfo">
<span class="FileUpload-itemName">large-video.mp4</span>
<div class="FileUpload-progress">
<div class="FileUpload-progressBar" style="width: 65%"></div>
</div>
<span class="FileUpload-itemSize">65% · 12.4 MB of 19.1 MB</span>
</div>
<button class="FileUpload-itemRemove" aria-label="Cancel upload">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</li>
<li class="FileUpload-item FileUpload-item--complete">
<svg class="FileUpload-itemIcon FileUpload-itemIcon--success" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20 6 9 17 4 12"/>
</svg>
<div class="FileUpload-itemInfo">
<span class="FileUpload-itemName">report.pdf</span>
<span class="FileUpload-itemSize">Complete · 4.2 MB</span>
</div>
<button class="FileUpload-itemRemove" aria-label="Remove file">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</li>
<li class="FileUpload-item FileUpload-item--error">
<svg class="FileUpload-itemIcon FileUpload-itemIcon--error" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="8" x2="12" y2="12"/>
<line x1="12" y1="16" x2="12.01" y2="16"/>
</svg>
<div class="FileUpload-itemInfo">
<span class="FileUpload-itemName">corrupted.zip</span>
<span class="FileUpload-itemSize FileUpload-itemSize--error">Upload failed · File corrupted</span>
</div>
<button class="FileUpload-itemRetry" aria-label="Retry upload">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="23 4 23 10 17 10"/>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
</svg>
</button>
</li>
</ul>
States
Drag Over
Active state when files are dragged over the drop zone.
Drop files here
<div class="FileUpload">
<div class="FileUpload-dropzone is-dragover">
<svg class="FileUpload-icon" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
</svg>
<p class="FileUpload-text">Drop files here</p>
<input type="file" class="FileUpload-input" />
</div>
</div>
Disabled
Upload disabled
Contact admin for access
<div class="FileUpload FileUpload--disabled">
<div class="FileUpload-dropzone">
<svg class="FileUpload-icon" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
</svg>
<p class="FileUpload-text">Upload disabled</p>
<p class="FileUpload-hint">Contact admin for access</p>
<input type="file" class="FileUpload-input" disabled />
</div>
</div>
Error
File too large
Maximum file size is 10MB
<div class="FileUpload FileUpload--error">
<div class="FileUpload-dropzone">
<svg class="FileUpload-icon" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="8" x2="12" y2="12"/>
<line x1="12" y1="16" x2="12.01" y2="16"/>
</svg>
<p class="FileUpload-text">File too large</p>
<p class="FileUpload-hint">Maximum file size is 10MB</p>
<input type="file" class="FileUpload-input" />
</div>
</div>
Common Patterns
Profile Avatar Upload
Profile Photo
JPG or PNG, max 2MB
<div style="display: flex; align-items: center; gap: var(--space-4); max-width: 400px;">
<div style="width: 80px; height: 80px; border-radius: 50%; background: linear-gradient(135deg, oklch(60% 0.15 250), oklch(50% 0.2 280)); display: flex; align-items: center; justify-content: center; color: white; font-size: 1.5rem; font-weight: 600;">JD</div>
<div>
<p style="font-weight: 500; margin-bottom: var(--space-2);">Profile Photo</p>
<div style="display: flex; gap: var(--space-2);">
<button class="Button Button--secondary Button--small">Upload</button>
<button class="Button Button--ghost Button--small" style="color: oklch(55% 0.2 25);">Remove</button>
</div>
<p style="font-size: 0.8rem; color: var(--fg-3); margin-top: var(--space-1);">JPG or PNG, max 2MB</p>
</div>
</div>
Form with File Attachment
<div style="max-width: 400px;">
<div style="margin-bottom: var(--space-4);">
<label style="display: block; font-weight: 500; margin-bottom: var(--space-1);">Subject</label>
<input type="text" class="Input" placeholder="Brief description" style="width: 100%;">
</div>
<div style="margin-bottom: var(--space-4);">
<label style="display: block; font-weight: 500; margin-bottom: var(--space-1);">Attachments</label>
<div class="FileUpload FileUpload--compact">
<div class="FileUpload-dropzone">
<svg class="FileUpload-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>
</svg>
<span class="FileUpload-text"><span class="FileUpload-link">Attach files</span></span>
<input type="file" class="FileUpload-input" multiple />
</div>
</div>
</div>
<button class="Button Button--primary">Submit Ticket</button>
</div>
Multi-file Upload with Progress
Upload files or drag and drop
-
design-v2.fig Complete · 8.1 MB
-
assets.zip40% · 3.2 MB of 8 MB
<div style="max-width: 450px;">
<div class="FileUpload">
<div class="FileUpload-dropzone" style="padding: var(--space-4);">
<svg class="FileUpload-icon" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>
</svg>
<p class="FileUpload-text"><span class="FileUpload-link">Upload files</span> or drag and drop</p>
<input type="file" class="FileUpload-input" multiple />
</div>
<ul class="FileUpload-list">
<li class="FileUpload-item FileUpload-item--complete">
<svg class="FileUpload-itemIcon FileUpload-itemIcon--success" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
<div class="FileUpload-itemInfo">
<span class="FileUpload-itemName">design-v2.fig</span>
<span class="FileUpload-itemSize">Complete · 8.1 MB</span>
</div>
</li>
<li class="FileUpload-item FileUpload-item--uploading">
<svg class="FileUpload-itemIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
<div class="FileUpload-itemInfo">
<span class="FileUpload-itemName">assets.zip</span>
<div class="FileUpload-progress"><div class="FileUpload-progressBar" style="width: 40%"></div></div>
<span class="FileUpload-itemSize">40% · 3.2 MB of 8 MB</span>
</div>
</li>
</ul>
</div>
</div>
Customization
CSS Variables
.FileUpload {
/* Dropzone */
--file-upload-bg: var(--surface-1);
--file-upload-border: var(--border-default);
--file-upload-border-hover: var(--bd-s);
--file-upload-border-active: var(--color-primary);
--file-upload-radius: var(--radius-lg);
--file-upload-padding: var(--space-8);
/* Colors */
--file-upload-icon-color: var(--text-muted);
--file-upload-text-color: var(--text-default);
--file-upload-hint-color: var(--text-muted);
--file-upload-link-color: var(--color-primary);
/* Progress */
--file-upload-progress-bg: var(--surface-2);
--file-upload-progress-fill: var(--color-primary);
/* States */
--file-upload-error-color: var(--color-error);
--file-upload-success-color: var(--color-success);
}
Custom Styling Example
/* Circular avatar upload */
.FileUpload--avatar .FileUpload-dropzone {
width: 120px;
height: 120px;
border-radius: 50%;
padding: var(--space-4);
}
.FileUpload--avatar .FileUpload-preview {
border-radius: 50%;
overflow: hidden;
}
API Reference
| Class | Description |
|---|---|
.FileUpload | Base container |
.FileUpload--compact | Smaller, inline variant |
.FileUpload--bordered | Solid border style |
.FileUpload--preview | Image preview layout |
.FileUpload--disabled | Disabled state |
.FileUpload--error | Error state |
.FileUpload-dropzone | Drop target area |
.FileUpload-dropzone.is-dragover | Active drag state |
.FileUpload-input | Hidden file input |
.FileUpload-icon | Upload icon |
.FileUpload-text | Main instruction text |
.FileUpload-link | Clickable upload trigger |
.FileUpload-hint | Help text (file types, size) |
.FileUpload-preview | Image thumbnail container |
.FileUpload-thumbnail | Preview image |
.FileUpload-list | File list container |
.FileUpload-item | Individual file row |
.FileUpload-item--uploading | Upload in progress |
.FileUpload-item--complete | Upload complete |
.FileUpload-item--error | Upload failed |
.FileUpload-itemIcon | File type icon |
.FileUpload-itemInfo | File name and size container |
.FileUpload-itemName | File name |
.FileUpload-itemSize | File size / status |
.FileUpload-progress | Progress bar container |
.FileUpload-progressBar | Progress bar fill |
.FileUpload-itemRemove | Remove/cancel button |
.FileUpload-itemRetry | Retry button |
CSS Reference
/* Base container */
.FileUpload {
width: 100%;
}
/* Dropzone */
.FileUpload-dropzone {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-2);
padding: var(--space-8);
border: 2px dashed var(--bd);
border-radius: var(--r-m);
background: var(--bg);
cursor: pointer;
transition: border-color 0.15s, background 0.15s;
position: relative;
text-align: center;
}
.FileUpload-dropzone:hover {
border-color: var(--bd-s);
background: var(--bg-s);
}
.FileUpload-dropzone.is-dragover {
border-color: var(--accent);
background: oklch(60% 0.15 250 / 0.05);
}
/* Hidden input */
.FileUpload-input {
position: absolute;
inset: 0;
opacity: 0;
cursor: pointer;
}
/* Icon */
.FileUpload-icon {
color: var(--fg-3);
}
/* Text */
.FileUpload-text {
font-size: 0.9rem;
color: var(--fg-2);
margin: 0;
}
.FileUpload-link {
color: var(--accent);
font-weight: 500;
cursor: pointer;
}
.FileUpload-hint {
font-size: 0.8rem;
color: var(--fg-3);
margin: 0;
}
/* Compact variant */
.FileUpload--compact .FileUpload-dropzone {
flex-direction: row;
padding: var(--space-3) var(--space-4);
gap: var(--space-3);
}
/* Bordered variant */
.FileUpload--bordered .FileUpload-dropzone {
border-style: solid;
}
/* File list */
.FileUpload-list {
list-style: none;
padding: 0;
margin: var(--space-3) 0 0;
}
.FileUpload-item {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3);
border: 1px solid var(--bd);
border-radius: var(--r-s);
margin-bottom: var(--space-2);
}
.FileUpload-itemIcon {
flex-shrink: 0;
color: var(--fg-3);
}
.FileUpload-itemIcon--success { color: oklch(55% 0.15 150); }
.FileUpload-itemIcon--error { color: oklch(55% 0.2 25); }
.FileUpload-itemInfo {
flex: 1;
min-width: 0;
}
.FileUpload-itemName {
display: block;
font-size: 0.9rem;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.FileUpload-itemSize {
display: block;
font-size: 0.8rem;
color: var(--fg-3);
}
.FileUpload-itemSize--error {
color: oklch(55% 0.2 25);
}
/* Progress bar */
.FileUpload-progress {
height: 4px;
background: var(--bg-s);
border-radius: 2px;
margin: var(--space-1) 0;
overflow: hidden;
}
.FileUpload-progressBar {
height: 100%;
background: var(--accent);
border-radius: 2px;
transition: width 0.3s;
}
/* Remove / Retry buttons */
.FileUpload-itemRemove,
.FileUpload-itemRetry {
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
padding: var(--space-1);
cursor: pointer;
color: var(--fg-3);
border-radius: var(--r-s);
transition: background 0.15s;
}
.FileUpload-itemRemove:hover,
.FileUpload-itemRetry:hover {
background: var(--bg-s);
color: var(--fg);
}
/* States */
.FileUpload--disabled .FileUpload-dropzone {
opacity: 0.5;
pointer-events: none;
}
.FileUpload--error .FileUpload-dropzone {
border-color: oklch(55% 0.2 25);
background: oklch(55% 0.2 25 / 0.05);
}
/* Preview variant */
.FileUpload--preview .FileUpload-dropzone {
flex-direction: row;
text-align: left;
}
.FileUpload-thumbnail {
width: 80px;
height: 80px;
object-fit: cover;
border-radius: var(--r-s);
}
Accessibility
- Use
aria-labelon remove/retry buttons - Add
aria-describedbylinking to hint text - Announce upload progress to screen readers via
aria-live - Ensure keyboard access to drop zone via the file input
- Provide clear error messages for validation failures
Keyboard Support
| Key | Action |
|---|---|
| Enter / Space | Open file browser (when focused) |
| Delete | Remove focused file from list |
| Tab | Navigate between files and actions |
Screen Reader Guidance
<div class="FileUpload">
<div class="FileUpload-dropzone">
<p class="FileUpload-text" id="upload-desc">
<span class="FileUpload-link">Click to upload</span> or drag and drop
</p>
<p class="FileUpload-hint" id="upload-hint">PNG, JPG up to 10MB</p>
<input type="file" class="FileUpload-input"
aria-describedby="upload-desc upload-hint"
aria-label="Upload file" />
</div>
<div aria-live="polite" class="sr-only">
<!-- Announce upload progress and completion -->
</div>
</div>
Best Practices
Do
- ✓ Show accepted file types and size limits — Users need to know constraints before uploading
- ✓ Display upload progress for large files — Visual feedback prevents uncertainty
- ✓ Allow retry for failed uploads — Network errors happen; let users recover gracefully
- ✓ Show image previews when appropriate — Helps users confirm they selected the right file
- ✓ Provide clear error messages — “File exceeds 10MB limit” not just “Error”
- ✓ Support drag-and-drop and click — Both interaction patterns are expected
Don’t
- ✗ Allow uploads without size limits — Unbounded uploads risk server issues and poor UX
- ✗ Remove files without confirmation — Accidental clicks shouldn’t lose uploaded work
- ✗ Hide the file list when processing — Users need to see what’s happening
- ✗ Use vague error messages — “Upload failed” tells users nothing actionable
- ✗ Auto-upload on selection without user control — Let users review before submitting
- ✗ Forget mobile touch targets — Drop zones and buttons must be at least 44px