All articles
8 min read

How to Build an AI Chat Textarea in React with @Mentions

Modern AI apps need a chat input that does more than hold text — @mentions, /commands, attachments, and clean structured output. Here is how to build one in React without dragging in a full editor framework.

Every AI app eventually needs the same thing: a good chat input. Not a plain <textarea>, but a composer where users can @mention context, run /commands, attach files, and where you get clean structured data back to send to your model. The usual options are heavyweight — ProseMirror or Slate-based editors built for documents, not chat.

This guide builds a production-grade AI chat textarea in React using Prompt Area, a zero-dependency component distributed through the shadcn registry. You install the source into your project, so there is no black-box dependency to fight.

What we are building

  • An auto-growing chat input that feels like ChatGPT or Claude
  • @mentions for users, agents, or documents with a dropdown
  • /commands for actions like summarize or deep-research
  • Structured output you can send straight to an LLM

Step 1 — Install the component

Prompt Area installs through the shadcn registry. One command copies the component, its types, helpers, and the segment engine into your project:

npx shadcn@latest add https://prompt-area.com/r/prompt-area.json

Because it is a registry component, the source lives in your repo. There are no extra runtime dependencies beyond React.

Step 2 — Render the input with state

The usePromptAreaState() hook manages the editor value and exposes derived state (plainText, isEmpty, chips). Spread its bind onto the component and you have a working input:

'use client'

import { PromptArea } from '@/components/prompt-area'
import { usePromptAreaState } from '@/components/use-prompt-area-state'

export function ChatInput() {
  const { bind, plainText, isEmpty, clear } = usePromptAreaState()

  function handleSubmit() {
    if (isEmpty) return
    sendToModel(plainText) // your LLM call
    clear()
  }

  return (
    <PromptArea
      {...bind}
      placeholder="Ask anything…"
      onSubmit={handleSubmit}
      minHeight={48}
    />
  )
}

Step 3 — Add @mentions

Mentions are just a trigger. Use the mentionTrigger() preset and give it an onSearch function that returns items as the user types. Each selection becomes an immutable chip in the input.

import { mentionTrigger } from '@/components/trigger-presets'

const AGENTS = [
  { value: 'copywriter', label: 'Copywriter', description: 'Ad copy & content' },
  { value: 'analyst', label: 'Analyst', description: 'Performance insights' },
]

const searchAgents = (q: string) =>
  AGENTS.filter((a) => a.label.toLowerCase().includes(q.toLowerCase()))

<PromptArea
  {...bind}
  triggers={[mentionTrigger({ onSearch: searchAgents })]}
  placeholder="Mention an @agent…"
/>

Step 4 — Add /commands

Slash commands work the same way with commandTrigger(), using start-of-line detection so a / only opens the menu at the beginning of a line.

import { commandTrigger } from '@/components/trigger-presets'

const COMMANDS = [
  { value: 'summarize', label: 'summarize', description: 'Summarize the thread' },
  { value: 'deep-research', label: 'deep-research', description: 'Research in depth' },
]

const searchCommands = (q: string) =>
  COMMANDS.filter((c) => c.label.toLowerCase().includes(q.toLowerCase()))

<PromptArea
  {...bind}
  triggers={[
    mentionTrigger({ onSearch: searchAgents }),
    commandTrigger({ onSearch: searchCommands }),
  ]}
/>

Step 5 — Read structured data, not a string

This is where a chat-first component pays off. Instead of parsing a markup string, read typed chips directly with getChipsByTrigger(). You can route the data to your model however you like:

import { getChipsByTrigger } from '@/components/segment-helpers'

const mentions = getChipsByTrigger(bind.value, '@')
const commands = getChipsByTrigger(bind.value, '/')

await runChat({
  prompt: plainText,
  agents: mentions.map((m) => m.value),
  command: commands[0]?.value,
})

Because every chip is a typed segment, you never have to regex your way through the input to figure out what the user referenced.

Why not Tiptap, Lexical, or Slate?

Those frameworks are excellent for document editing, but for a chat input they are a lot of machinery: you assemble extensions or plugins and pull in several dependencies just to get mentions and a command menu. For an AI composer, a focused zero-dependency component gets you to the same UX faster and lighter. If you do need full document editing, that is the moment to reach for one of those frameworks.

Wrapping up

In a handful of steps you have an AI chat textarea with mentions, commands, auto-grow, and structured output — ready to wire into any LLM provider or the Vercel AI SDK. From here you can add #tags, file attachments, and companion components like an Action Bar or full Chat Prompt Layout.

Frequently asked questions

What is the best React component for an AI chat input?

Prompt Area is purpose-built for AI chat inputs: it combines @mentions, /commands, #tags, inline markdown, and file attachments in a single zero-dependency component installed via the shadcn registry, and returns structured data you can send straight to an LLM.

Do I need ProseMirror or Slate to build a chat textarea?

No. Those are document-editor frameworks. For a chat input you can use a focused contentEditable component like Prompt Area, which has no ProseMirror, Slate, or Lexical dependency.

How do I get structured data out of the chat input?

Call getChipsByTrigger(value, "@") (or "/") to read resolved mentions and commands as typed chips, and read plainText for the message body. No string parsing required.

Keep reading

Build it with Prompt Area

A zero-dependency React chat input with @mentions, /commands, #tags, and file attachments — installed from the shadcn registry.