Skip to contentSkip to navigationSkip to topbar
Paste assistant Assistant
Figma
Star

Chat Composer

Version 5.1.1GithubStorybookPeer review pending

A Chat Composer is an input made for users to type rich chat messages.

Component preview theme
<ChatComposer config={{namespace: 'customer-chat', onError: (e) => { throw e } }} placeholder="Chat text" ariaLabel="A basic chat composer" />

Guidelines

Guidelines page anchor

About Chat Composer

About Chat Composer page anchor

A Chat Composer is an input made for users to type rich chat messages. Chat Composer is best used as one part of larger chat user interface to provide a seamless authoring experience. Within the context of Paste, Chat Composer is most typically used alongside the Chat Log component.

Chat Composer supports a variety of aria attributes which are passed into the content editable region of the component.

  • If the surrounding UI includes a screen reader visible label reference the label element using aria-labelledby.
  • If the surrounding UI does not include a screen reader visible label, use aria-label to describe the input.
  • If the surrounding UI includes additional help or error text use aria-describedby to reference the associated element.

Chat Composer is built on top of the Lexical(link takes you to an external page) editor. Lexical is extensible and follows a declarative approach to configuration via JSX. Developers can leverage a wide variety of existing plugins(link takes you to an external page) via the @twilio-paste/lexical-library package or other sources. Alternatively, developers can write their own custom plugin logic. Plugins are provided to the Chat Composer via the children prop.

Auto Link Plugin page anchor

Chat Composer uses a custom AutoLinkPlugin(link takes you to an external page) internally which you can see being configured here(link takes you to an external page) as a JSX child.

Set a placeholder value using a placeholder prop.

Component preview theme
<ChatComposer config={{namespace: 'customer-chat', onError: (e) => { throw e } }} placeholder="Chat text" ariaLabel="A placeholder chat composer" />

Set an initial value using an initialValue prop. This prop is limited to providing single line strings. For more complicated initial values interact with the Lexical API directly using the config prop and editorState callback.

Component preview theme
<ChatComposer config={{namespace: 'customer-chat', onError: (e) => { throw e } }} initialValue="This is my initial value" ariaLabel="An initial value chat composer" />

Restrict the height of the composer using a maxHeight prop.

Component preview theme
const MaxHeightExample = () => {
return (
<ChatComposer
maxHeight="size10"
ariaLabel="A max height chat composer"
config={{
namespace: 'customer-chat',
onError (e) { throw e },
editorState () {
const root = $getRoot();
if (root.getFirstChild() !== null) return;
for (let i = 0; i < 10; i++) {
root.append(
$createParagraphNode().append(
$createTextNode('this is a really really long initial value')
)
);
}
},
}}
/>
)
}
render(<MaxHeightExample />)

Set a rich text value using one of the Lexical formatting APIs such as toggleFormat(link takes you to an external page)

Component preview theme
const RichTextExample = () => {
return (
<ChatComposer
ariaLabel="A rich text chat composer"
config={{
namespace: 'customer-chat',
onError (e) { throw e },
editorState () {
const root = $getRoot();
if (root.getFirstChild() !== null) return;
root.append(
$createParagraphNode().append(
$createTextNode('Hello '),
$createTextNode('world! ').toggleFormat('bold'),
$createTextNode('This is a '),
$createTextNode('chat composer ').toggleFormat('italic'),
$createTextNode('with rich text functionality.')
)
);
},
}}
/>
)
}
render(<RichTextExample/>)

Use Chat Composer alongside other Paste components such as Chat Log to build more complex chat UI.

