Attachment
Pre-alphaThe registry and the CLI are not published yet.Roadmap

Attachment

File attachment with preview, upload progress, removal and error state.

Expense report

Submit

Receipts

hotel-lisbon.pdf

1.2 MB

taxi-receipt.jpg

64%
dinner.heic

Upload failed · Tap to retry

Installation

npx axiom add attachment
pnpm dlx axiom add attachment
yarn dlx axiom add attachment
bun x axiom add attachment

Also copies: icon, icon-button, spinner, text and tappable.

Requires the close, error, file, image and refresh icons in your icon registry. Colors come from attachment.{row,tile}.{default,uploading,done,error} in the component tokens.

Usage

import { Attachment } from "@/components/ui/attachment";

<Attachment
	file={file}
	progress={uploadProgress}
	error={uploadError}
	onRemove={() => removeFile(file.id)}
	onRetry={() => upload(file)}
/>;

Props

PropTypeDefaultDescription
file{ name: string; size?: number; mimeType?: string; uri?: string }The file to show. An image/* or video/* mimeType gets the image icon and, with a uri, a thumbnail; anything else gets file.
status'idle' | 'uploading' | 'done' | 'error'derivedUpload state. Derived from progress and error when not set.
progressnumberUpload progress from 0 to 1. Shows a bar (row) or a ring (tile).
errorstring | booleanError state: red border and the message instead of the size.
variant'row' | 'tile''row'row for documents with a name, tile for a square thumbnail in a grid of photos.
onPress() => voidOpens the file, for example in a preview.
onRemove() => voidShows a remove button. While uploading, it’s up to you to cancel the request.
onRetry() => voidIn the error state, pressing the attachment or the retry icon calls it.
formatSize(bytes: number) => string1.2 MB styleCustom size formatting.
styleStyleProp<ViewStyle>Extra styles for the container.

Axiom handles the UI and its states. The upload itself stays in your app’s code: pass progress and error from your upload logic.

Use cases

Attachments in a message composer

Tiles above the input, each with its own upload state, before the message is sent.

brief.pdf

Here are the shots

<ScrollView horizontal>
	{drafts.map((d) => (
		<Attachment
			key={d.id}
			variant={isMedia(d) ? "tile" : "row"}
			file={d.file}
			progress={d.progress}
			onRemove={() => removeDraft(d.id)}
		/>
	))}
</ScrollView>

Documents required by a form

A dashed placeholder to add a file, replaced by the attachment once picked.

Verify identity
Front of ID
id-front.jpgUploaded
Back of ID
Take a photo or choose a file
{
	back ? (
		<Attachment
			file={back.file}
			progress={back.progress}
			error={back.error}
			onRemove={() => setBack(null)}
		/>
	) : (
		<UploadPlaceholder
			icon="camera"
			label="Take a photo or choose a file"
			onPress={pickBack}
		/>
	);
}

Upload failure

The error replaces the size, and the whole attachment becomes a retry button.

Files
interview.m4a

File too large (max 25 MB)

notes.txt

Connection lost · Tap to retry

<Attachment
	file={file}
	error={
		error?.code === "too_large"
			? "File too large (max 25 MB)"
			: "Connection lost · Tap to retry"
	}
	onRetry={error?.code === "too_large" ? undefined : () => upload(file)}
	onRemove={() => remove(file.id)}
/>