Nasaq
Components

FileUpload

Dropzone with type, size and count validation, a file list with per-file progress, remove and retry, and a single-image upload with a thumbnail. You run the upload; it holds the state.

PreviewOpen ↗

Code

import { FileUpload, type FileUploadControls, type UploadFile } from "@fadymondy/nasaq/web";function send(item: UploadFile, controls: FileUploadControls) {  const body = new FormData();  body.append("file", item.file);  const xhr = new XMLHttpRequest();  xhr.open("POST", "/api/files");  xhr.upload.onprogress = (e) =>    controls.update(item.id, { status: "uploading", progress: Math.round((e.loaded / e.total) * 100) });  xhr.onload = () =>    controls.update(item.id, xhr.status < 300 ? { status: "done", progress: 100 } : { status: "error", error: "Upload failed" });  xhr.onerror = () => controls.update(item.id, { status: "error", error: "Network error" });  xhr.send(body);}export function Attachments() {  return (    <FileUpload      accept="image/*,.pdf"      maxSize={5 * 1024 * 1024}      maxFiles={4}      onFiles={(added, controls) => added.forEach((f) => send(f, controls))}      onRetry={send}    />  );}

Files · beta

Live examples and controls: FileUpload in the lab.

Install

npx shadcn@latest add https://docs.nasaqui.com/r/file-upload.json

Choose files by clicking, pressing a key or dragging them in, see each one with its size and progress, and remove or retry it. Nasaq does not send anything: your onFiles callback starts the request and reports progress by patching the file's state, so it works with any transport (fetch, XHR, a presigned URL, tus).

When to use

  • Attachments, documents, imports: FileUpload.
  • A profile or product picture: ImageUpload.
  • Your own list or layout: Dropzone alone.

When not to use

  • Picking one value from a list: use Select.
  • Showing progress of something that is not a file: use Progress.

Import

import { FileUpload, ImageUpload, Dropzone } from "@fadymondy/nasaq/web";

Quick start

import { FileUpload, type FileUploadControls, type UploadFile } from "@fadymondy/nasaq/web";

function send(item: UploadFile, controls: FileUploadControls) {
  const body = new FormData();
  body.append("file", item.file);
  const xhr = new XMLHttpRequest();
  xhr.open("POST", "/api/files");
  xhr.upload.onprogress = (e) =>
    controls.update(item.id, { status: "uploading", progress: Math.round((e.loaded / e.total) * 100) });
  xhr.onload = () =>
    controls.update(item.id, xhr.status < 300 ? { status: "done", progress: 100 } : { status: "error", error: "Upload failed" });
  xhr.onerror = () => controls.update(item.id, { status: "error", error: "Network error" });
  xhr.send(body);
}

export function Attachments() {
  return (
    <FileUpload
      accept="image/*,.pdf"
      maxSize={5 * 1024 * 1024}
      maxFiles={4}
      onFiles={(added, controls) => added.forEach((f) => send(f, controls))}
      onRetry={send}
    />
  );
}

Anatomy

FileUpload                     data-slot="file-upload"
├─ Dropzone                    data-slot="dropzone"  (data-dragging, data-invalid, data-disabled)
├─ ul (rejections, role=alert) data-slot="file-upload-errors"
└─ UploadList                    data-slot="file-list"
   └─ UploadListItem             data-slot="file-list-item"  (data-status)
      └─ Progress              while pending or uploading

ImageUpload                    data-slot="image-upload"
├─ preview (img + remove)      data-slot="image-upload-preview"
└─ Dropzone

API

FileUpload

PropTypeDefaultDescription
valuereadonly UploadFile[]noneControlled list. Omit for uncontrolled.
defaultValuereadonly UploadFile[][]Initial list when uncontrolled.
onValueChange(files: UploadFile[]) => voidnoneFires on every list change, including progress patches.
onFiles(added: UploadFile[], controls: FileUploadControls) => voidnoneNew, validated files (status "pending"). Start the upload here.
onRetry(item: UploadFile, controls: FileUploadControls) => voidnoneThe retry button of a failed file was pressed.
onRemove(item: UploadFile) => voidnoneAfter removal. Cancel the request here.
acceptstringnoneNative syntax: "image/*,.pdf".
maxSizenumbernoneLargest file in bytes.
maxFilesnumbernoneLargest total number of files, counting those already in the list.
multiplebooleantrueAllow several files per pick.
disabledbooleanfalse
childrenReactNodelocalised promptReplaces the prompt text. Localise it yourself.

