Message

The entries of a conversation, your own as bubbles and others at full width.

StableSince 1.0.0
On this page

Import

import { Message, MessageList } from '@envsemble/envui';

Usage

A MessageList holds the conversation, oldest first, and each Message in it takes from:

  • self for the person using the app, a compact bubble at the inline end; your own bubble grows to three quarters of the list, and to at least 16 rem in a narrow list.
  • other for anyone else, such as an assistant or another participant, full-width content with no bubble.
  • Never set from="self" on someone else's message to put it on a side.

author names who wrote it, meta holds supporting facts beside the author, such as a RelativeTime, and children the message: plain text keeps its line breaks, and formatted content, such as Markdown, fills the width of a message from someone else.

Conversation.tsx
import { useEffect, useRef, useState } from 'react';
import { Composer, Message, MessageList, Column } from '@envsemble/envui';

interface Entry {
  id: number;
  from: 'self' | 'other';
  text: string;
}

const answer =
  'Noted. I added it to the launch checklist and will remind Sam Lee on Friday, before the review.';

export default function Conversation() {
  const [messages, setMessages] = useState<Entry[]>([
    { id: 1, from: 'self', text: 'What is still open for the launch?' },
    {
      id: 2,
      from: 'other',
      text:
        'Three items: the pricing page, the press kit and the support rota. ' +
        'Sam Lee owns the first two.',
    },
  ]);
  const [replying, setReplying] = useState(false);
  const timer = useRef<number>(undefined);
  useEffect(() => () => window.clearInterval(timer.current), []);

  function stop() {
    window.clearInterval(timer.current);
    setReplying(false);
  }

  function send(text: string) {
    const id = Date.now();
    const words = answer.split(' ');
    let shown = 1;
    setMessages((list) => [
      ...list,
      { id, from: 'self', text },
      { id: id + 1, from: 'other', text: words[0] },
    ]);
    setReplying(true);
    // A timer stands in for a reply that streams in word by word.
    timer.current = window.setInterval(() => {
      shown += 1;
      const reply = words.slice(0, shown).join(' ');
      setMessages((list) =>
        list.map((entry) => (entry.id === id + 1 ? { ...entry, text: reply } : entry)),
      );
      if (shown === words.length) stop();
    }, 120);
  }

  return (
    <Column gap={5}>
      <MessageList aria-label="Conversation with the assistant" busy={replying}>
        {messages.map((entry) => (
          <Message
            key={entry.id}
            from={entry.from}
            author={entry.from === 'other' ? 'Assistant' : undefined}
          >
            {entry.text}
          </Message>
        ))}
      </MessageList>
      <Composer
        aria-label="Message the assistant"
        placeholder="Ask anything"
        onSend={send}
        running={replying}
        onStop={stop}
      />
    </Column>
  );
}

Actions and status

actions holds small actions below the message, such as Copy or Try again: buttons without a variant or size of their own are small ghost buttons. status is sending, with a spinner and Sending, or failed, with an error icon and Not sent; statusLabel replaces the words.

<Message
  from="self"
  meta="09:41"
  status={status}
  actions={
    status === 'failed' && (
      <Button variant="text" onClick={retry}>
        Try again
      </Button>
    )
  }
>
  {`Northwind Hall
12 Harbour Street`}
</Message>

Streaming

busy on MessageList shows a busy row after the last message, where the reply will show: busyAuthor, who the reply comes from, as a message's author, then a quiet spinner and Working in the muted color (busyLabel, or messages.message.working). It reports aria-busy while a reply is on its way or streams in, until it is cleared. The list doesn't scroll by itself: keep the newest message in view yourself, without moving focus.

<MessageList aria-label="Conversation with Sam Lee" busy={streaming} busyAuthor="Assistant">
  …
</MessageList>

Composition

skeleton shows a message as its own skeleton while it loads. Message and MessageList can render in React Server Components.

Customize

The list is env-message-list, with env-message-list-busy for its busy row, and each message env-message, with its author, env-message-author, and meta, env-message-meta, in its header, then its content, env-message-content, its actions, env-message-actions, and its status, env-message-status.

Accessibility

