Composer mobile
A bottom-anchored phone composer that rides the on-screen keyboard, with attach sources and tools in a bottom sheet.
On this page
Installation
Usage
import { ComposerMobile } from "@/components/ui/chatcn/composer-mobile"
<ComposerMobile
status={busy ? "streaming" : "idle"}
onSubmit={({ text, tools }) => send(text, { tools })}
onStop={stop}
onAttach={(source) => openPicker(source)}
/>Put it last in a scrolling column: it is sticky by default. Use position="fixed" for a full-page chat, or absolute inside a positioned frame.
Examples
Keyboard aware
iOS Safari lays the keyboard over the page instead of resizing it, so a bottom-anchored composer would sit behind it. useChatcnShellKeyboard reads the covered height from visualViewport and the composer moves up by exactly that, every frame, without a transition that would lag behind the keyboard. While the keyboard is up, the home-indicator padding goes away. Browsers that resize the page report no inset and nothing moves.
Tools sheet
The plus button lowers the keyboard and opens a sheet with attach sources and tools. Turning a tool on closes the sheet, shows a chip above the field and swaps the placeholder. Drag the grabber down or flick to dismiss.
Props
| Prop | Type | Notes |
|---|---|---|
value, defaultValue, onValueChange | string | Controlled or uncontrolled text |
onSubmit | ({ text, tools }) => void | Send button or Cmd/Ctrl+Enter. Enter adds a line on touch screens |
status | "idle" | "submitting" | "streaming" | Streaming turns Send into Stop |
onStop | () => void | Stop button, or Escape in the field |
disabled | boolean | |
placeholder | string | Default Message…. An active tool's placeholder wins |
sources | ComposerMobileSource[] | Camera, Photos and Files by default. [] hides the row |
tools | ComposerMobileTool[] | Web search, Deep research, Image and Canvas by default |
activeTools, defaultActiveTools, onToolsChange | string[] | Ids of the tools that are on |
onToolSelect | (id, active) => void | One tool turned on or off |
onAttach | (sourceId) => void | Open the camera, photo library or file picker here |
header | ReactNode | Attachments or chips above the field |
position | "sticky" | "fixed" | "absolute" | Default sticky |
keyboardInset | number | Overrides the measured keyboard height, for demos and tests |
sheetOpen, defaultSheetOpen, onSheetOpenChange | boolean | Controlled or uncontrolled sheet |
container | HTMLElement | Portal container for the sheet |
Keyboard
| Key | Action |
|---|---|
Cmd/Ctrl+Enter | Send. On a hardware keyboard with a fine pointer, Enter sends too |
Escape in the field | Stop a streaming response |
Escape in the sheet | Close the sheet and return focus to the plus button |
Tab | Move through sources and tools in the sheet |
Accessibility
- The plus button has
aria-haspopup="dialog"andaria-expanded. The sheet is a modal dialog titled Add to message. - Each tool row is a
switchwitharia-checked; its label and description are the accessible name. - Every target in the sheet is at least 44 px, and the field uses 16 px type on touch so iOS never zooms in.
- Tool chips have a remove button named after the tool, for example Turn off Web search.
Motion
- The composer tracks the keyboard frame by frame with no transition of its own.
- The sheet slides up over 500 ms on the drawer curve and leaves in 375 ms. Drags follow the finger 1:1; a flick over 0.11 px/ms dismisses.
- The tool switch thumb slides over 200 ms. Under reduced motion the sheet fades in 150 ms and nothing slides.