Composer

The prompt input, and the clearest expression of the seam language: the whole surface is a debossed well you act into; the send key inside it is the embossed token that fires the action.

Enter to send · Enter for a new line

tsx
"use client"

import * as React from "react"
import { Paperclip } from "lucide-react"

import { Button } from "@/registry/seam/ui/button"
import {
  Composer,
  ComposerSubmit,
  ComposerTextarea,
  ComposerToolbar,
  ComposerTools,
} from "@/registry/seam/ui/composer"
import { Kbd } from "@/registry/seam/ui/kbd"

export default function ComposerDemo() {
  const [value, setValue] = React.useState("")

  const submit = (e: React.FormEvent) => {
    e.preventDefault()
    if (!value.trim()) return
    setValue("")
  }

  return (
    <div className="w-full max-w-md">
      <Composer onSubmit={submit}>
        <ComposerTextarea
          value={value}
          onChange={(e) => setValue(e.target.value)}
          placeholder="Ask anything…"
        />
        <ComposerToolbar>
          <ComposerTools>
            <Button
              type="button"
              variant="ghost"
              size="icon"
              className="size-8"
              aria-label="Attach file"
            >
              <Paperclip />
            </Button>
          </ComposerTools>
          <ComposerSubmit disabled={!value.trim()} />
        </ComposerToolbar>
      </Composer>
      <p className="text-muted-foreground mt-2 text-center text-xs">
        <Kbd>Enter</Kbd> to send · <Kbd>⇧</Kbd> <Kbd>Enter</Kbd> for a new line
      </p>
    </div>
  )
}
bunx --bun @seamui/cli@latest add composer

API

PropTypeDefaultDescription
status"ready" | "streaming""ready"While streaming, the submit key becomes a stop control (its type switches to button).
onStop() => voidFired by the stop key while status is streaming.
onSubmitFormEventHandlerComposer renders a real <form>; Enter in the textarea submits it.
value / onChangeTextarea propsOn ComposerTextarea — the controlled text; you own the string.
onRemove() => voidOn ComposerAttachment — renders a ghost remove key inside the chip.

ComposerTextarea accepts all Textarea props; ComposerSubmit accepts all Button props.

Notes

  • Fully controlled and transport-agnostic: you own value, onSubmit, and status — the shape maps 1:1 onto the AI SDK's useChat, with no runtime dependency on it.
  • Enter submits, Shift+Enter inserts a newline, and submitting via keyboard never steals focus from the textarea. The submit/stop control swaps its aria-label and button type with the status.
  • The send/stop icon crossfades on opacity — identical under reduced motion. Attachment chips rise in and fall out on springs.snappy, collapsing to opacity fades under reduced motion.
  • The well itself stays still — text entry is calm, with only a focus ring for feedback.

Press feedback, reduced motion, and haptics follow the global policy — see Motion and Haptics.