Skip to content
ShellComposer mobileMenu

Composer mobile

A bottom-anchored phone composer that rides the on-screen keyboard, with attach sources and tools in a bottom sheet.

A phone chat. Show the keyboard and the composer rides above it, tracking every frame.

Installation

$ pnpm dlx shadcn@latest add https://chatcn.dev/r/composer-mobile.json

Usage

tsx
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.

A phone chat. Show the keyboard and the composer rides above it, tracking every frame.

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.

Attach from the camera, photos or files, or turn on a tool for the next message.

Props

PropTypeNotes
value, defaultValue, onValueChangestringControlled or uncontrolled text
onSubmit({ text, tools }) => voidSend button or Cmd/Ctrl+Enter. Enter adds a line on touch screens
status"idle" | "submitting" | "streaming"Streaming turns Send into Stop
onStop() => voidStop button, or Escape in the field
disabledboolean
placeholderstringDefault Message…. An active tool's placeholder wins
sourcesComposerMobileSource[]Camera, Photos and Files by default. [] hides the row
toolsComposerMobileTool[]Web search, Deep research, Image and Canvas by default
activeTools, defaultActiveTools, onToolsChangestring[]Ids of the tools that are on
onToolSelect(id, active) => voidOne tool turned on or off
onAttach(sourceId) => voidOpen the camera, photo library or file picker here
headerReactNodeAttachments or chips above the field
position"sticky" | "fixed" | "absolute"Default sticky
keyboardInsetnumberOverrides the measured keyboard height, for demos and tests
sheetOpen, defaultSheetOpen, onSheetOpenChangebooleanControlled or uncontrolled sheet
containerHTMLElementPortal container for the sheet

Keyboard

KeyAction
Cmd/Ctrl+EnterSend. On a hardware keyboard with a fine pointer, Enter sends too
Escape in the fieldStop a streaming response
Escape in the sheetClose the sheet and return focus to the plus button
TabMove through sources and tools in the sheet

Accessibility

  • The plus button has aria-haspopup="dialog" and aria-expanded. The sheet is a modal dialog titled Add to message.
  • Each tool row is a switch with aria-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.