You provide

  • A name for the list, such as Conversation with Sam Lee.
  • busy while a reply streams in.
  • Names for icon-only actions, and Try again for a failed message.
  • Scrolling, without moving focus.

envUI provides

  • MessageList as a polite log, and aria-busy while busy, with the busy row shown but not read.
  • Each message as an <article>, with its author and meta in a header.
  • The status as text beside its icon.

Props

Message

One entry of a conversation, such as a chat message or a comment, as an article: a compact bubble at the inline end for your own messages, full-width content for others', with an optional author, meta, status and actions.

PropTypeDescription
fromrequired
'self''other'
self: written by the person using the app, a compact bubble at the inline end. other: from anyone else, such as an assistant or another participant, full-width content without a bubble.
author
ReactNode
Who wrote the message, such as a name, shown above it at weight 500. Leave it out where the conversation makes it clear, such as on your own messages in a conversation with one person.
meta
ReactNode
Supporting facts beside the author, in the muted color, such as the time or a RelativeTime.
actions
ReactNode
Small actions below the message, such as Copy or Try again: buttons without a variant or size of their own are ghost buttons at the small control height, with the touch height on touch screens; at the inline end for your own messages. A text IconButton at the edge lines its icon up with the message. false and null count as absent.
status
'sending''failed'
A message on its way, sending, with a spinner and Sending; or one that could not be sent, failed, with an error icon and Not sent in the danger tone and, on your own message, a red outline around the bubble. Without it the message is sent.
statusLabelDefault 'Sending' or 'Not sent'
string
The text beside the status icon, such as "Could not deliver". It overrides EnvUIProvider's `messages.message.sending` and `.failed`.
childrenrequired
ReactNode
The message. Plain text keeps its line breaks; formatted content, such as Markdown, fills the width of a message from someone else.
skeletonDefault false
boolean
Shows it and its content as a busy skeleton while that loads; turning it on or off renders the content anew.

Also accepts the native props of an HTML element, such as ref, className and aria-* attributes.

MessageList

The messages of a conversation in order, as a log: assistive technology politely announces messages added anywhere in it and text that changes. Set busy while a reply is on its way or streams in. Name it with aria-label or aria-labelledby.

PropTypeDescription
busyDefault false
boolean
A reply is on its way or streaming in: after the last message, where the reply will show, a row with the busy author, a quiet spinner and Working in the muted color; and the log reports aria-busy, so assistive technology can wait for the reply to finish instead of announcing every step.
busyAuthor
ReactNode
Who the reply on its way comes from, such as Agent, named on the busy row as a message's author.
busyLabelDefault 'Working'
string
The text beside the busy spinner, such as "Thinking". It overrides EnvUIProvider's `messages.message.working`.

Also accepts the native props of <div>, such as ref, className and aria-* attributes.

Part names

Each part's class and state attributes are public and versioned, for an application's CSS to style.

Message

env-message
One message in a conversation.
env-message-actions
The message's actions.
env-message-author
The author's name.
env-message-content
The message's content.
env-message-header
The row with the author and the meta.
env-message-meta
The time or another fact about the message.
env-message-status
The sending or failed status.
data-env-skeleton
Any value, or present on env-message, env-message-author, env-message-header, env-message-meta
data-from
other · self
data-status
failed · sending
data-text
Present on env-message-content

MessageList

env-message-list
The list of messages.
env-message-list-busy
The row after the last message while busy: the busy author, a spinner and Working.

Strings

EnvUIProvider's messages overrides each, and a component's own label prop wins over it.

messages.message.sending
"Sending" The status of a message on its way.
messages.message.failed
"Not sent" The status of a message that could not be sent.
messages.message.working
"Working" Beside the spinner of a MessageList while a reply is on its way.

Changelog

1.0.0

Buttons in actions are small ghost buttons unless they set their own variant or size, and the statuses come from messages.message. The page is message, after the component, instead of messages. Message and MessageList can render in React Server Components.

1.0.0-prerelease.6

The busy row names who replies, busyAuthor, and says Working beside its spinner, busyLabel or messages.message.working.

While busy, MessageList shows a quiet spinner in the muted color after the last message, env-message-list-busy.