Skip to the page
Tactile UI

Kit part · AI chats

Message

One chat message as a plate, with no chat around it, and made the way the chat is made. What you wrote is typed into a recess and sits to the right; what the assistant said is printed on a slip of paper torn along a perforation; what someone else wrote is a small raised plate. Along the top run the sender, printed as a legend, and the time; a lamp and a word say what has become of your message (sending, sent or not sent). A message that did not go grows a Retry key. Use it in a thread you build yourself, in a notification or in a log of what was said.

Element
<article class="tui-message" data-from="user">
Needs
kit.css + kit.mjs
Rules
message.rules.md · 6
Finishes
Aluminium · Graphite · Oak · Walnut · Porcelain · Black · Daylight · Night · Paper · Phosphor · E-paper · AMOLED
Volumes
Tactile · Quiet
On this page
  1. Variants
  2. Every state, at once
  3. API
  4. Markup
  5. Accessibility
  6. How it’s built
  7. Rules
  8. Related parts

Seats open in 11 days. Want one email that morning?

Variants

YouSent

Is Studio A free from 15:00 today?

Yours · sent
YouSending

Is Studio A free from 15:00 today?

Yours · sending, the lamp blinking
YouNot sent

Is Studio A free from 15:00 today?

Yours · not sent, with Retry
Studio assistant

Studio A is free from 15:00 to 18:00 today. Room 2 is held for Tom until 16:30.

The assistant’s · a torn slip
Tom Hale

I can take Room 2 after 16:30 if that helps.

Someone else’s · a plate
Studio assistant

Studio A is free from 15:00 to 18:00.

Room 2 is held for Tom until 16:30, then free. I have not booked anything.

Several paragraphs
You

Is Studio A free from 15:00 today?

No delivery to speak of · no data-state

Every state, at once

Both finishes, side by side. Hover, pressed and focus are forced with .is-hover, .is-down and .is-focus; checked and disabled are real.

Aluminium

PartSendingSentNot sentRetry focused
Yours
YouSending

Is Studio A free from 15:00 today?

YouSent

Is Studio A free from 15:00 today?

YouNot sent

Is Studio A free from 15:00 today?

YouNot sent

Is Studio A free from 15:00 today?

The assistant’s
Studio assistant

Studio A is free from 15:00 to 18:00 today. Room 2 is held for Tom until 16:30.

Tom Hale

I can take Room 2 after 16:30 if that helps.

Only your own messages have a delivery state
Same as yours

Graphite

PartSendingSentNot sentRetry focused
Yours
YouSending

Is Studio A free from 15:00 today?

YouSent

Is Studio A free from 15:00 today?

YouNot sent

Is Studio A free from 15:00 today?

YouNot sent

Is Studio A free from 15:00 today?

The assistant’s
Studio assistant

Studio A is free from 15:00 to 18:00 today. Room 2 is held for Tom until 16:30.

Tom Hale

I can take Room 2 after 16:30 if that helps.

Only your own messages have a delivery state
Same as yours

API

NameKindValuesWhat it does
article.tui-message[data-from][aria-labelledby]elementuser · assistant · otherThe message. Yours is a recess to the right, the assistant’s a torn slip, anyone else’s a raised plate. Named by its sender.
[data-state]attributesending · sent · failedWhat became of it. The lamp follows it in CSS. Leave it off for a message with no delivery to report.
.tui-message__meta > .tui-message__whoelementthe senderA printed legend, which is also the message’s name.
.tui-message__state[role="status"]elementlamp and wordsA polite status region: a lit lamp (hidden from a screen reader) and the words “Sending”, “Sent” or “Not sent”.
time.tui-message__time[datetime]elementthe timeA real time element with a machine-readable datetime.
.tui-message__bodyelementany textThe words. Paragraphs, line breaks kept, long words broken so nothing overflows.
button.tui-message__retry[data-message-retry]elementshown when failedRetry. Pressing it sets the message back to sending and fires tui-message-retry; focus stays with the message.
setMessageState(message, state)functionneeds kit.mjsSets data-state and the words, and fires tui-message-state. null removes the state.
messageStateText(state)functionpureThe words for a state.

Markup

<!-- data-from: user · assistant · other. data-state: sending · sent · failed (leave it off when delivery is not in question). -->
<article class="tui-message" data-from="user" data-state="failed" aria-labelledby="m1-who">
  <header class="tui-message__meta">
    <span class="tui-label tui-message__who" id="m1-who">You</span>
    <span class="tui-message__state" role="status">
      <span class="tui-lamp" data-on aria-hidden="true"></span><span class="tui-message__words">Not sent</span>
    </span>
    <time class="tui-message__time" datetime="2026-10-07T14:02">14:02</time>
  </header>
  <div class="tui-message__body"><p>Is Studio A free from 15:00 today?</p></div>
  <button class="tui-key tui-key--flat tui-key--sm tui-message__retry" type="button" data-message-retry>Retry</button>
</article>
import { setMessageState } from '/kit/kit.mjs';

setMessageState(message, 'sending');          // the lamp follows data-state; the words change with it
document.addEventListener('tui-message-retry', async (e) => {
  try { await resend(e.detail.message); setMessageState(e.detail.message, 'sent'); }
  catch { setMessageState(e.detail.message, 'failed'); }
});

Needs kit.css and kit.mjs. How to install the kit.

Accessibility

  • The message is an <article> named by its sender. The sender, the time, the state and the text are all real text, read in that order; the time is a <time> with a datetime.
  • The state is a polite role="status" region, so changing its words says them aloud without moving focus. The lamp is hidden from a screen reader and always sits beside the word that says the same.
  • A failure is also bold and says “Not sent”, so it is never a red lamp alone, and Retry appears as a real key in the reading order straight after the text. The words on the paper are in the paper’s own ink, above 6:1.
  • When Retry is pressed with the keyboard, the key goes away, so the focus is moved to the message, which takes a visible 2px ring. The blink stops under reduced motion and the lamp stays lit.
  • The key is 44px tall on a phone and the plate never scrolls sideways: long words break inside it.

How it’s built

The recess is the one the chat uses for what you type, and the slip is the chat’s: a paper layer cut by a mask with small bites along the top and teeth at the foot, lifted by a shadow filter that follows the torn edge. It is made lighter here, with the perforation as two rows of gradients and no fibres, so a page can hold many. In the flat finishes the paper’s edge is drawn as a line, as the chat’s is.

Rules

message.rules.mdIn the brain

A single message with its sender, its time and what became of it, in the same hand as the chat: recessed for you, a torn slip for the assistant, a plate for anyone else.

6 rules in this file, with the foundations they rest on and the anti-patterns that break them. Full rules ship in the brain with the Pass.

Chat

An AI chat as a teleprinter, with restraint: what you type sits in a recess; the answer prints on a perforated slip with an amber lamp at the print head; code comes up on display glass. Composer with attachments and a send key that becomes Stop.

Saving… Saved 2 min agoOffline

Sync status

A small legend that says whether your work is safe: Saving, Saved or Offline, with a lamp that matches (amber and blinking while it saves, green when it has, red when it cannot). The words are the state and the lamp only agrees with them. It is a polite status region, so changing its words is what a screen reader hears.

AOTRMH+4

Avatar

A person’s mark, struck like a coin: initials stamped into the field, or a photo set under the rim, with a status lamp mounted at five o’clock. Stacks into a group with a count.