@xsolla/xui-image-uploader
v0.159.0
Published
A cross-platform React image uploader component supporting click-to-pick, drag-and-drop (web), controlled and uncontrolled usage, image preview with hover-to-remove, automatic loading state from async `onUpload`, error states, and a wide horizontal layout
Readme
ImageUploader
A cross-platform React image uploader component supporting click-to-pick, drag-and-drop (web), controlled and uncontrolled usage, image preview with hover-to-remove, automatic loading state from async onUpload, error states, and a wide horizontal layout.
Installation
npm install @xsolla/xui-image-uploaderDemo
Basic (Uncontrolled)
import * as React from "react";
import {
ImageUploader,
type ImageUploaderFile,
} from "@xsolla/xui-image-uploader";
export default function Basic() {
const handleUpload = (file: ImageUploaderFile) => {
console.log("Picked file:", file.name, file.size);
};
return <ImageUploader onUpload={handleUpload} />;
}Controlled
import * as React from "react";
import {
ImageUploader,
type ImageUploaderValue,
} from "@xsolla/xui-image-uploader";
export default function Controlled() {
const [value, setValue] = React.useState<ImageUploaderValue | null>(null);
return (
<ImageUploader
value={value}
onChange={setValue}
placeholder="Upload avatar"
/>
);
}Wide View with Description
import * as React from "react";
import { ImageUploader } from "@xsolla/xui-image-uploader";
export default function WideView() {
return (
<ImageUploader
wideView
placeholder="Upload cover image"
description="PNG or JPG, up to 5 MB"
/>
);
}Async Upload (Recommended)
If onUpload returns a Promise, the component automatically:
- Shows the uploading state (spinner +
uploadingPlaceholder) until the promise settles. - On resolve with
{ url, filename? }or a barestringURL, firesonChange(value)with the canonical value — no manual wiring needed. - On reject, fires
onChange(null, error).
You don't need async/await in the handler — just return the upload promise:
import { ImageUploader } from "@xsolla/xui-image-uploader";
export default function AutoUpload() {
return (
<ImageUploader
uploadingPlaceholder="Uploading…"
// uploadFn can return Promise<string> or Promise<{ url, filename? }>
onUpload={(file) => uploadFn(file.file)}
onChange={(value) => form.setFieldValue("avatar", value?.url ?? null)}
/>
);
}Manual Loading Control
For finer-grained control (e.g. uploads triggered outside the component, or
to keep the spinner visible during post-upload work), pass loading explicitly:
import * as React from "react";
import {
ImageUploader,
type ImageUploaderFile,
} from "@xsolla/xui-image-uploader";
export default function WithLoading() {
const [loading, setLoading] = React.useState(false);
const handleUpload = async (file: ImageUploaderFile) => {
setLoading(true);
try {
await uploadToServer(file.file);
} finally {
setLoading(false);
}
};
return (
<ImageUploader
loading={loading}
uploadingPlaceholder="Uploading…"
onUpload={handleUpload}
/>
);
}Error State
import * as React from "react";
import { ImageUploader } from "@xsolla/xui-image-uploader";
export default function WithError() {
return (
<ImageUploader
placeholder="cover.png"
errorMessage="File is too large (max 5 MB)."
/>
);
}React Native (Custom Picker)
On native there is no DOM <input type="file">. Pass an openPicker prop that
returns a normalized ImageUploaderFile (e.g. wrapping expo-image-picker or
react-native-image-picker).
import * as React from "react";
import * as ImagePicker from "expo-image-picker";
import {
ImageUploader,
type ImageUploaderFile,
} from "@xsolla/xui-image-uploader";
export default function Native() {
const openPicker = async (): Promise<ImageUploaderFile | null> => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
});
if (result.canceled) return null;
const asset = result.assets[0];
return {
name: asset.fileName ?? "image",
size: asset.fileSize ?? 0,
uri: asset.uri,
mimeType: asset.mimeType,
};
};
return <ImageUploader openPicker={openPicker} />;
}Anatomy
+--------------------------+
| |
| [Image] | <- icon (or Spinner while loading)
| Upload | <- placeholder
| PNG/JPG, up to 5MB | <- description (wideView only)
| |
+--------------------------+
File is too large… <- errorMessage (when present)When a file has been uploaded, the box renders the image preview and reveals a trash-can affordance over a dimming scrim on hover; clicking (or tapping) removes the image. On touch and React Native devices — where there is no hover — the trash overlay is shown unconditionally so the delete affordance is always discoverable.
API Reference
ImageUploader
| 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. |
| size | "xl" \| "lg" \| "md" \| "sm" \| "xs" | "xl" | Visual size of the uploader. |
| placeholder | React.ReactNode | "Upload" | Label shown under the icon (or filename in error state). Accepts a string or a custom React node. |
| uploadingPlaceholder | React.ReactNode | "Uploading" | Label shown while loading is true. Accepts a string or a custom React node. |
| description | React.ReactNode | - | Helper text below the placeholder. Only rendered when wideView is true. |
| errorMessage | string | - | Error message — when provided, the component renders in the error state. |
| wideView | boolean | false | Use the horizontal layout. The box stretches to fill its parent's width. |
| disabled | boolean | false | Disabled state. |
| loading | boolean | false | Controlled loading state (shows spinner + uploading label). |
| value | ImageUploaderValue \| null | - | Controlled value. When provided, the component reflects this value. |
| onUpload | (file: ImageUploaderFile) => void \| Promise<ImageUploaderValue \| string \| void> | - | Fires when the user picks (or drops) a file. If it returns a Promise, the component shows the uploading state until it settles, forwards a resolved value (object or bare URL string) to onChange, and forwards a rejection to onChange(null, error). |
| onChange | (value: ImageUploaderValue \| null, error?: Error) => void | - | Fires when the displayed value changes — on file pick (with a local data-URL preview), after onUpload resolves (with the server value), or on remove (null). The optional error arg carries any rejection from onUpload. |
| onDelete | () => void | - | Fires when the user removes the image (trash click / clear). |
| accept | string | "image/*" | Accepted MIME types for the file input (web only). |
| openPicker | () => Promise<ImageUploaderFile \| null> | - | Native file picker hook. Required on native (no DOM <input type="file">). On web, omit this and the component falls back to a hidden file input. |
| themeMode | ThemeMode | - | Override the theme mode for this instance. |
| themeProductContext | ThemeProductContext | - | Override the product theme context for this instance. |
ImageUploaderValue
Controlled value shape.
interface ImageUploaderValue {
filename: string;
url?: string;
}Loading and error states are driven by the
loadinganderrorMessageprops, not by fields onImageUploaderValue.
ImageUploaderFile
Normalized file shape produced by the platform picker / drag-drop pipeline.
On web, uri is a data-URL and the original DOM File is passed through for
consumers that need it (e.g. to upload via FormData / fetch). On native,
uri is a file URI and file is omitted.
interface ImageUploaderFile {
name: string;
size: number;
uri: string;
mimeType?: string;
/** The original DOM File (web only) */
file?: File;
}Platform Support
This package works on both web and React Native. The component uses
cross-platform primitives (Box, Text, Spinner) and gates web-only APIs
(DOM file input, drag-and-drop, FileReader) behind an isWeb check. On
native, supply an openPicker prop to integrate with your image picker
library of choice.
Touch / Mobile
On touch web (detected via matchMedia("(hover: none)")) and on React Native,
the component skips the hover-only delete affordance and always shows the
trash overlay over the image preview, ensuring the remove action is reachable
without a pointer.
Layout
In wideView, the component stretches to fill its parent's full width
(width: 100%), letting the consumer's layout (form column, grid cell, etc.)
control the maximum size. The square (non-wide) variants use fixed pixel
dimensions from the size tokens.
Accessibility
- The picker surface is rendered as a
<button>on web with an accessible label that reflects the current state ("Upload","Uploading", or"Remove image: <filename>"). aria-busyis set whileloadingis true.aria-invalidis set in the error state and the error message is linked viaaria-describedby(and rendered withrole="alert").- The
descriptionelement (when present) is linked viaaria-describedby. - Decorative icons and the dimming scrim are marked
aria-hidden. - Focus styling only appears for keyboard focus (
:focus-visible), not for pointer focus left over after the native picker is dismissed. - The hidden file input is reset after every selection so the same file can be re-selected to retry a failed upload.
