Components
File/image chip for AI chat input. Renders file icons by extension, image thumbnails, image-only mode, and a hover-revealed remove button. Self-contained port from 21st.dev Agent Elements.
npx shadcn@latest add https://21st.dev/r/21stdev/file-attachmentLoading preview...
import { useState } from "react";
import { FileAttachment } from "@/components/ui/file-attachment";
const diagramSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" fill="#475569"/><rect x="12" y="12" width="16" height="16" rx="2" fill="#93c5fd"/><rect x="36" y="12" width="16" height="16" rx="2" fill="#fcd34d"/><rect x="12" y="36" width="40" height="16" rx="2" fill="#86efac"/></svg>`;
const diagramImg = `data:image/svg+xml;utf8,${encodeURIComponent(diagramSvg)}`;
type FileItem = {
id: string;
filename: string;
size: number;
isImage?: boolean;
url?: string;
};
export default function Demo() {
const [files, setFiles] = useState<FileItem[]>([
{ id: "1", filename: "notes.md", size: 2_400 },
{ id: "2", filename: "config.json", size: 580 },
{ id: "3", filename: "diagram.png", size: 124_000, isImage: true, url: diagramImg },
]);
return (
<div className="flex h-full w-full items-center justify-center p-8">
<div className="flex items-center gap-3 flex-wrap justify-center">
{files.map((f) => (
<FileAttachment
key={f.id}
{...f}
onRemove={() => setFiles((prev) => prev.filter((x) => x.id !== f.id))}
/>
))}
</div>
</div>
);
}