@xsolla/xui-uploader
v0.191.0
Published
A cross-platform React file uploader component that provides a button to select files, displays selected files, and allows removing them. <!-- BEGIN:xui-mcp-instructions:uploader --> A compact button-style control that triggers a file selection dialog and
Readme
Uploader
A cross-platform React file uploader component that provides a button to select files, displays selected files, and allows removing them.
A compact button-style control that triggers a file selection dialog and optionally displays a list of uploaded or selected files below the trigger. Supports five sizes, six states including an upload-in-progress loading state, and a toggleable file list. Used wherever a user must attach, replace, or upload a file — profile photos, document uploads, asset imports.
When to use
- When the user must select and upload a file from their device — profile picture, document, CSV import, media asset
- When the file selection happens inline in a form alongside other fields
- When the number of accepted files is small (one or a few) and a full drag-and-drop zone would occupy too much space
- When the upload state and error feedback must be communicated within the field itself, without a separate Toast or modal
When not to use
When batch uploading many files at once with drag-and-drop is the primary interaction — use a dedicated Drag&dropUploader component
When the upload triggers a complex multi-step flow — open a dedicated upload modal instead
When only a URL or link is needed, not an actual file — use a standard Input
Content guidelines
Trigger label — use a short imperative phrase: "Upload file", "Choose file", "Add document", "Upload photo". For replace scenarios, switch to "Replace file" or "Change photo".
Loading label — show "Uploading…" or "Processing…" while in State=Loading. If the upload has measurable progress, include the percentage: "Uploading… 64%". Error messages — be specific and actionable:
- "File type not supported. Please upload a PDF or DOCX file."
- "File is too large. Maximum allowed size is 10 MB."
- "Upload failed. Check your connection and try again."
- "Please upload a file to continue." (required field, empty on submit)
- File type guidance — add helper text below the component (outside FileUploader) specifying accepted formats and size limits: "Accepted formats: PDF, DOCX. Max size: 10 MB." Do not put this information inside the trigger label.
- Field label — always provide a visible label above the component: "Profile photo", "Company logo", "Import file". Do not rely on the trigger label alone.
Behaviour guidelines (from industry practice)
Click to select — clicking the trigger opens the native OS file selection dialog. After the user selects a file, the component transitions based on the product's flow: either immediately begins uploading (State=Loading) or adds the file to the list (if deferred upload is used).
Loading state — switch to State=Loading immediately when the upload begins. The trigger is non-interactive during upload. Show a progress indicator if the file is large and upload duration is meaningful (e.g. a progress bar inside or below the trigger, outside the component itself).
Upload complete — after a successful upload, return the trigger to State=Default (or a custom "Replace" state). If Uploader list=true, add the uploaded file to the list. Error handling — switch to State=Error when:
- The file type is not accepted (e.g. a PNG was uploaded but only PDF is allowed)
- The file size exceeds the maximum allowed
- The upload request fails on the server
- Required file was not provided on form submission Show a specific error message below the trigger.
- Remove file — clicking the ✕ button in an uploader list item removes that file from the selection. If the product uses immediate upload, also cancel or delete the server-side upload. After removal, if the list is empty, keep Uploader list=true visible (empty state) or revert to Uploader list=false depending on the product logic.
- Replace file — for single-file contexts, clicking the trigger again while a file is already selected or uploaded should replace the previous selection. Provide a clear label change (e.g. "Replace file") to signal this behaviour.
- File validation — validate the file type and size client-side before uploading and before switching to State=Loading. Do not start an upload for an invalid file — switch directly to State=Error with a specific message.
- Disabled state — State=Disable prevents opening the file dialog. Show a tooltip or nearby explanation for why the upload is unavailable.
- Multiple files — if the product allows multiple file selection, set multiple on the underlying <input type="file"> and enable Uploader list=true to show all selected files. Define a clear max file count and enforce it client-side.
Accessibility
The trigger must be implemented as a or element associated with a visually hidden <input type="file">. The must not be the visible trigger.
The trigger must have aria-label if the visible label text is not descriptive enough — e.g. aria-label="Upload profile photo".
When State=Loading, set aria-busy="true" on the trigger and update aria-label to reflect the in-progress state: aria-label="Uploading profile photo…".
When State=Error, the error message must be associated via aria-describedby so screen readers announce it when the trigger receives focus.
When State=Disable, the trigger must have aria-disabled="true". Keep it focusable so screen readers can discover and announce it.
The file list (when Uploader list=true) must be in a region with aria-label="Selected files" or equivalent. Each file item must be announced with its name and size. The remove button for each item must have aria-label="Remove [file name]" — not just aria-label="Remove".
When a file is removed from the list, announce the removal to screen readers using aria-live="polite": e.g. "document.pdf removed".
When a file is successfully uploaded or added to the list, use aria-live="polite" to announce: "document.pdf added".
The accepted file types and size limits (shown in helper text) should be linked to the trigger via aria-describedby so screen readers surface the constraints when the trigger is focused.
Installation
npm install @xsolla/xui-uploaderDemo
Basic Uploader
import * as React from "react";
import { Uploader } from "@xsolla/xui-uploader";
export default function BasicUploader() {
return (
<Uploader
label="Upload file"
onFilesChange={(files) => console.log("Selected:", files)}
/>
);
}Multiple Files
import * as React from "react";
import { Uploader } from "@xsolla/xui-uploader";
export default function MultipleFiles() {
return (
<Uploader
label="Upload documents"
placeholder="Select multiple files"
multiple
onFilesChange={(files) => console.log("Files:", files)}
/>
);
}Accept Specific Types
import * as React from "react";
import { Uploader } from "@xsolla/xui-uploader";
export default function ImageUploader() {
return (
<Uploader
label="Upload image"
placeholder="Select image file"
accept=".jpg,.jpeg,.png,.gif"
onFilesChange={(files) => console.log("Images:", files)}
/>
);
}Anatomy
import { Uploader } from "@xsolla/xui-uploader";
<Uploader
label="Label" // Label above button
placeholder="Select files" // Button text
accept=".pdf,.doc" // Accepted file types
multiple={false} // Allow multiple selection
disabled={false} // Disabled state
size="md" // Button size
onFilesChange={handleFiles} // File change callback
/>;Examples
Document Upload
import * as React from "react";
import { Uploader } from "@xsolla/xui-uploader";
export default function DocumentUpload() {
const [files, setFiles] = React.useState<File[]>([]);
return (
<div>
<Uploader
label="Legal documents"
placeholder="Upload PDF documents"
accept=".pdf"
multiple
onFilesChange={setFiles}
/>
{files.length > 0 && (
<p style={{ marginTop: 8 }}>{files.length} file(s) selected</p>
)}
</div>
);
}Profile Image Upload
import * as React from "react";
import { Uploader } from "@xsolla/xui-uploader";
export default function ProfileImageUpload() {
const handleImageUpload = (files: File[]) => {
if (files.length > 0) {
const file = files[0];
// Create preview URL
const previewUrl = URL.createObjectURL(file);
console.log("Preview:", previewUrl);
}
};
return (
<div style={{ maxWidth: 300 }}>
<Uploader
label="Profile picture"
placeholder="Choose image"
accept="image/*"
onFilesChange={handleImageUpload}
size="sm"
/>
<p style={{ fontSize: 12, color: "#666", marginTop: 4 }}>
Recommended: Square image, at least 200x200px
</p>
</div>
);
}Form with File Upload
import * as React from "react";
import { Uploader } from "@xsolla/xui-uploader";
import { Input } from "@xsolla/xui-input";
import { Textarea } from "@xsolla/xui-textarea";
import { Button } from "@xsolla/xui-button";
export default function FormWithUpload() {
const [files, setFiles] = React.useState<File[]>([]);
const handleSubmit = () => {
const formData = new FormData();
files.forEach((file) => formData.append("attachments", file));
console.log("Submitting with", files.length, "files");
};
return (
<form
style={{
display: "flex",
flexDirection: "column",
gap: 16,
maxWidth: 400,
}}
onSubmit={(e) => {
e.preventDefault();
handleSubmit();
}}
>
<Input label="Subject" placeholder="Enter subject" />
<Textarea label="Message" placeholder="Enter your message" />
<Uploader
label="Attachments"
placeholder="Add attachments"
multiple
onFilesChange={setFiles}
/>
<Button type="submit">Submit</Button>
</form>
);
}Different Sizes
import * as React from "react";
import { Uploader } from "@xsolla/xui-uploader";
export default function UploaderSizes() {
return (
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
<Uploader size="xs" placeholder="Extra small" />
<Uploader size="sm" placeholder="Small" />
<Uploader size="md" placeholder="Medium" />
<Uploader size="lg" placeholder="Large" />
<Uploader size="xl" placeholder="Extra large" />
</div>
);
}Disabled State
import * as React from "react";
import { Uploader } from "@xsolla/xui-uploader";
export default function DisabledUploader() {
return (
<Uploader
label="Upload (disabled)"
placeholder="Cannot upload files"
disabled
/>
);
}API Reference
Uploader
UploaderProps:
| Prop | Type | Default | Description |
| :------------ | :------------------------------------- | :------------------------- | :------------------------------------------------------------------------------------------------------------ |
| testID | string | — | Test ID for testing frameworks. On web this renders as data-testid; on React Native it renders as testID. |
| label | string | - | Label text above the button. |
| placeholder | string | "Select files to upload" | Button text. |
| accept | string | - | Accepted file types (e.g., ".jpg,.png"). |
| multiple | boolean | false | Allow multiple file selection. |
| disabled | boolean | false | Disabled state. |
| size | "xs" \| "sm" \| "md" \| "lg" \| "xl" | "md" | Button size. |
| onFilesChange | (files: File[]) => void | - | Callback when files change. |
File List Display
When files are selected, they appear below the button with:
- File icon
- File name (truncated if too long)
- Remove button (X icon)
Removing a file updates the list and calls onFilesChange with the updated array.
Accept Patterns
| Pattern | Description |
| :---------------- | :---------------------- |
| .pdf | PDF files only |
| .jpg,.jpeg,.png | Specific image formats |
| image/* | All image types |
| video/* | All video types |
| .doc,.docx,.pdf | Multiple document types |
Behavior
- Click button to open native file picker
- Multiple selection when
multipleis true - Files accumulate when
multipleis true (new selections add to existing) - Files replace when
multipleis false - Each file can be removed individually
- Hidden native input, styled button
Accessibility
- Button is keyboard accessible
- File input is properly associated with the button
- Remove buttons have appropriate labels
- Disabled state prevents interaction
- File list is accessible to screen readers
