FileUpload
Upload files by drag and drop, file picker or Ctrl+V — with progress, cancel and retry for each file.
Jami: 0 · yuklandi: 0
'use client';
import { Field, FileUpload, type FileUploadContext, type FileUploadItem } from '@akhatmirzo/ui';
import { useState } from 'react';
const MB = 1024 * 1024;
/** Soxta yuklash: progress bilan ~2 soniya. Nomida "xato" bo'lsa — tarmoq xatosi */
function fakeUpload(file: File, { onProgress, signal }: FileUploadContext) {
return new Promise<{ url: string }>((resolve, reject) => {
let progress = 0;
const timer = setInterval(() => {
progress = Math.min(100, progress + 8);
onProgress(progress);
if (file.name.includes('xato') && progress > 50) {
clearInterval(timer);
reject(new Error('network'));
} else if (progress === 100) {
clearInterval(timer);
resolve({ url: `/uploads/${encodeURIComponent(file.name)}` });
}
}, 150);
signal.addEventListener('abort', () => clearInterval(timer));
});
}
export default function FileUploadBasic() {
const [files, setFiles] = useState<FileUploadItem[]>([]);
const done = files.filter((f) => f.status === 'done').length;
return (
<div className="flex w-full max-w-lg flex-col gap-2">
<Field label="Hujjatlar">
<FileUpload
accept={['image/*', '.pdf']}
maxSize={5 * MB}
maxFiles={5}
multiple
upload={fakeUpload}
onFilesChange={setFiles}
/>
</Field>
<p className="text-sm text-text-muted">
Jami: {files.length} · yuklandi: {done}
</p>
</div>
);
}
Import
import { FileUpload } from '@akhatmirzo/ui';When to use
- Documents, photos, screenshots — in applications, profiles and support forms. Drag and
drop, the file dialog and
Ctrl+Vall work in one component. - State the limits up front: with
accept,maxSizeandmaxFiles, a hint like "Rasmlar, PDF · 5 MB gacha · ko'pi bilan 5 ta" appears automatically, and files that break the rules are flagged right away. - Need instant uploads? Pass an
uploadfunction — progress, cancel and "Retry" come for free. - Sending files with a form? Skip
uploadand passname: the files end up inFormDatajust like with a plain<input type="file">. - Short on space? Use
variant="button"— a single button instead of the dropzone.
The upload function
upload(file, { onProgress, signal }) is called once per valid file and returns a Promise. Report
progress with onProgress(0–100); signal is aborted when the user clicks ✕ or the component
unmounts. Whatever the Promise resolves to is stored in item.result.
import type { FileUploadContext } from '@akhatmirzo/ui';
/** XMLHttpRequest — unlike fetch, it reports upload progress */
export function uploadFile(file: File, { onProgress, signal }: FileUploadContext) {
return new Promise<{ url: string }>((resolve, reject) => {
const xhr = new XMLHttpRequest();
const body = new FormData();
body.append('file', file);
xhr.upload.onprogress = (e) => e.lengthComputable && onProgress((e.loaded / e.total) * 100);
xhr.onload = () =>
xhr.status < 300 ? resolve(JSON.parse(xhr.responseText)) : reject(new Error(xhr.statusText));
xhr.onerror = () => reject(new Error('network'));
signal.addEventListener('abort', () => xhr.abort());
xhr.open('POST', '/api/upload');
xhr.send(body);
});
}import { FileUpload } from '@akhatmirzo/ui';
import { useState } from 'react';
import { uploadFile } from './upload';
export function Documents() {
const [urls, setUrls] = useState<string[]>([]);
return (
<>
<FileUpload
accept={['image/*', '.pdf']}
maxSize={5 * 1024 * 1024}
maxFiles={5}
multiple
upload={uploadFile}
onFilesChange={(items) =>
setUrls(items.flatMap((it) => (it.result ? [it.result.url] : [])))
}
/>
<input type="hidden" name="documents" value={urls.join(',')} />
</>
);
}If the Promise rejects, the file row shows an error and a Retry button. Choose the message with
getErrorMessage(error) (the default is "Tarmoq xatosi", a network error). In the demo above, any
file with xato in its name fails on purpose.
The component owns the list
FileUpload manages its own file list — there's no value prop. Use onFilesChange to read it:
it receives the full list on every change (added, progress, done, error, removed).
States
Without multiple, a new file replaces the old one. While dragging, an accepted type gets a
primary border; a rejected one gets danger and "Bu turdagi fayl qabul qilinmaydi". error,
disabled and required from Field are applied automatically. hint replaces the generated
limits text.
Bitta fayl — yangisi eskisini almashtiradi
Kamida bitta hujjat yuklang
'use client';
import { Field, FileUpload } from '@akhatmirzo/ui';
export default function FileUploadStates() {
return (
<div className="grid w-full gap-4 md:grid-cols-2">
<Field label="Faqat rasm" description="Bitta fayl — yangisi eskisini almashtiradi">
<FileUpload accept={['image/png', 'image/jpeg']} maxSize={1024 * 1024} />
</Field>
<Field label="Shartnoma" error="Kamida bitta hujjat yuklang" required>
<FileUpload accept={['.pdf']} />
</Field>
<Field label="O'chirilgan" disabled>
<FileUpload />
</Field>
<Field label="O'z cheklov matni bilan">
<FileUpload hint="Excel yoki CSV · 10 MB gacha" accept={['.xlsx', '.csv']} />
</Field>
</div>
);
}
Files that break a rule still show up in the list with the reason ("Hajmi 5 MB dan katta"), but
they aren't uploaded and don't count toward maxFiles. Files over the maxFiles limit aren't
added at all; a "Ko'pi bilan 5 ta fayl yuklash mumkin" notice appears instead.
Button variant
variant="button" swaps the dropzone for a "Fayl tanlash" button. The file list works the same.
Old va orqa tomoni
'use client';
import { Field, FileUpload } from '@akhatmirzo/ui';
export default function FileUploadButton() {
return (
<div className="w-full max-w-sm">
<Field label="Pasport nusxasi" description="Old va orqa tomoni">
<FileUpload
variant="button"
accept={['image/*', '.pdf']}
maxSize={5 * 1024 * 1024}
maxFiles={2}
multiple
/>
</Field>
</div>
);
}
With a form
Without upload, files are only selected (status: 'idle'). Pass name and the component keeps a
hidden <input type="file" name="..."> in sync — every file without an error is written to it
and submitted with the form.
'use client';
import { formatFileSize } from '@akhatmirzo/core';
import { Button, Field, FileUpload } from '@akhatmirzo/ui';
import { useState, type FormEvent } from 'react';
export default function FileUploadForm() {
const [sent, setSent] = useState<string | null>(null);
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const files = (new FormData(e.currentTarget).getAll('rezyume') as File[]).filter(
(f) => f.size > 0,
);
setSent(files.map((f) => `${f.name} (${formatFileSize(f.size)})`).join(', ') || "Fayl yo'q");
};
return (
<form onSubmit={onSubmit} className="flex w-full max-w-sm flex-col gap-3">
<Field label="Rezyume" description="PDF yoki DOCX, 2 MB gacha">
<FileUpload
variant="button"
name="rezyume"
accept={['.pdf', '.docx']}
maxSize={2 * 1024 * 1024}
/>
</Field>
<div className="flex items-center gap-3">
<Button type="submit" variant="primary" size="sm">
Yuborish
</Button>
{sent && <span className="text-sm text-text-muted">{sent}</span>}
</div>
</form>
);
}
Paste from the clipboard
By default (paste={true}), Ctrl+V adds a copied file or screenshot while focus is inside the
component. paste="global" listens on the whole page (except inputs and textareas);
paste={false} turns it off.
Rasmni nusxalang va sahifaning istalgan joyida Ctrl+V bosing
'use client';
import { Field, FileUpload } from '@akhatmirzo/ui';
export default function FileUploadPaste() {
return (
<div className="w-full max-w-lg">
<Field
label="Skrinshot"
description="Rasmni nusxalang va sahifaning istalgan joyida Ctrl+V bosing"
>
<FileUpload accept={['image/*']} multiple maxFiles={3} paste="global" />
</Field>
</div>
);
}
Props
Accepts the remaining <div> attributes (className, id, aria-* ...).
| Prop | Type | Default | Description |
|---|---|---|---|
accept | string[] | — | Allowed types: ['image/*', '.pdf', 'application/zip'] |
maxSize | number | — | Maximum size in bytes |
maxFiles | number | — | Maximum number of files (1 without multiple) |
multiple | boolean | false | Allow several files |
upload | (file: File, ctx: FileUploadContext) => Promise<R> | — | Upload function. Without it, files are only selected |
onFilesChange | (files: FileUploadItem<R>[]) => void | — | Called with the full list on every change |
variant | 'dropzone' | 'button' | 'dropzone' | Large drop area or a single button |
disabled | boolean | — | |
invalid | boolean | — | Automatic inside a Field with an error |
required | boolean | — | |
name | string | — | Name of the <input type="file"> for forms |
hint | ReactNode | — | Limits text; built from accept, maxSize and maxFiles if omitted |
paste | boolean | 'global' | true | Ctrl+V: true — while focused, 'global' — anywhere on the page |
getErrorMessage | (error: unknown) => string | undefined | — | Turns an upload error into a message |
labels | FileUploadLabels | — | All UI strings |
FileUploadItem
Each item passed to onFilesChange:
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | — | |
file | File | — | |
status | 'idle' | 'uploading' | 'done' | 'error' | — | idle — no upload function, the file is just selected |
progress | number | — | 0–100 |
error | string | — | Error message |
errorKind | 'type' | 'size' | 'upload' | — | Only upload errors get a Retry button |
result | R | — | What upload resolved with |
previewUrl | string | — | A blob: URL for images |
Strings (labels)
prompt, browse, dragActive, dragReject, button, images, videos, audio, maxSize,
maxFiles, tooManyFiles, invalidType, tooLarge, uploadError, uploaded, retry,
cancel, remove, dismiss, list. The ones that include a size or a count are functions:
<FileUpload
labels={{
prompt: 'Drop a file here or',
browse: 'browse',
maxSize: (size) => `up to ${size}`,
tooLarge: (size) => `Larger than ${size}`,
}}
/>matchesAccept(file, accept) is exported too, so you can apply the same rule elsewhere.
Data attributes
| Attribute | Element | Value |
|---|---|---|
data-variant | root | dropzone · button |
data-disabled | root | present when disabled |
data-invalid | root | present when invalid |
data-drag | dropzone | idle · over · reject |
data-status | file row (li) | idle · uploading · done · error |
Accessibility
- The dropzone has
role="button", is reachable withTab, andEnterorSpaceopens the file dialog. Its name combines theFieldlabel and its own text; the limits text is read viaaria-describedby. - Drag feedback ("Faylni qo'yib yuboring", "Bu turdagi fayl qabul qilinmaydi") is announced
through
aria-live="polite", and so is themaxFilesnotice. - Progress uses
role="progressbar"witharia-valuenowand the file name. - Row buttons include the file name: "Bekor qilish: report.pdf" (cancel, while uploading), "O'chirish: …" (remove), and "Olib tashlash: …" (dismiss) for errors.
- The list is a
<ul aria-label="Tanlangan fayllar">; long names are shortened in the middle, with the full name intitle. - Errors are exposed as
aria-invalidon the dropzone or button.