Navigation

v0.0.4
Content

Message

One turn of a conversation, as a bubble that points at its author.

On this page

A message is one turn of a conversation. A received message is a raised sheet at the start of the row; a sent one is ink at the end, the way a primary button is ink. A bubble has a surface's corners, except the one that points at its author, which is tight. A discussion in which any comment can be answered is a thread.

Message

<ol class="max-w(440) mx-auto m(0) p(0)">  <li class="message">    <span class="avatar avatar-sm message-avatar">      <img class="avatar-image" src="/demo/portrait-1.svg" alt="Ada">    </span>    <div class="message-bubble">Did the build for the docs site finish?</div>  </li>  <li class="message message-sent">    <div class="message-bubble">Yes, a minute ago. 71 pages, no warnings.</div>  </li>  <li class="message message-sent">    <div class="message-bubble">I also moved the previews to static pages, so the next build takes about forty seconds on a cold cache.</div>    <span class="message-meta">Delivered · 14:02</span>  </li>  <li class="message">    <span class="avatar avatar-sm message-avatar">      <img class="avatar-image" src="/demo/portrait-1.svg" alt="Ada">    </span>    <div class="message-bubble">Perfect. Ship it.</div>    <span class="message-meta">Ada · 14:03</span>  </li></ol>

The thread is any list. Use an <ol>, since the order is the content, and clear its margin and padding with m(0) p(0); the turns bring their own rhythm. A turn stands 12px from the one before it and 4px when both come from the same side, so a change of author reads as a break. A bubble takes most of a narrow thread and never more than a comfortable measure of a wide one.

#Without avatars

In a conversation between two people the side already says who is speaking. Leave the avatar out and the bubble starts the row.

Without avatars

<ol class="max-w(360) mx-auto m(0) p(0)">  <li class="message">    <div class="message-bubble">Are we still on for Thursday?</div>  </li>  <li class="message">    <div class="message-bubble">I can do any time after two.</div>    <span class="message-meta">09:41</span>  </li>  <li class="message message-sent">    <div class="message-bubble">Thursday at three.</div>    <span class="message-meta">Read · 09:44</span>  </li></ol>

#Typing

message-typing is three dots that rise in turn, inside a bubble. The dots mean nothing to assistive technology, so the bubble carries the words: role="status" and a label such as "Ada is typing".

Typing

<ol class="max-w(360) mx-auto m(0) p(0)">  <li class="message message-sent">    <div class="message-bubble">Can you send the notes from the review?</div>  </li>  <li class="message">    <span class="avatar avatar-sm message-avatar">      <img class="avatar-image" src="/demo/portrait-1.svg" alt="Ada">    </span>    <div class="message-bubble" role="status" aria-label="Ada is typing">      <span class="message-typing"></span>    </div>  </li></ol>

#Accessibility

  • Use an <ol> for the thread. Put new turns in an aria-live="polite" region, or make the list one with role="log", so arriving messages are announced without stealing focus.
  • Color and side are not the only signals of authorship for a screen reader: name the author in each turn, visibly in message-meta or in a visually hidden prefix.
  • The avatar's alt is the author's name when no name is written in the turn; when one is, leave it empty.
  • With reduced motion the typing dots rest. A sent bubble is ink on moonstone and pearl on obsidian, at full text contrast in both.

#Classes

ClassDescription
messageA received turn, at the start of the row. It stands 12px from the turn before it and 4px when both come from the same side
message-sentA turn of your own: the row mirrors and the bubble is ink. With message
message-avatarThe author's avatar, level with the foot of the bubble; it goes on the avatar itself and may be left out
message-bubbleThe words, in a bubble whose tight corner points at the author
message-metaA name, a time or a receipt under the bubble
message-typingThree dots that rise in turn, inside a bubble that carries role="status" and a label such as "Ada is typing"

#Custom properties

PropertyDefaultDescription
--message-radius1.125remA bubble's corner
--message-tail.375remThe corner that points at the author

#Source

