# Prompt Input

The field people type into to message an agent, with attachments, model menus and a send button.

Source: https://suiss.app/ui/docs/ai/prompt-input

**React**

**Example**

```tsx
"use client"

import {
  ArrowUpIcon,
  CaretDownIcon,
  FileTextIcon,
  PlusIcon,
  XIcon,
} from "@phosphor-icons/react"
import * as React from "react"
import {
  Attachment,
  AttachmentAction,
  AttachmentActions,
  AttachmentContent,
  AttachmentMedia,
  AttachmentTitle,
} from "@/components/ui/attachment"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuLabel,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
  InputGroup,
  InputGroupAddon,
  InputGroupButton,
  InputGroupTextarea,
} from "@/components/ui/input-group"

type AttachmentFile = {
  id: string
  name: string
}

const providers = [
  {
    name: "Anthropic",
    models: [
      { id: "claude-opus-5-5", name: "Claude Opus 5.5" },
      { id: "claude-sonnet-5-5", name: "Claude Sonnet 5.5" },
    ],
  },
  {
    name: "OpenAI",
    models: [
      { id: "gpt-5", name: "GPT-5" },
      { id: "gpt-5-mini", name: "GPT-5 mini" },
    ],
  },
]

const models = providers.flatMap((provider) => provider.models)

const efforts = [
  { id: "low", name: "Low" },
  { id: "medium", name: "Medium" },
  { id: "high", name: "High" },
]

export default function PromptInputDemo() {
  const [text, setText] = React.useState("")
  const [files, setFiles] = React.useState<AttachmentFile[]>([
    { id: "settings-page", name: "settings-page.tsx" },
  ])
  const [model, setModel] = React.useState("claude-opus-5-5")
  const [effort, setEffort] = React.useState("medium")
  const formRef = React.useRef<HTMLFormElement>(null)
  const inputRef = React.useRef<HTMLInputElement>(null)
  const modelName = models.find((item) => item.id === model)?.name ?? model
  const effortName = efforts.find((item) => item.id === effort)?.name ?? effort
  const canSend = text.trim().length > 0

  function onSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault()
    if (!canSend) {
      return
    }

    setText("")
    setFiles([])
  }

  function onAttach(event: React.ChangeEvent<HTMLInputElement>) {
    const selected = event.target.files
    if (!selected) {
      return
    }

    setFiles((current) => [
      ...current,
      ...Array.from(selected, (file) => ({
        id: crypto.randomUUID(),
        name: file.name,
      })),
    ])
    event.target.value = ""
  }

  return (
    <div className="w-full max-w-lg py-6">
      <form ref={formRef} onSubmit={onSubmit}>
        <input
          ref={inputRef}
          type="file"
          multiple
          aria-label="Files"
          className="sr-only"
          tabIndex={-1}
          onChange={onAttach}
        />
        <InputGroup>
          {files.length > 0 && (
            <InputGroupAddon align="block-start" className="flex-wrap">
              {files.map((file) => (
                <Attachment key={file.id} size="sm">
                  <AttachmentMedia>
                    <FileTextIcon />
                  </AttachmentMedia>
                  <AttachmentContent>
                    <AttachmentTitle>{file.name}</AttachmentTitle>
                  </AttachmentContent>
                  <AttachmentActions>
                    <AttachmentAction
                      aria-label={`Remove ${file.name}`}
                      onClick={() =>
                        setFiles((current) =>
                          current.filter((item) => item.id !== file.id)
                        )
                      }
                    >
                      <XIcon />
                    </AttachmentAction>
                  </AttachmentActions>
                </Attachment>
              ))}
            </InputGroupAddon>
          )}
          <InputGroupTextarea
            value={text}
            onChange={(event) => setText(event.target.value)}
            placeholder="Ask the agent to change your code..."
            aria-label="Message"
            onKeyDown={(event) => {
              if (event.key === "Enter" && !event.shiftKey) {
                event.preventDefault()
                formRef.current?.requestSubmit()
              }
            }}
            className="p-3"
          />
          <InputGroupAddon align="block-end">
            <InputGroupButton
              size="icon-sm"
              variant="outline"
              className="rounded-full"
              aria-label="Attach files"
              onClick={() => inputRef.current?.click()}
            >
              <PlusIcon />
            </InputGroupButton>
            <DropdownMenu>
              <DropdownMenuTrigger
                render={
                  <InputGroupButton
                    size="sm"
                    variant="outline"
                    aria-label={`Model: ${modelName}`}
                    className="ms-auto"
                  />
                }
              >
                {modelName}
                <CaretDownIcon />
              </DropdownMenuTrigger>
              <DropdownMenuContent align="start" className="w-52">
                <DropdownMenuRadioGroup value={model} onValueChange={setModel}>
                  {providers.map((provider, index) => (
                    <React.Fragment key={provider.name}>
                      {index > 0 && <DropdownMenuSeparator />}
                      <DropdownMenuGroup>
                        <DropdownMenuLabel>{provider.name}</DropdownMenuLabel>
                        {provider.models.map((item) => (
                          <DropdownMenuRadioItem
                            key={item.id}
                            value={item.id}
                            closeOnClick
                          >
                            {item.name}
                          </DropdownMenuRadioItem>
                        ))}
                      </DropdownMenuGroup>
                    </React.Fragment>
                  ))}
                </DropdownMenuRadioGroup>
              </DropdownMenuContent>
            </DropdownMenu>
            <DropdownMenu>
              <DropdownMenuTrigger
                render={
                  <InputGroupButton
                    size="sm"
                    variant="outline"
                    aria-label={`Effort: ${effortName}`}
                  />
                }
              >
                <span className="text-muted-foreground">Effort</span>
                {effortName}
                <CaretDownIcon />
              </DropdownMenuTrigger>
              <DropdownMenuContent align="start" className="w-40">
                <DropdownMenuGroup>
                  <DropdownMenuLabel>Effort</DropdownMenuLabel>
                  <DropdownMenuRadioGroup
                    value={effort}
                    onValueChange={setEffort}
                  >
                    {efforts.map((item) => (
                      <DropdownMenuRadioItem
                        key={item.id}
                        value={item.id}
                        closeOnClick
                      >
                        {item.name}
                      </DropdownMenuRadioItem>
                    ))}
                  </DropdownMenuRadioGroup>
                </DropdownMenuGroup>
              </DropdownMenuContent>
            </DropdownMenu>
            <InputGroupButton
              type="submit"
              size="icon-sm"
              variant="default"
              className="rounded-full"
              disabled={!canSend}
              aria-label="Send"
            >
              <ArrowUpIcon />
            </InputGroupButton>
          </InputGroupAddon>
        </InputGroup>
      </form>
    </div>
  )
}
```

