File upload
File upload is a neobrutalist React component from Cruise UI. A drag-and-drop zone that's also a button, with a file list, thumbnails, progress and remove. Install it with npx shadcn@latest add @cruiseui/file-upload.
Install
Terminal
npx shadcn@latest add @cruiseui/file-uploadFirst time? Installation shows the one-off components.json setup (the @cruiseui registry) and how to turn the theme on.
Example
Corners
Up to 3 photos.
"use client";
import { CruiseFileUpload } from "@/components/cruise/file-upload";
/** A pretend upload: reports progress, then finishes. Use your own (S3, Cloudinary…). */
async function fakeUpload(file: File, report: (percent: number) => void) {
for (let p = 0; p <= 100; p += 20) {
report(p);
await new Promise((r) => setTimeout(r, 250));
}
}
export default function FileUploadExample() {
return (
<div className="grid gap-6 md:grid-cols-2">
<CruiseFileUpload
label="Product photos"
hint="Up to 3 photos."
accept="image/*"
maxSize={5 * 1024 * 1024}
multiple
maxFiles={3}
uploader={fakeUpload}
/>
<CruiseFileUpload label="Receipt" accept="image/*,.pdf" maxSize={2 * 1024 * 1024} />
</div>
);
}
<div class="field">
<label class="field-label" for="photos">Product photos</label>
<label class="upload-zone">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M12 13v8M8 17l4-4 4 4"/><path d="M20.4 14.5A5 5 0 0 0 17 6h-1.3A8 8 0 1 0 4 15.3"/></svg>
<span class="upload-title">Drop files here or browse</span>
<span class="upload-hint">Images · max 5MB · up to 3 files</span>
<input class="visually-hidden" type="file" id="photos" accept="image/*" multiple>
</label>
<ul class="upload-list" aria-label="Chosen files">
<li class="upload-item">
<span class="upload-thumb" aria-hidden="true"><img src="blob:…" alt=""></span>
<span class="upload-meta"><span class="upload-name">cake.jpg</span><span class="upload-size">1.2MB</span></span>
<button type="button" class="upload-remove" aria-label="Remove cake.jpg"><svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="3" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
</li>
</ul>
</div>Needs theme.css. Interactive parts (menus, tabs, drawers) also need the script in the spec for that component.
When to use it
When to use
Photos, receipts and documents in a form.
When not to
Pasting a link to a file (use an Input).
Props
CruiseFileUpload
| Prop | Type | Default | Description |
|---|---|---|---|
label? | string | — | Visible label. Leave out inside a CruiseFormField (it brings the label). |
hint? | string | — | — |
error? | string | — | Your own error; size, type and count errors are added for you. |
required? | boolean | — | — |
accept? | string | — | e.g. "image/*,.pdf" |
maxSize? | number | — | In bytes, e.g. 5 * 1024 * 1024. |
multiple? | boolean | false | — |
maxFiles? | number | — | — |
disabled? | boolean | — | — |
name? | string | — | — |
id? | string | — | — |
onFilesChange? | ((files: File[]) => void) | — | Called with the accepted files whenever the list changes. |
uploader? | ((file: File, report: (percent: number) => void) => Promise<void>) | — | Uploads one file; call report(0-100) as it goes. Throw to fail it. |
className? | string | — | — |
Accessibility
- The zone is a <label> for a real file input: click, Enter or Space opens the picker.
- Size, type and count errors show in words through the Form field.
- Each file has a "Remove photo.jpg" button and a labelled progress bar.