@master/ui/components/message.css
/* * Message: one turn of a conversation. * A received message is a raised sheet at the start of the row; a sent one is ink at the end, the way a * primary button is ink. A bubble's corners are those of a surface, except the one that points at its * author, which is tight. A bubble takes most of a narrow thread and no more than a comfortable measure of * a wide one. The thread itself is any list: turns keep their own distance from each other. */@layer components {  .message {    --message-radius: 1.125rem;    --message-tail: .375rem;    --avatar-size: 1.75rem;    display: grid;    grid-template-columns: auto minmax(0, 1fr);    align-items: end;    justify-items: start;    gap: .25rem .5rem;    min-width: 0;    /* Sent: the row mirrors, and the bubble is ink. */    &.message-sent {      grid-template-columns: minmax(0, 1fr) auto;      justify-items: end;      .message-bubble {        --surface-color: var(--color-surface-inverse);        @apply --on-ink;        border-end-start-radius: calc(var(--message-radius) * var(--corner-scale));        border-end-end-radius: calc(var(--message-tail) * var(--corner-scale));        @apply --surface-ink;        color: var(--color-text-inverse);        box-shadow:          inset 0 .5px 0 var(--color-ink-highlight),          0 0 0 .5px var(--color-ink-edge),          0 1px 2px -1px var(--color-shade-1), 0 2px 4px -2px var(--color-shade-2);      }      .message-meta {        text-align: end;      }    }    /*     * The rhythm of a thread belongs to its turns, so any list or column can hold them: a turn stands     * 12px from the one before it, and 4px when both come from the same side.     */    & + & {      margin-top: .75rem;    }    &:not(.message-sent) + &:not(.message-sent), &.message-sent + &.message-sent {      margin-top: .25rem;    }  }  /* The author's avatar, level with the foot of the bubble. Leave it out and the bubble starts the row. */  .message-avatar {    grid-row: 1;    align-self: end;  }  .message-bubble {    grid-column: 2;    grid-row: 1;    min-width: 0;    max-width: min(34rem, 86%);    padding: .5rem .75rem;    border-radius: calc(var(--message-radius) * var(--corner-scale));    border-end-start-radius: calc(var(--message-tail) * var(--corner-scale));    corner-shape: var(--corner-shape);    @apply --surface-sheet;    color: var(--color-text-strong);    box-shadow: var(--shadow-raised);    font-size: .875rem;    line-height: 1.5;    overflow-wrap: anywhere;    text-wrap: pretty;  }  .message-sent .message-bubble {    grid-column: 1;  }  /* Without an avatar the bubble takes the first column of a received row. */  .message:not(.message-sent, :has(.message-avatar)) .message-bubble {    grid-column: 1 / -1;  }  .message:not(.message-sent, :has(.message-avatar)) .message-meta {    grid-column: 1 / -1;  }  /* Under the bubble: a name, a time, a receipt. */  .message-meta {    grid-column: 2;    padding-inline: .25rem;    color: var(--color-text-muted);    font-size: .6875rem;    line-height: 1.4;    font-variant-numeric: tabular-nums;  }  .message-sent .message-meta {    grid-column: 1;  }  /*   * Typing: three dots that rise in turn, inside a bubble. Name it for assistive technology on the bubble   * ("Mira is typing"). Without motion the dots rest.   */  .message-typing {    display: block;    width: 1.75rem;    height: 1.3125rem;    background:      radial-gradient(circle closest-side, currentColor calc(100% - .5px), transparent) 0 50% / 6px 6px no-repeat,      radial-gradient(circle closest-side, currentColor calc(100% - .5px), transparent) 50% 50% / 6px 6px no-repeat,      radial-gradient(circle closest-side, currentColor calc(100% - .5px), transparent) 100% 50% / 6px 6px no-repeat;    opacity: .5;    animation: message-typing 1.2s var(--easing-glide) infinite;    @media (prefers-reduced-motion: reduce) {      animation: none;    }  }  @media (forced-colors: active) {    .message-bubble {      outline: 1px solid CanvasText;      outline-offset: -1px;    }  }}@keyframes message-typing {  0%, 60%, 100% {    background-position: 0 50%, 50% 50%, 100% 50%;  }  15% {    background-position: 0 22%, 50% 50%, 100% 50%;  }  30% {    background-position: 0 50%, 50% 22%, 100% 50%;  }  45% {    background-position: 0 50%, 50% 50%, 100% 22%;  }}