<ChatComposerContainer><ChatComposer config={{namespace: 'customer-chat', onError: (e) => { throw e } }} placeholder="Chat text" ariaLabel="A basic chat composer" /><ChatComposerActionGroup><Button variant="secondary_icon" size="reset"><AttachIcon decorative={false} title="attach files to the message" /></Button><Button variant="primary_icon" size="reset"><SendIcon decorative={false} title="Send" /></Button></ChatComposerActionGroup></ChatComposerContainer>
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 and AI Chat Log components.
When referring to ChatComposer it is the rich text area only. You can use the ChatComposer component by itself, or use it within the ChatComposerContainer for consistent styling across chat features.
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-labelto describe the input. - If the surrounding UI includes additional help or error text, use
aria-describedbyto reference the associated element.
Chat Composer is built on top of the Lexical editor. Lexical is extensible and follows a declarative approach to configuration via JSX. Developers can leverage a
wide variety of existing plugins 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.
Chat Composer uses a custom AutoLinkPlugin internally
which you can see being configured here as a JSX child.
The Chat Composer component suite offers a variety of components designed to enhance and enrich the chat experience. Each element plays a crucial role in maintaining a consistent and cohesive styling, ensuring a seamless user interaction. The available components include:
- ChatComposerContainer: The primary container that houses the entire chat composer interface.
- ChatComposerActionGroup: A collection of buttons and controls, allowing users to perform various actions.
- ChatComposerAttachmentGroup: Groups multiple attachments together in responsive columns.
- ChatComposerAttachmentCard: A card-like component for showcasing attachment previews, making it easy for users to view details at a glance with the option to set the icon for the attachment.
- ChatComposerAttachmentDescription: Provides a description or additional information about an attachment, adding context for the user.
- ChatComposerAttachmentLink: Creates clickable links for attachments, facilitating easy access and interaction.
Set a placeholder value using a placeholder prop.
<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.
<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.
const MaxHeightExample = () => {return (<ChatComposermaxHeight="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
const RichTextExample = () => {return (<ChatComposerariaLabel="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/>)
For responsive attachment cards when using the Chat Composer component suite, use the columns prop.
const ResponsiveContainedAttachmentsExample = () => {const ExampleAttachment = () => (<ChatComposerAttachmentCard onDismiss={() => {}} attachmentIcon={<DownloadIcon decorative />}><ChatComposerAttachmentLink href="www.google.com">Document-FINAL.doc</ChatComposerAttachmentLink><ChatComposerAttachmentDescription>123 MB</ChatComposerAttachmentDescription></ChatComposerAttachmentCard>)return (<ChatComposerContainer><ChatComposerariaLabel="A chat with attachments"initialValue="This is my initial value"config={{namespace: "customer-chat",onError: (e) => {throw e;},}}/><ChatComposerActionGroup><Button variant="secondary_icon" size="reset"><AttachIcon decorative={false} title="attach files to the message" /></Button><Button variant="primary_icon" size="reset"><SendIcon decorative={false} title="Send" /></Button></ChatComposerActionGroup><ChatComposerAttachmentGroup columns={[1, 1, 2, 3]}>{Array.from({ length: 6 }).map((_, index) => (<ExampleAttachment key={index} />))}</ChatComposerAttachmentGroup></ChatComposerContainer>)}render(<ResponsiveContainedAttachmentsExample />)
The ChatComposerContainer component has 2 variants, default and contained.
const ContainedExample = () => {return (<ChatComposerContainer variant="contained"><ChatComposerariaLabel="A chat with attachments"initialValue="This is my initial value"config={{namespace: "customer-chat",onError: (e) => {throw e;},}}/><ChatComposerActionGroup><Button variant="secondary_icon" size="reset"><AttachIcon decorative={false} title="attach files to the message" /></Button><Button variant="primary_icon" size="reset"><SendIcon decorative={false} title="Send" /></Button></ChatComposerActionGroup></ChatComposerContainer>);}render(<ContainedExample />)
When the container is disabled, styling is applied to the container component. The disabled state is managed at the implementation level. If action buttons are included, their disabled state must also be managed by the developer.
const ContainedDisabledExample = () => {const [isDisabled, setIsDisabled] = React.useState(true);return (<><Box marginBottom="space50"><Checkbox checked={isDisabled} onClick={() => setIsDisabled((disabled) => !disabled)}>Disable Input</Checkbox></Box><ChatComposerContainer variant="contained"><ChatComposerariaLabel="A chat that is disabled"initialValue="This is my initial value"config={{namespace: "customer-chat",onError: (e) => {throw e;},}}disabled={isDisabled}/><ChatComposerActionGroup><Button variant="secondary_icon" size="reset" aria-disabled={isDisabled} disabled={isDisabled}><AttachIcon decorative={false} title="attach files to the message" /></Button><Button variant="primary_icon" size="reset" aria-disabled={isDisabled} disabled={isDisabled}><SendIcon decorative={false} title="Send" /></Button></ChatComposerActionGroup></ChatComposerContainer></>);}render(<ContainedDisabledExample />)
Use Chat Composer alongside other Paste components such as Chat Log to build more complex chat UI.
export const ChatComposerChatLogExample = () => {
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<HTMLDivElement>(null);
const scrollerRef = React.useRef<HTMLDivElement>(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): void => {
editorState.read(() => {
const text = $getRoot().getTextContent();
setMessage(text);
});
};
const submitMessage = (): void => {
if (message === "") return;
push(createNewMessage(message));
};
const editorInstanceRef = React.useRef<LexicalEditor>(null);
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"
columnGap="space30"
paddingX="space70"
paddingTop="space50"
>
<ChatComposerContainer>
<ChatComposer
maxHeight="size10"
config={{
namespace: "foo",
onError: (error) => {
throw error;
},
}}
ariaLabel="Message"
placeholder="Type here..."
onChange={handleComposerChange}
editorInstanceRef={editorInstanceRef}
>
<ClearEditorPlugin />
<EnterKeySubmitPlugin onKeyDown={submitMessage} />
</ChatComposer>
<ChatComposerActionGroup>
<Button variant="secondary_icon" size="reset">
<AttachIcon decorative={false} title="attach files to the message" />
</Button>
<Button
variant="primary_icon"
size="reset"
onClick={() => {
submitMessage();
editorInstanceRef.current?.dispatchCommand(CLEAR_EDITOR_COMMAND, undefined);
}}
>
<SendIcon decorative={false} title="Send" />
</Button>
</ChatComposerActionGroup>
</ChatComposerContainer>
</Box>
</Box>
);
}:Use Chat Composer alongside other Paste components such as AI Chat Log to build more complex chat UI. For the AI experience be sure to use the contained variant.
export const ChatComposerAIChatLogExample = () => {
const { aiChats, push } = useAIChatLogger(
{
variant: "user",
content: (
<AIChatMessage variant="user">
<AIChatMessageAuthor aria-label="you said at 2:36pm">Gibby Radki</AIChatMessageAuthor>
<AIChatMessageBody>Hi, I am getting errors codes when sending an SMS.</AIChatMessageBody>
</AIChatMessage>
),
},
{
variant: "bot",
content: (
<AIChatMessage variant="bot">
<AIChatMessageAuthor aria-label="AI said">Good Bot</AIChatMessageAuthor>
<AIChatMessageBody>
Error codes can be returned from various parts of the process. What error codes are you encountering?
<Box marginTop="space50">
<ButtonGroup>
<Button variant="secondary" onClick={() => {}} size="rounded_small">
30007
</Button>
<Button variant="secondary" onClick={() => {}} size="rounded_small">
30007
</Button>
<Button variant="secondary" onClick={() => {}} size="rounded_small">
30009
</Button>
</ButtonGroup>
</Box>
</AIChatMessageBody>
<AIChatMessageActionGroup>
<AIChatMessageActionCard aria-label="Feedback form">
Is this helpful?
<Button variant="reset" size="reset" aria-label="this is a helpful response">
<ThumbsUpIcon decorative={false} title="like result" />
</Button>
<Button variant="reset" size="reset" aria-label="this is not a helpful response">
<ThumbsDownIcon decorative={false} title="dislike result" />
</Button>
</AIChatMessageActionCard>
</AIChatMessageActionGroup>
</AIChatMessage>
),
}
);
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;
const scrollPosition: any = scrollerRef.current;
const scrollHeight: any = loggerRef.current;
scrollPosition?.scrollTo({ top: scrollHeight.scrollHeight, behavior: "smooth" });
}, [aiChats, mounted]);
const handleComposerChange = (editorState): void => {
editorState.read(() => {
const text = $getRoot().getTextContent();
setMessage(text);
});
};
const submitMessage = (): void => {
if (message === "") return;
push({
variant: "user",
content: (
<AIChatMessage variant="user">
<AIChatMessageAuthor aria-label="You said at 2:39pm">Gibby Radki</AIChatMessageAuthor>
<AIChatMessageBody>{message}</AIChatMessageBody>
</AIChatMessage>
),
});
};
const editorInstanceRef = React.useRef<LexicalEditor>(null);
return (
<Box>
<Box ref={scrollerRef} overflowX="hidden" overflowY="auto" maxHeight="size50" tabIndex={0}>
<AIChatLogger ref={loggerRef} aiChats={aiChats} />
</Box>
<ChatComposerContainer variant="contained">
<ChatComposer
maxHeight="size10"
config={{
namespace: "foo",
onError: (error) => {
throw error;
},
}}
ariaLabel="Message"
placeholder="Type here..."
onChange={handleComposerChange}
editorInstanceRef={editorInstanceRef}
>
<ClearEditorPlugin />
<EnterKeySubmitPlugin onKeyDown={submitMessage} />
</ChatComposer>
<ChatComposerActionGroup>
<Button variant="secondary_icon" size="reset">
<AttachIcon decorative={false} title="attach a file to your message" />
</Button>
<Button
variant="primary_icon"
size="reset"
onClick={() => {
submitMessage();
editorInstanceRef.current?.dispatchCommand(CLEAR_EDITOR_COMMAND, undefined);
}}
>
<SendIcon decorative={false} title="Send" />
</Button>
</ChatComposerActionGroup>
</ChatComposerContainer>
</Box>
);
};Use Chat Composer alongside other Paste components such as Chat Log to build more complex chat UI.
In the above example, we're using 2 Lexical plugins: ClearEditorPlugin that is provided by Lexical, and a custom plugin, 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 and submit button handler using
EnterKeySubmitPlugin
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. 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. 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 a plugin: EnterKeySubmitPlugin which dispatch the CLEAR_EDITOR_COMMAND, and clear the composer content as a result.
<ChatComposer onChange={handleComposerChange}>
<ClearEditorPlugin />
</ChatComposer>
To access the Lexical state out of the context we make use of the <EditorRedPlugin/> provided by the library. In order to use this you must create a ref to the LexicalEditor instance and pass it to the ChatComposer component.
export const ChatComposerImpl = () => {
const editorInstanceRef = React.useRef<LexicalEditor>(null);
return (
<ChatComposer
ariaLabel="Message"
placeholder="Type here..."
onChange={handleComposerChange}
editorInstanceRef={editorInstanceRef}
>
<ClearEditorPlugin />
</ChatComposer>
);
};
EnterKeySubmitPlugin is a custom plugin that submits a user message and clear the composer content when the enter key is pressed. They first must be passed to the ChatComposer as a child.
<ChatComposer onChange={handleComposerChange}>
<ClearEditorPlugin />
<EnterKeySubmitPlugin />
</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. It is recommended to avoid putting buttons in the Composer, instead use the container with ChatComposerActionGroup:
export const EnterKeySubmitPlugin = ({ onKeyDown }: { onKeyDown: () => void }): null => {
// get the editor from the composer context
const [editor] = useLexicalComposerContext();
const handleEnterKey = React.useCallback(
(event: KeyboardEvent) => {
const { shiftKey, ctrlKey } = event;
if (shiftKey || ctrlKey) return false;
event.preventDefault();
event.stopPropagation();
onKeyDown();
editor.dispatchCommand(CLEAR_EDITOR_COMMAND, undefined);
return true;
},
[editor, onKeyDown]
);
React.useEffect(() => {
// register the command to be dispatched when the enter key is pressed
return editor.registerCommand(KEY_ENTER_COMMAND, handleEnterKey, COMMAND_PRIORITY_HIGH);
}, [editor, handleEnterKey]);
return null;
};
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.