Marble UI
← Components

Prompt Input / Composer

Preview: Card

Chat composer: auto-growing textarea, Enter to send (Shift+Enter for newline), attachments, Stop while streaming. IME-safe: Enter during composition (e.g. Japanese input) never sends.

Import

import { PromptInput } from '@marbleui/ui'

Props

<PromptInput>

PropTypeDefaultDescription
onSubmit *required(text: string, attachments: PromptAttachment[]) => void—Gets the trimmed text and the attachments, each with its `File`. Make your own object URL from `file` if you show it after sending.
allowAttachmentsbooleantrue
classNamestring—
defaultValuestring''
isStreamingboolean—While true the send button becomes Stop.
maxRowsnumber8
onChange(v: string) => void—
onStop() => void—
placeholderstring'Ask anything…'
toolsReactNode—Controls on the left of the toolbar (model picker, tools).
valuestring—
variant'card' | 'pill''card'

Accessibility

Keyboard interaction, focus management and ARIA semantics come from the underlying primitives: React Aria Button · React Aria FileTrigger. Follow the primitive's docs for the full keyboard map.

Usage notes

Chat composer: auto-growing textarea, Enter to send, Shift+Enter for a newline, attachments, and a Stop button while streaming.

<PromptInput onSubmit={(text, files) => send(text, files)} isStreaming={busy} onStop={abort} />
  • onSubmit(text, attachments): each attachment has the picked File as file, ready to upload. Image previews use object URLs that are revoked once the attachment is sent or removed, so make your own URL from file if you show it after sending.
  • value/defaultValue/onChange control the text.
  • IME-safe: Enter during composition (Japanese, Chinese input) doesn’t submit.
  • tools adds controls to the footer (model picker, web search toggle).
  • Pair with ChatConversation and ChatMessage for the transcript.
Example source · show
import { useState } from 'react'
import { Button } from '../button'
import { PromptInput } from './prompt-input'

function Demo({ variant }: { variant: 'card' | 'pill' }) {
  const [sent, setSent] = useState<string | null>(null)
  const [streaming, setStreaming] = useState(false)
  return (
    <div className="flex w-[34rem] flex-col gap-3">
      {sent && <div className="self-end rounded-2xl bg-surface-sunken px-3 py-2 text-sm text-fg">{sent}</div>}
      <PromptInput
        variant={variant}
        isStreaming={streaming}
        onStop={() => setStreaming(false)}
        onSubmit={(t) => {
          setSent(t)
          setStreaming(true)
          setTimeout(() => setStreaming(false), 2500)
        }}
        tools={variant === 'card' ? <Button size="sm" variant="ghost" className="rounded-full text-fg-muted">Search</Button> : undefined}
      />
    </div>
  )
}

export const styles = { Card: () => <Demo variant="card" />, Pill: () => <Demo variant="pill" /> }

Source: packages/ui/src/prompt-input/prompt-input.tsx

Catalog

Category
ai-agents
Priority
Pro
Primitive
composed
React Aria
TextField + Toolbar