Prompt Input is the field the person types into to message the agent. It is an [Input Group](https://suiss.app/ui/docs/components/input-group) with a textarea, an attach button, the [Model Selector](https://suiss.app/ui/docs/ai/model-selector) menus and a send button. Attached files show as [Attachment](https://suiss.app/ui/docs/components/attachment) chips above the text.

## Installation

Prompt Input is built with Input Group, Attachment and Dropdown Menu.

```bash title="Terminal"
npx shadcn@latest add @esuiss/input-group @esuiss/attachment @esuiss/dropdown-menu
```

Or use the [suiss/ui CLI](https://suiss.app/ui/docs/cli):

```bash title="Terminal"
npx @esuiss/ui@latest add input-group attachment dropdown-menu
```

## Usage

```tsx
import {
  InputGroup,
  InputGroupAddon,
  InputGroupButton,
  InputGroupTextarea,
} from "@/components/ui/input-group"
```

```tsx
<form ref={formRef} onSubmit={onSubmit}>
  <InputGroup>
    <InputGroupTextarea
      value={text}
      onChange={(event) => setText(event.target.value)}
      placeholder="Ask the agent to change your code..."
      aria-label="Message"
      className="p-3"
    />
    <InputGroupAddon align="block-end">
      <InputGroupButton
        size="icon-sm"
        variant="outline"
        className="rounded-full"
        aria-label="Attach files"
        onClick={() => inputRef.current?.click()}
      >
        <PlusIcon />
      </InputGroupButton>
      <InputGroupButton
        type="submit"
        size="icon-sm"
        variant="default"
        className="ms-auto rounded-full"
        disabled={!canSend}
        aria-label="Send"
      >
        <ArrowUpIcon />
      </InputGroupButton>
    </InputGroupAddon>
  </InputGroup>
</form>
```

The attach button opens a hidden file input. Show each file as an `Attachment` in an addon with `align="block-start"`, with a button to remove it.

Put the two [Model Selector](https://suiss.app/ui/docs/ai/model-selector) menus in the `block-end` addon, between the attach and send buttons. Move `ms-auto` from the send button to the first menu trigger, so both menus sit next to the send button.

Enter sends the message and Shift+Enter adds a new line:

```tsx
onKeyDown={(event) => {
  if (event.key === "Enter" && !event.shiftKey) {
    event.preventDefault()
    formRef.current?.requestSubmit()
  }
}}
```

## AI SDK

Send the text and the `FileList` from the file input.

```tsx
sendMessage({ text, files })
```

The route streams the reply with `result.toUIMessageStreamResponse()`.

## API Reference

See [Input Group](https://suiss.app/ui/docs/components/input-group#api-reference), [Attachment](https://suiss.app/ui/docs/components/attachment#api-reference) and [Dropdown Menu](https://suiss.app/ui/docs/components/dropdown-menu#api-reference).

## Accessibility

- Give the textarea an accessible name such as "Message", since the placeholder is not a label.
- Name each remove button after its file, such as "Remove settings-page.tsx".
- Disable the send button while the field is empty.

**React Native, SwiftUI, Jetpack Compose**

```tsx
import { ArrowUpIcon, CaretDownIcon, FileTextIcon, PlusIcon, XIcon } from "phosphor-react-native"
import { View } from "react-native"
import { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentMedia, AttachmentTitle } from "@/components/ui/attachment"
import { Icon } from "@/components/ui/icon"
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupTextarea } from "@/components/ui/input-group"
import { Text } from "@/components/ui/text"

export function AiPromptInputExample() {
  return (
    <InputGroup>
      <InputGroupAddon align="block-start">
        <Attachment size="xs">
          <AttachmentMedia>
            <Icon as={FileTextIcon} />
          </AttachmentMedia>
          <AttachmentContent>
            <AttachmentTitle>settings-page.tsx</AttachmentTitle>
          </AttachmentContent>
          <AttachmentActions>
            <AttachmentAction accessibilityLabel="Remove">
              <Icon as={XIcon} />
            </AttachmentAction>
          </AttachmentActions>
        </Attachment>
      </InputGroupAddon>
      <InputGroupTextarea
        placeholder="Ask the agent to change your code..."
        numberOfLines={3}
      />
      <InputGroupAddon align="block-end" className="justify-between">
        <InputGroupButton size="icon-xs" accessibilityLabel="Attach a file">
          <Icon as={PlusIcon} />
        </InputGroupButton>
        <View className="flex-row items-center gap-2">
          <InputGroupButton size="xs">
            <Text>Claude Opus 5.5</Text>
            <Icon as={CaretDownIcon} />
          </InputGroupButton>
          <InputGroupButton
            variant="default"
            size="icon-xs"
            accessibilityLabel="Send"
          >
            <Icon as={ArrowUpIcon} />
          </InputGroupButton>
        </View>
      </InputGroupAddon>
    </InputGroup>
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct PromptInputExample: View {
  @State private var message = ""

  var body: some View {
    InputGroup {
      InputGroupAddon(align: .blockStart) {
        Attachment(size: .xs) {
          AttachmentMedia { Ph.fileText.regular }
          AttachmentContent { AttachmentTitle("settings-page.tsx") }
          AttachmentActions { AttachmentAction("Remove", icon: Ph.x.bold) {} }
        }
      }
      InputGroupTextarea("Ask the agent to change your code...", text: $message, lineLimit: 3...8)
      InputGroupAddon(align: .blockEnd) {
        InputGroupButton(size: .iconXs) {} label: {
          Ph.plus.regular.frame(width: 16, height: 16)
        }
        .accessibilityLabel(Text("Attach a file"))
        Spacer(minLength: 0)
        InputGroupButton(size: .xs) {} label: {
          Text("Claude Opus 5.5")
        }
        InputGroupButton(variant: .default, size: .iconXs) {} label: {
          Ph.arrowUp.regular.frame(width: 16, height: 16)
        }
        .accessibilityLabel(Text("Send"))
      }
    }
  }
}
```

```kotlin
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.ArrowUp
import com.adamglin.phosphoricons.regular.FileText
import com.adamglin.phosphoricons.regular.Plus
import com.adamglin.phosphoricons.regular.X
import com.esuiss.ui.components.Attachment
import com.esuiss.ui.components.AttachmentAction
import com.esuiss.ui.components.AttachmentActions
import com.esuiss.ui.components.AttachmentContent
import com.esuiss.ui.components.AttachmentMedia
import com.esuiss.ui.components.AttachmentSize
import com.esuiss.ui.components.AttachmentTitle
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.InputGroup
import com.esuiss.ui.components.InputGroupAddon
import com.esuiss.ui.components.InputGroupAddonAlign
import com.esuiss.ui.components.InputGroupButton
import com.esuiss.ui.components.InputGroupButtonSize
import com.esuiss.ui.components.InputGroupTextarea
import com.esuiss.ui.components.Text

@Composable
fun PromptInputAIDefault() {
    var message by rememberSaveable { mutableStateOf("") }
    InputGroup {
        InputGroupAddon(Modifier.fillMaxWidth(), align = InputGroupAddonAlign.BlockStart) {
            Attachment(size = AttachmentSize.Xs) {
                AttachmentMedia { Icon(PhosphorIcons.Regular.FileText, contentDescription = null) }
                AttachmentContent { AttachmentTitle("settings-page.tsx") }
                AttachmentActions {
                    AttachmentAction(onClick = {}, contentDescription = "Remove") {
                        Icon(PhosphorIcons.Regular.X, contentDescription = null)
                    }
                }
            }
        }
        InputGroupTextarea(message, { message = it }, placeholder = "Ask the agent to change your code...", minLines = 3)
        InputGroupAddon(Modifier.fillMaxWidth(), align = InputGroupAddonAlign.BlockEnd) {
            InputGroupButton(onClick = {}, size = InputGroupButtonSize.IconXs) {
                Icon(PhosphorIcons.Regular.Plus, contentDescription = "Attach a file")
            }
            Spacer(Modifier.weight(1f))
            InputGroupButton(onClick = {}, size = InputGroupButtonSize.Xs) { Text("Claude Opus 5.5") }
            InputGroupButton(onClick = {}, size = InputGroupButtonSize.IconXs, variant = ButtonVariant.Default) {
                Icon(PhosphorIcons.Regular.ArrowUp, contentDescription = "Send")
            }
        }
    }
}
```

Prompt Input on mobile is built from the same parts as on the web: [Input Group](https://suiss.app/ui/docs/components/input-group), [Attachment](https://suiss.app/ui/docs/components/attachment). The example above is a static demo; connect it to your agent by rendering the same parts from your messages.

## Installation

**React Native**

```bash title="Terminal"
npx @esuiss/ui@latest add input-group attachment
```

**SwiftUI**

```bash title="Terminal"
npx @esuiss/ui@latest add input-group attachment --platform swift
```

**Jetpack Compose**

```bash title="Terminal"
npx @esuiss/ui@latest add input-group attachment --platform kotlin
```

## API Reference

See [Input Group](https://suiss.app/ui/docs/components/input-group), [Attachment](https://suiss.app/ui/docs/components/attachment).

