File chips for a chat composer: drop, paste or pick files, with image thumbnails, upload progress, error chips and remove.
Set your access key from API keys as NEXTUI_TOKEN in .env.local and add the @nextui namespace below to components.json once. Then every item installs by name. Each install counts as a copy on a free account.
$pnpm dlx shadcn@latest add @nextui/attachments
1{2"registries": {3"@nextui": {4"url": "https://nextui.dev/r/{name}.json",5"headers": { "Authorization": "Bearer ${NEXTUI_TOKEN}" }6}7}8}
1"use client";23import { useState } from "react";4import { useChat } from "@ai-sdk/react";5import { Attachments } from "@/components/nextui/attachments";67export function Composer() {8const { sendMessage } = useChat();9const [text, setText] = useState("");10const [files, setFiles] = useState<File[]>([]);11// Remount the chips after sending to clear them.12const [round, setRound] = useState(0);1314return (15<form16onSubmit={(e) => {17e.preventDefault();18// AI SDK 7 turns File objects into file parts (data URLs) for you.19const dt = new DataTransfer();20files.forEach((f) => dt.items.add(f));21sendMessage({ text, files: dt.files });22setText("");23setFiles([]);24setRound((r) => r + 1);25}}26>27<Attachments28key={round}29accept="image/*,.pdf,.csv,.txt"30maxSize={10 * 1024 * 1024}31onAdd={(added) => setFiles((prev) => [...prev, ...added])}32onRemove={(_, file) => setFiles((prev) => prev.filter((f) => f !== file))}33>34<textarea value={text} onChange={(e) => setText(e.target.value)} />35</Attachments>36</form>37);38}3940// Uploading to your own storage? Pass attachments yourself, with status "uploading" and progress 0–1.
| Prop | Type | Default | Description |
|---|---|---|---|
| attachments | Attachment[] | – | Controlled list: id, name, size, type, url, status, progress, error. Leave out and the component keeps picked files itself |
| onAdd | (files: File[]) => void | – | Called with files that pass the accept and size checks |
| onRemove | (id: string, file?: File) => void | – | Called when a chip's remove button is clicked, with the File it came from |
| accept | string | – | Allowed types, same format as the input accept attribute ("image/*,.pdf") |
| maxSize | number | 20971520 | Largest file in bytes; bigger files show an error chip |
| maxFiles | number | 10 | Files past this count are ignored |
| children | ReactNode | – | Your composer. Dropping or pasting files anywhere on it attaches them; a paperclip button sits beside it |
| autoPlay | boolean | false | Loop a scripted drop with uploads and an error, for demos. Stops when the viewer interacts |
| className | string | – | Extra classes on the wrapper |
Sign in to see the code
Preview it freely. A free account copies code and prompts a few times a day.