Component preview theme
const ChatDialog = () => {
const {chats, push} = useChatLogger(
{
content: (
<ChatBookend>
<ChatBookendItem>Today</ChatBookendItem>
<ChatBookendItem>
<strong>Chat Started</strong>・3:34 PM
</ChatBookendItem>
</ChatBookend>
),
},
{
variant: 'inbound',
content: (
<ChatMessage variant="inbound">
<ChatBubble>Quisque ullamcorper ipsum vitae lorem euismod sodales.</ChatBubble>
<ChatBubble>
<ChatAttachment attachmentIcon={<DownloadIcon color="colorTextIcon" decorative />}>
<ChatAttachmentLink href="www.google.com">Document-FINAL.doc</ChatAttachmentLink>
<ChatAttachmentDescription>123 MB</ChatAttachmentDescription>
</ChatAttachment>
</ChatBubble>
<ChatMessageMeta aria-label="said by Gibby Radki at 5:04pm">
<ChatMessageMetaItem>Gibby Radki ・ 5:04 PM</ChatMessageMetaItem>
</ChatMessageMeta>
</ChatMessage>
),
},
{
content: (
<ChatEvent>
<strong>Lauren Gardner</strong> has joined the chat ・ 4:26 PM
</ChatEvent>
),
},
{
variant: 'inbound',
content: (
<ChatMessage variant="inbound">
<ChatBubble>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</ChatBubble>
<ChatMessageMeta aria-label="said by Lauren Gardner at 4:30pm">
<ChatMessageMetaItem>
<Avatar name="Lauren Gardner" size="sizeIcon20" />
Lauren Gardner ・ 4:30 PM
</ChatMessageMetaItem>
</ChatMessageMeta>
</ChatMessage>
),
}
);
const [message, setMessage] = React.useState('');
const [mounted, setMounted] = React.useState(false);
const loggerRef = React.useRef(null);
const scrollerRef = React.useRef(null);
React.useEffect(() => {
setMounted(true);
}, []);
React.useEffect(() => {
if (!mounted || !loggerRef.current) return;
scrollerRef.current?.scrollTo({top: loggerRef.current.scrollHeight, behavior: 'smooth'});
}, [chats, mounted]);
const handleComposerChange = (editorState) => {
editorState.read(() => {
const text = $getRoot().getTextContent();
setMessage(text);
});
};
const submitMessage = () => {
if (message === '') return;
push(createNewMessage(message));
};
return (
<Box>
<Box ref={scrollerRef} overflowX="hidden" overflowY="auto" maxHeight="size50" tabIndex={0}>
<ChatLogger ref={loggerRef} chats={chats} />
</Box>
<Box
borderStyle="solid"
borderWidth="borderWidth0"
borderTopWidth="borderWidth10"
borderColor="colorBorderWeak"
display="flex"
flexDirection="row"
columnGap="space30"
paddingX="space70"
paddingTop="space50"
>
<ChatComposer
maxHeight="size10"
config={{
namespace: 'foo',
onError: (error) => {
throw error;
},
}}
ariaLabel="Message"
placeholder="Type here..."
onChange={handleComposerChange}
>
<ClearEditorPlugin />
<SendButtonPlugin onClick={submitMessage} />
<EnterKeySubmitPlugin onKeyDown={submitMessage} />
</ChatComposer>
</Box>
</Box>
);
};
render(<ChatDialog />)

Adding interactivity with plugins

Adding interactivity with plugins page anchor

In the above example, we're using 3 Lexical plugins: ClearEditorPlugin that is provided by Lexical, and 2 custom plugins, SendButtonPlugin and EnterKeySubmitPlugin. We also keep track of the content provided to the composer via the onChange handler. Together we can add custom interactivity such as:

  • Clear the editor on button click using ClearEditorPlugin
  • Submit on enter key press using EnterKeySubmitPlugin
  • Submit on button click using SendButtonPlugin

Plugins are functions that must be children of the ChatComposer component, so that they can access the Composer context.

onChange event handler

The onChange handler provided to the ChatComposer takes 3 arguments, the first of which is the editorState(link takes you to an external page). This allows us to read the current content of the editor using the utilities provided by Lexical.

$getRoot is a utility to access the composer root ElementNode(link takes you to an external page). We can then get the text content of the editor everytime it is updated, and store it in our component state for later use.

const handleComposerChange = (editorState: EditorState): void => {
  editorState.read(() => {
    const text = $getRoot().getTextContent();
    setMessage(text);
  });
};

ClearEditorPlugin

The ClearEditorPlugin supplied by Lexical allows you to build functionality into the composer that will clear the composer content when a certain action is performed.

When passed as a child to ChatComposer, it will automatically register a CLEAR_EDITOR_COMMAND. You can then dispatch this command from elsewhere to clear the composer content. In the example, we created 2 plugins: SendButtonPlugin and EnterKeySubmitPlugin which both dispatch the CLEAR_EDITOR_COMMAND, and clear the composer content as a result.

<ChatComposer onChange={handleComposerChange}>
  <ClearEditorPlugin />
</ChatComposer>

SendButtonPlugin and EnterKeySubmitPlugin are custom plugins that submit a user message and clear the composer content. They first must be passed to the ChatComposer as a child.

<ChatComposer onChange={handleComposerChange}>
  <ClearEditorPlugin />
  <EnterKeySubmitPlugin />
  <SendButtonPlugin />
</ChatComposer>

Once "registered" as children of ChatComposer, the plugins gain access to the composer context and can dispatch commands. They can also return JSX to be rendered into the composer. Take the SendButtonPlugin as an example:

export const SendButtonPlugin = ({onClick}: {onClick: () => void}): JSX.Element => {
  // get the editor from the composer context
  const [editor] = useLexicalComposerContext();

  // an event handler called from custom UI can the interact with the editor to perform certain actions
  const handleSend = (): void => {
    onClick();
    editor.dispatchCommand(CLEAR_EDITOR_COMMAND, undefined);
  };

  return (
    <Box position="absolute" top="space30" right="space30">
      <Button variant="primary_icon" size="reset" onClick={handleSend}>
        <SendIcon decorative={false} title="Send message" />
      </Button>
    </Box>
  );
};

Here we're rendering a button that when clicked can call a callback function, and dispatch the CLEAR_EDITOR_COMMAND for the ClearEditorPlugin respond to. We use it to add a new chat message in the chat log, and then clear the composer ready for the next message to be typed.