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

Chat Composer

Version 5.2.0GithubStorybookPeer review pending

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

Component preview theme
<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>

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

Accessibility

Accessibility page anchor

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.

ChatComposer component suite

ChatComposer component suite page anchor

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.

ChatComposer with placeholder

ChatComposer with placeholder page anchor

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" />

Chat Composer with initial value

Chat Composer with initial value page anchor

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" />

Chat Composer with max height

Chat Composer with max height page anchor

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 />)

Chat Composer with rich text

Chat Composer with rich text page anchor

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/>)

Chat Composer with responsive attachments

Chat Composer with responsive attachments page anchor

For responsive attachment cards when using the Chat Composer component suite, use the columns prop.

Component preview theme
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>
<ChatComposer
ariaLabel="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 />)

Chat Composer contained variant

Chat Composer contained variant page anchor

The ChatComposerContainer component has 2 variants, default and contained.

Component preview theme
const ContainedExample = () => {
return (
<ChatComposerContainer variant="contained">
<ChatComposer
ariaLabel="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 />)

Chat Composer disabled contained variant

Chat Composer disabled contained variant page anchor

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.

Component preview theme
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">
<ChatComposer
ariaLabel="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 />)

Chat Composer with Chat Log

Chat Composer with Chat Log page anchor

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>
  );
}:

Chat Composer with AI Chat Log

Chat Composer with AI Chat Log page anchor

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>
  );
};

Chat Composer with AI Chat Logger

Chat Composer with AI Chat Logger page anchor

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

Adding interactivity to ChatComposer with plugins

Adding interactivity to ChatComposer with plugins page anchor

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(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 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/>(link takes you to an external page) 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.