FileUploadControls

MemberTypeDescription
update(id: string, patch: Partial<Omit<UploadFile, "id" | "file">>) => voidPatch status, progress or error of one file.
remove(id: string) => voidDrop a file from the list.

UploadFile

FieldTypeDescription
idstringStable key.
fileFile
status"pending" | "uploading" | "done" | "error"
progressnumber | null0 to 100. null shows an indeterminate bar.
errorstringShown under the row when status is "error". Localise it.

Dropzone

PropTypeDefaultDescription
onFiles(files: File[]) => voidrequiredFiles that passed validation.
onReject(rejections: FileRejection[]) => voidnoneCalled after every pick or drop; empty when all passed.
accept, maxSize, maxFilesnoneAs above.
countnumber0Files already added, so maxFiles counts them.
multiplebooleantrue
invalidbooleanfalseDanger border.
inputPropsinput propsnoneFor the hidden <input type="file">, for example name.
childrenReactNodelocalised prompt

FileRejection is { file: File; code: "too-large" | "wrong-type" | "too-many"; message: string } with a localised message.

UploadList / UploadListItem

PropTypeDescription
items (UploadList)readonly UploadFile[]Rows to render. Renders nothing when empty.
item (UploadListItem)UploadFile
onRemove, onRetry(item: UploadFile) => voidButtons show only when set. Retry shows only on errors.

ImageUpload

PropTypeDefaultDescription
valueFile | nullnoneControlled image.
defaultValueFile | nullnull
onValueChange(file: File | null) => voidnone
srcstringnoneSaved image URL shown until a file is chosen.
onRemove() => voidnoneThe remove button was pressed (also for a saved src).
acceptstring"image/*"
maxSizenumbernoneBytes.
progressnumber | nullnoneShows a bar over the thumbnail while below 100.
altstring""Alt text of the preview. Localise it.
disabledbooleanfalse

Helpers

ExportSignatureDescription
useObjectUrl(file: File | Blob | null | undefined) => string | nullObject URL that is revoked when the file changes or the component unmounts.
formatFileSize(bytes: number, locale?: string) => string"1.5 MB", or Arabic units in Arabic.
matchesAccept(file: { name, type }, accept?: string) => booleanThe accept matcher.

Examples

Controlled, with a saved avatar:

import { ImageUpload } from "@fadymondy/nasaq/web";
import { useState } from "react";

export function Avatar() {
  const [file, setFile] = useState<File | null>(null);
  return (
    <ImageUpload value={file} onValueChange={setFile} src="/avatars/current.png" alt="Profile picture" maxSize={2 * 1024 * 1024} />
  );
}

Arabic copy for your own errors:

controls.update(item.id, { status: "error", error: "انقطع الاتصال أثناء الرفع." });

Accessibility

KeyAction
TabMoves to the dropzone, then to each row's buttons.
Enter, SpaceOn the dropzone: opens the file dialog.
  • The dropzone is role="button" and described by the accepted types and size limit.
  • Rejections render in a role="alert" list, so they are announced.
  • Every row button is named with the file name ("Remove report.pdf"). Each Progress is named "report.pdf · Uploading".
  • You localise: alt on ImageUpload, children prompts and the error text you set.

RTL & i18n

  • Built-in strings (prompt, statuses, limits, validation messages, size units) follow the Nasaq locale in English and Arabic.
  • Sizes use Nasaq number formatting (Western digits by default). File names are wrapped in <bdi>, so an English name inside Arabic text keeps its order.
  • The remove button on an image sits at the inline end and mirrors; progress fills from the inline start.

Styling & tokens

Uses border-input, bg-card, bg-nq-hover, bg-nq-selected, border-nq-focus and text-nq-danger-text. Target [data-dragging], [data-invalid], [data-disabled] on the dropzone and [data-status] on rows. Extend with className; never override colours with raw hex.

Do / Don't

  • Do validate again on the server: accept and maxSize are conveniences, not security.
  • Do give every failed file an error message that says what to do.
  • Do not start uploads during render; start requests from onFiles.
  • Do not revoke object URLs by hand; useObjectUrl does it.

Lab

https://docs.nasaqui.com/?path=/docs/components-files-file-upload--docs

On this page