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, AttachmentTile } from "@/components/ui/attachment";

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

<AttachmentTile file={photo} progress={photoProgress} onRemove={removePhoto} />;

Two components share the upload logic of use-attachment.ts and take the same props:

ComponentWhat it is
AttachmentA row for a document: thumbnail or icon, name, size or error, and a progress bar while it uploads.
AttachmentTileA 72pt square for a grid of photos: a spinner while it uploads, a remove button in the corner.

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.
progressnumber—Upload progress from 0 to 1. A bar on a row, a spinner on a tile.
errorstring | boolean—Error state: red border and the message instead of the size.
onPress() => void—Opens the file, for example in a preview.
onRemove() => void—Shows a remove button. While uploading, it’s up to you to cancel the request.
onRetry() => void—In 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) => {
		const Preview = isMedia(d) ? AttachmentTile : Attachment;
		return (
			<Preview
				key={d.id}
				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)}
/>