Attachment chip
A file in one compact chip: its type, name and size. Under an email it opens and downloads; in the Flo composer it shows the upload and can be removed.
Playground
Change the controls to try each prop. The code under the preview updates as you go and lists only the props you changed; copy it into your page.
import { AttachmentChip } from '@/ui/attachment-chip';<AttachmentChip name="Maya Chen CV.pdf" bytes={253952} />Default
A file that came with an email. The name opens a preview (onOpen) and the icon downloads it (onDownload). They are two targets: hover fills the chip and underlines the name, and each takes its own focus ring.
import { AttachmentChip } from '@/ui/attachment-chip';
import { toast } from '@/ui/toast';
export default function AttachmentChipDefault() {
return (
<AttachmentChip
name="Maya Chen CV.pdf"
bytes={253952}
onOpen={() => toast.add({ title: 'Opened Maya Chen CV.pdf' })}
onDownload={() => toast.add({ title: 'Downloading Maya Chen CV.pdf' })}
/>
);
}File types
The icon carries the type in a status colour: PDF red, Word blue, image purple, sheet green, anything else grey. The chips sit in an AttachmentList, labelled "Attachments", and wrap with a 6px gap.
import { AttachmentChip, AttachmentList } from '@/ui/attachment-chip';
const files = [
{ name: 'Maya Chen CV.pdf', bytes: 253952 },
{ name: 'Senior Data Engineer JD.docx', bytes: 36864 },
{ name: 'offer-letter-scan.png', bytes: 1258291 },
{ name: 'Q3 fees.xlsx', bytes: 86016 },
{ name: 'invite.ics', bytes: 2048 },
];
export default function AttachmentChipFileTypes() {
return (
<AttachmentList>
{files.map((file) => (
<AttachmentChip key={file.name} name={file.name} bytes={file.bytes} onOpen={() => {}} onDownload={() => {}} />
))}
</AttachmentList>
);
}Long name
The chip stops at 288px. The name ends in an ellipsis; the size and Download stay.
import { AttachmentChip } from '@/ui/attachment-chip';
export default function AttachmentChipLongName() {
return <AttachmentChip name="Halcyon Bank · Head of Platform · shortlist notes v3.docx" bytes={53248} onOpen={() => {}} onDownload={() => {}} />;
}Read only
On a thread shared through a job, teammates see that a file came, not its contents: a paperclip, no preview and no Download.
import { AttachmentChip } from '@/ui/attachment-chip';
export default function AttachmentChipReadOnly() {
return <AttachmentChip name="Maya Chen CV.pdf" bytes={253952} readOnly />;
}In the composer
variant="composer" is the 36px chip inside the Flo composer: the type in letters on a 24px tile, the name over the size, and a remove button. While uploading, a spinner takes the tile and the size line says "Uploading…", or the percentage with a 2px line along the bottom when progress is known. A refused file turns red and says why in words; it stays until removed, and the other files still send.
- Maya Chen CV.pdf
- Cover letter, Halcyon.docx
- Q3 shortlist.xlsx
- Portfolio.zip
import { useState } from 'react';
import { AttachmentChip, AttachmentList } from '@/ui/attachment-chip';
export default function AttachmentChipComposer() {
const [files, setFiles] = useState([
{ name: 'Maya Chen CV.pdf', bytes: 188416, status: 'ready' as const },
{ name: 'Cover letter, Halcyon.docx', bytes: 28672, status: 'uploading' as const, progress: 62 },
{ name: 'Q3 shortlist.xlsx', bytes: 86016, status: 'uploading' as const },
{ name: 'Portfolio.zip', bytes: 35651584, status: 'failed' as const, error: 'Too big · 34 MB, limit 20 MB' },
]);
return (
<AttachmentList label="Files for Flo">
{files.map((file) => (
<AttachmentChip
key={file.name}
variant="composer"
{...file}
onRemove={() => setFiles((current) => current.filter((other) => other.name !== file.name))}
/>
))}
</AttachmentList>
);
}