Image upload
● Stable · v0.1Add photos as a grid of thumbnails — a receipt, a product, a shopfront — each showing its upload state right on the tile. The same controlled model as File upload, but you see the picture, not a filename.
In context
Try it
On the web this opens your picker (images only) and previews them instantly. On a phone it asks the app to open the camera/library picker.
Every state
Purpose
Image upload is for pictures, where the thumbnail itself is the label. A grid of tiles with an "add" tile at the end makes it obvious how many photos there are and how to add another, with each tile carrying its own progress or error.
Anatomy
- Thumbnail tile — the image, cropped to fill a square (or a circle for a single avatar). Carries a remove (×) badge.
- Add tile — a dashed tile with a plus that opens the picker. It hides once
maxis reached. - Per-tile overlay — a dimmed tile with a progress bar while uploading, or a red overlay with Retry if it fails.
- Counter / helper — quiet text under the grid, showing
n / maxwhen a limit is set.
Behaviour
Controlled like File upload: you own the images array ({ id, uri, status?, progress?, error? }). On web the add tile calls onSelect with object-URL
previews; on native it calls onRequestPick (wire expo-image-picker). Use
shape="circle" with max={1} for a single avatar/logo.
Code
import { ImageUpload } from '@daytwo/components'const [images, setImages] = useState([])<ImageUploadlabel="Add photos"helperText="Up to 4 photos of your products"max={4}images={images}onSelect={(picked) => addAndUpload(picked)} // webonRequestPick={pickWithExpo} // nativeonRemove={(id) => setImages((m) => m.filter((x) => x.id !== id))}onRetry={(id) => retry(id)}/>// Single avatar:<ImageUpload label="Shop logo" shape="circle" max={1} images={logo} />
When to use
- For photos — products, a shopfront, a receipt the owner snaps with the camera.
- For an avatar or logo (
shape="circle",max={1}). - When seeing the picture matters more than seeing a filename.
When NOT to use
- For documents (PDFs, statements), use File upload.
- For a single profile photo with cropping UI beyond this — compose a dedicated flow.
- To display a gallery that can't be edited — that's just images, not an upload.
Anti-patterns
The whole point of an image upload is the preview. Always show the thumbnail so the owner can confirm they picked the right one — and remove it if not.
Edge cases
n / maxshape="circle", max={1} — replaces in place