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.

Upload file
<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.

Preview

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.mp4
    65% · 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

JD

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

Attach files
<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.zip
    40% · 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

ClassDescription
.FileUploadBase container
.FileUpload--compactSmaller, inline variant
.FileUpload--borderedSolid border style
.FileUpload--previewImage preview layout
.FileUpload--disabledDisabled state
.FileUpload--errorError state
.FileUpload-dropzoneDrop target area
.FileUpload-dropzone.is-dragoverActive drag state
.FileUpload-inputHidden file input
.FileUpload-iconUpload icon
.FileUpload-textMain instruction text
.FileUpload-linkClickable upload trigger
.FileUpload-hintHelp text (file types, size)
.FileUpload-previewImage thumbnail container
.FileUpload-thumbnailPreview image
.FileUpload-listFile list container
.FileUpload-itemIndividual file row
.FileUpload-item--uploadingUpload in progress
.FileUpload-item--completeUpload complete
.FileUpload-item--errorUpload failed
.FileUpload-itemIconFile type icon
.FileUpload-itemInfoFile name and size container
.FileUpload-itemNameFile name
.FileUpload-itemSizeFile size / status
.FileUpload-progressProgress bar container
.FileUpload-progressBarProgress bar fill
.FileUpload-itemRemoveRemove/cancel button
.FileUpload-itemRetryRetry 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-label on remove/retry buttons
  • Add aria-describedby linking 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