Skip to the page
TACTILE UI

Kit part · AI chats

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.

Element
<tui-chat>
Needs
kit.css + kit.mjs
Rules
chat.rules.md · 5
Finishes
Aluminium · Graphite · Porcelain · Black
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
01

Variants

Ready

Ask about rooms, sessions and bookings. Answers print here.

Empty · suggestions to start

Assistant · printing

Studio A is free from 15:00 to 18:00. Room 2 is held

A slip printing
Shell
node kit/build.mjs
Code on glass
02

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

PartRestFocusPrintingDisabled
Send key
Composer
Slip

Assistant · 14:18

Studio A is free.

—

Assistant · printing

Studio A is

—

Graphite

PartRestFocusPrintingDisabled
Send key
Composer
Slip

Assistant · 14:18

Studio A is free.

—

Assistant · printing

Studio A is

—
03

API

NameKindValuesWhat it does
labelattributerequiredWho answers. Names the log (“Conversation with Studio assistant”), the text area and each slip.
placeholderattributean example questionThe composer’s placeholder: an example, never the label.
demoattribute—Print a sample answer when nobody cancels tui-chat-send. For specimens and static pages.
.tui-chat__emptyclasschildThe empty state, shown until the first message: an unlit lamp, one sentence, suggestions.
[data-suggest]attributeon a keySends its own words as a message.
.tui-chat__msg[data-from]class"user" · "assistant"A message. aria-label “Name, time” becomes the printed stamp on a slip.
pre[data-label]elementin an answerBecomes a code block on glass, with its label and a copy key.
say(text, files)method—Add your own message to the thread.
reply()method→ { write, code, end, stop }Start a slip. write(text) prints (a blank line starts a paragraph), code(text, label) adds a block, end() stamps it, stop() ends it early.
tui-chat-sendeventcancelable · { text, files }A message was sent. Cancel it to answer yourself.
tui-chat-stopeventbubblesStop was pressed while an answer printed.
--tui-chat-hCSS property540pxThe chat’s height; the thread scrolls inside it.
Enter · Shift+Enterkeyboardin the composerSend · a new line.
04

Markup

<tui-chat label="Studio assistant" placeholder="e.g. Which room is free at 3?">
  <!-- The empty state: shown until the first message. -->
  <div class="tui-chat__empty">
    <span class="tui-status"><span class="tui-lamp" data-lamp="amber"></span>Ready</span>
    <p>Ask about rooms, sessions and bookings.</p>
    <div class="tui-chat__suggest">
      <button type="button" class="tui-key tui-key--sm" data-suggest>Which room is free at 3?</button>
    </div>
  </div>
  <!-- Messages you already have. <pre> in an answer becomes a code block with a copy key. -->
  <article class="tui-chat__msg" data-from="user" aria-label="You, 14:18">
    <div class="tui-chat__body"><p>Is anything free this afternoon?</p></div>
  </article>
  <article class="tui-chat__msg" data-from="assistant" aria-label="Studio assistant, 14:18">
    <div class="tui-chat__body"><p>Studio A is free from 15:00 to 18:00.</p>
      <pre data-label="JavaScript"><code>await fetch('/api/holds', …)</code></pre></div>
  </article>
</tui-chat>
import '/kit/kit.mjs';   // defines <tui-chat>

const chat = document.querySelector('tui-chat');
chat.addEventListener('tui-chat-send', async (e) => {
  e.preventDefault();                          // we answer, not the demo
  const out = chat.reply();                    // a slip feeds out; the lamp blinks
  for await (const piece of ask(e.detail.text, e.detail.files)) {
    piece.code ? out.code(piece.code, piece.language) : out.write(piece.text);
  }
  out.end();                                   // lamp out, time stamped, Copy added
});
chat.addEventListener('tui-chat-stop', () => abort());

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

05

Accessibility

  • The thread is a role="log", a polite live region, and focusable so it scrolls from the keyboard. A slip is aria-busy while it prints, so it is read once, whole, not letter by letter.
  • Every message is an <article> named by who and when (“Studio assistant, 14:18”).
  • The send key names what it will do (“Send” or “Stop the reply”) and is disabled while there is nothing to send.
  • Attached files are listed as tags, each with a remove key that names the file.
  • Printing is shown by words (“printing”) beside the amber lamp; the lamp stops blinking under reduced motion.
06

How it’s built

A custom element that builds a scrolling log and a composer around your markup. A slip is a raised face with a perforation drawn as one repeating radial gradient along its top (holes in the face colour with a shaded rim) and it feeds out with a 180ms clip and a 6px drop. The printer appends text nodes as they come, keeping the amber lamp as the last thing on the line; end() removes it, stamps the time and adds the copy key. The text area grows with field-sizing: content up to 140px; no script measures it.

07

Rules

chat.rules.mdIn the brain

The lamp at the print head is the whole loading state (no dots, no spinner) and Stop is always one key away.

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

08
03600:0012:0024:00

Glass

Dark display glass for plots and live readouts, whatever the finish around it. Everything printed inside it is lit.

Lamp

The only colour in the kit: a lamp that is on. Green is on, amber is waiting, red is recording or failing, blue is linked, white is standing by. Always next to words.

Field

Text entry in a recess, the label printed above it, and a hint that says how to fix an error.

Docs
Finish
Volume