conversation / 07

Installation

tokens.css + chat-input.css + chat-input-features.css + button.css + host-chrome.css
head.html
<link rel="stylesheet" href="tokens.css">
<link rel="stylesheet" href="components/chat-input/chat-input.css">

Basic

chat-input / Basicinteractive

Code

chat-input.html + chat-input.css
chat-input
<style>
/* ============================================================
   chat-input — agent composer (CORE)
   Structural skeleton: field, textarea fallback, footer, send,
   and host states. The 40-item craft layers (ring, chips, pills,
   slash palette, enhance flow, menus, popover) live in the sibling
   chat-input-features.css, imported below so linking just this file
   keeps every existing consumer working unchanged.
   No-JS baseline works from this file alone.
   ============================================================ */
@import url("chat-input-features.css");

/* ============================================================
   chat-input — agent composer
   Ported craft from aicss.dev/components/ai-agent-input (PromptInput)
   into the agent-ui token system. No-JS baseline: native textarea
   + native details menus + native file input + form submit.
   Enhanced (JS): contentEditable field, skill pills, slash palette,
   enhance/revert flow with conic ring + shimmer + FLIP height.
   ============================================================ */

.au-chat-input {
  --au-composer-ring: color-mix(in srgb, var(--au-fg) 8%, transparent);
  --au-composer-drop-1: color-mix(in srgb, var(--au-fg) 5%, transparent);
  --au-composer-drop-2: color-mix(in srgb, var(--au-fg) 2%, transparent);
  --au-composer-hairline: color-mix(in srgb, var(--au-fg) 10%, transparent);
  --au-composer-item-hover: color-mix(in srgb, var(--au-fg) 6%, transparent);
  --au-composer-item-active: color-mix(in srgb, var(--au-fg) 9%, transparent);
  --au-composer-fill: color-mix(in srgb, var(--au-fg) 6%, transparent);
  --au-composer-fill-strong: color-mix(in srgb, var(--au-fg) 12%, transparent);
  --au-composer-muted: var(--au-fg-subtle);
  --au-skill-bg: var(--au-brand-blue-soft);
  --au-skill-fg: var(--au-brand-blue-fg);
  --au-skill-bg-hover: var(--au-brand-blue-soft-hover);
  --au-ring-gradient: conic-gradient(from var(--pi-angle), var(--au-brand-blue), var(--au-brand-violet), var(--au-brand-pink), var(--au-brand-cyan), var(--au-brand-blue));
  --au-composer-max: 440px;

  background: var(--au-composer-bg, var(--au-surface));
  border: 0.5px solid transparent; /* keeps geometry for the enhancing ::after ring */
  border-radius: var(--au-radius-md); /* 12px */
  box-shadow:
    0 0 0 0.5px var(--au-composer-ring),
    0 1px 2px var(--au-composer-drop-1),
    0 2px 4px var(--au-composer-drop-2);
  display: grid;
  gap: 12px;
  padding: 8px 12px 12px;
  transition:
    box-shadow var(--au-duration-fast) var(--au-ease),
    border-color var(--au-duration-fast) var(--au-ease);
}

.au-chat-input[data-au-layout="standalone"] {
  max-width: var(--au-composer-max);
}

.au-chat-input:focus-within {
  border-color: color-mix(in srgb, var(--au-fg) 14%, transparent);
  box-shadow:
    0 0 0 0.5px color-mix(in srgb, var(--au-fg) 16%, transparent),
    0 1px 2px var(--au-composer-drop-1),
    0 2px 4px var(--au-composer-drop-2);
}

/* ------------------------------------------------------------
   Field — native textarea (no-JS) + contentEditable (enhanced)
   (items 7–11, 32)
   ------------------------------------------------------------ */
.au-chat-input__fieldwrap {
  position: relative;
}

.au-chat-input__field {
  background: transparent;
  color: var(--au-fg);
  font-size: 12px;
  line-height: 18px;
  letter-spacing: -0.12px;
  margin: 0;
  min-height: 20px;
  max-height: 160px;
  outline: 0;
  overflow-y: auto;
  position: relative;
  white-space: pre-wrap;
  width: 100%;
  word-break: break-word;
}

.au-chat-input__field ::selection,
.au-chat-input__field::selection {
  background: Highlight;
  color: HighlightText;
}

.au-chat-input__field[data-empty]::before {
  content: attr(data-placeholder);
  color: var(--au-fg);
  left: 0;
  opacity: 0.5;
  pointer-events: none;
  position: absolute;
  top: 0;
}


/* Native textarea fallback keeps the no-JS path readable */
.au-chat-input__textarea {
  background: transparent;
  border: 0;
  color: var(--au-fg);
  font: 400 12px/18px var(--au-font-sans);
  letter-spacing: -0.12px;
  margin: 0;
  max-height: 160px;
  min-height: 20px;
  outline: 0;
  overflow-y: auto;
  padding: 0;
  resize: none;
  width: 100%;
}

.au-chat-input__textarea:focus-visible,
.au-chat-input__icon:focus-visible {
  box-shadow: var(--au-focus-ring);
  outline: 2px solid var(--au-focus);
  outline-offset: 2px;
}

.au-chat-input__textarea::placeholder {
  color: color-mix(in srgb, var(--au-fg-muted) 80%, transparent);
}

/* ------------------------------------------------------------
   Footer row + icon buttons (items 18, 22, 34–35)
   Fill lives on ::before so :active scales only the container.
   ------------------------------------------------------------ */
.au-chat-input__footer {
  align-items: center;
  display: flex;
  gap: 8px;
  justify-content: space-between;
  padding-inline: 4px;
}

.au-chat-input__hint {
  color: var(--au-fg-subtle);
  font: 12px/1.4 var(--au-font-sans);
}

.au-chat-input__tools,
.au-chat-input__actions {
  align-items: center;
  display: flex;
  gap: 8px;
}

.au-chat-input__icon {
  align-items: center;
  background: transparent;
  border: 0;
  color: var(--au-fg);
  cursor: pointer;
  display: inline-flex;
  flex: none;
  height: 24px;
  justify-content: center;
  padding: 0;
  position: relative;
  width: 24px;
}

@media (max-width: 640px) {
  .au-chat-input__icon {
    height: 40px;
    width: 40px;
  }
}

@media (pointer: coarse) {
  .au-chat-input__icon {
    height: 44px;
    width: 44px;
  }
}

.au-chat-input__icon::before {
  background: var(--au-composer-fill);
  border-radius: 999px;
  content: "";
  inset: 0;
  position: absolute;
  transition: background 150ms cubic-bezier(0.22, 1, 0.36, 1), transform 150ms cubic-bezier(0.22, 1, 0.36, 1);
}

.au-chat-input__icon:hover::before {
  background: var(--au-composer-item-hover);
}

.au-chat-input__icon:active::before {
  transform: scale(0.98);
}

.au-chat-input__icon > svg,
.au-chat-input__icon > span {
  position: relative;
}

.au-chat-input__plusicon {
  display: inline-flex;
  transition: transform 200ms cubic-bezier(0.35, 1.55, 0.65, 1);
}

.au-chat-input__plus[data-open]::before {
  background: var(--au-composer-fill-strong);
}

.au-chat-input__plus[data-open] .au-chat-input__plusicon {
  transform: rotate(45deg);
}

/* Send button — idle muted, active filled (item 39) */
.au-chat-input__send {
  color: var(--au-composer-muted);
}

.au-chat-input__send:disabled {
  cursor: default;
}

.au-chat-input__send:disabled:active::before {
  transform: none;
}

.au-chat-input__send[data-active] {
  color: var(--au-on-accent);
}

.au-chat-input__send[data-active]::before {
  background: var(--au-fg);
}

.au-chat-input__send[data-active]:hover::before {
  background: color-mix(in srgb, var(--au-fg) 82%, transparent);
}

/* ------------------------------------------------------------
   Streaming / disabled / error (existing contract, retuned)
   ------------------------------------------------------------ */
.au-chat-input[data-au-state="streaming"] {
  border-color: color-mix(in srgb, var(--au-action) 30%, transparent);
}

.au-chat-input[data-au-state="disabled"] {
  opacity: var(--au-disabled-opacity);
  pointer-events: none;
}

.au-chat-input[data-au-state="error"] {
  border-color: var(--au-danger);
}

@media (prefers-reduced-motion: reduce) {
  .au-chat-input,
  .au-chat-input__icon::before,
  .au-chat-input__plusicon {
    animation: none;
    transition-duration: 0.01ms;
  }
}
</style>

<form class="au-chat-input" method="post" aria-label="Message" enctype="multipart/form-data"><div class="au-chat-input__attachments"><div class="au-chat-input__attachment"><button type="button" class="au-chat-input__attachment-preview" aria-label="Image attachment"><img alt="Attachment preview" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='112' height='112'%3E%3Crect fill='%23e8e8e8' width='112' height='112'/%3E%3Ccircle cx='40' cy='40' r='12' fill='%23bbb'/%3E%3Cpath d='M0 90 L35 55 L55 75 L80 45 L112 90 Z' fill='%23cfcfcf'/%3E%3C/svg%3E"></button><button type="button" class="au-btn au-chat-input__attachment-remove" aria-label="Remove file">×</button></div></div><textarea class="au-chat-input__textarea" id="input-preview-message" name="message" rows="1" placeholder="Send a message..." aria-label="Message"></textarea><div class="au-chat-input__footer"><div class="au-chat-input__tools"><label class="au-btn au-btn--ghost au-chat-input__tool" title="Add attachment"><span class="sr-only">Add attachment</span><input class="sr-only" type="file" name="attachment" accept="image/*,.pdf,.txt,.md" multiple><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="M5 12h14"/><path d="M12 5v14"/></svg></label><details class="au-chat-input__model"><summary><span class="au-chat-input__model-mark" aria-hidden="true">AI</span><span class="au-chat-input__model-label">GPT-5.4 Nano</span><svg class="au-chat-input__model-chevron" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></summary><ul class="au-chat-input__model-menu" role="listbox" aria-label="Model"><li><button type="button" class="au-chat-input__model-option" role="option" aria-selected="true"><span class="au-chat-input__model-mark" aria-hidden="true">AI</span>GPT-5.4 Nano</button></li><li><button type="button" class="au-chat-input__model-option" role="option" aria-selected="false"><span class="au-chat-input__model-mark" aria-hidden="true">AI</span>GPT-5.4 Mini</button></li><li><button type="button" class="au-chat-input__model-option" role="option" aria-selected="false"><span class="au-chat-input__model-mark" aria-hidden="true">G</span>Gemini 3.1 Flash Lite</button></li><li><button type="button" class="au-chat-input__model-option" role="option" aria-selected="false"><span class="au-chat-input__model-mark" aria-hidden="true">X</span>Grok 4.1 Fast</button></li><li><button type="button" class="au-chat-input__model-option" role="option" aria-selected="false"><span class="au-chat-input__model-mark" aria-hidden="true">X</span>Grok 3 Mini</button></li><li><button type="button" class="au-chat-input__model-option" role="option" aria-selected="false"><span class="au-chat-input__model-mark" aria-hidden="true">L</span>Llama 4 Scout 17B</button></li><li><button type="button" class="au-chat-input__model-option" role="option" aria-selected="false"><span class="au-chat-input__model-mark" aria-hidden="true">Q</span>Qwen3 32B</button></li></ul></details></div><div class="au-chat-input__actions"><button class="au-btn au-btn--ghost" type="button" aria-label="Start voice input"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19v3"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><rect x="9" y="2" width="6" height="13" rx="3"/></svg></button><button class="au-btn au-btn--primary" type="submit" aria-label="Send"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 7-7 7 7"/><path d="M12 19V5"/></svg></button></div></div></form>

Quote preview

chat-input / Quote previewinteractive

Quoted selection…

Code

chat-input.html + chat-input.css
chat-input
<style>
/* ============================================================
   chat-input — agent composer (CORE)
   Structural skeleton: field, textarea fallback, footer, send,
   and host states. The 40-item craft layers (ring, chips, pills,
   slash palette, enhance flow, menus, popover) live in the sibling
   chat-input-features.css, imported below so linking just this file
   keeps every existing consumer working unchanged.
   No-JS baseline works from this file alone.
   ============================================================ */
@import url("chat-input-features.css");

/* ============================================================
   chat-input — agent composer
   Ported craft from aicss.dev/components/ai-agent-input (PromptInput)
   into the agent-ui token system. No-JS baseline: native textarea
   + native details menus + native file input + form submit.
   Enhanced (JS): contentEditable field, skill pills, slash palette,
   enhance/revert flow with conic ring + shimmer + FLIP height.
   ============================================================ */

.au-chat-input {
  --au-composer-ring: color-mix(in srgb, var(--au-fg) 8%, transparent);
  --au-composer-drop-1: color-mix(in srgb, var(--au-fg) 5%, transparent);
  --au-composer-drop-2: color-mix(in srgb, var(--au-fg) 2%, transparent);
  --au-composer-hairline: color-mix(in srgb, var(--au-fg) 10%, transparent);
  --au-composer-item-hover: color-mix(in srgb, var(--au-fg) 6%, transparent);
  --au-composer-item-active: color-mix(in srgb, var(--au-fg) 9%, transparent);
  --au-composer-fill: color-mix(in srgb, var(--au-fg) 6%, transparent);
  --au-composer-fill-strong: color-mix(in srgb, var(--au-fg) 12%, transparent);
  --au-composer-muted: var(--au-fg-subtle);
  --au-skill-bg: var(--au-brand-blue-soft);
  --au-skill-fg: var(--au-brand-blue-fg);
  --au-skill-bg-hover: var(--au-brand-blue-soft-hover);
  --au-ring-gradient: conic-gradient(from var(--pi-angle), var(--au-brand-blue), var(--au-brand-violet), var(--au-brand-pink), var(--au-brand-cyan), var(--au-brand-blue));
  --au-composer-max: 440px;

  background: var(--au-composer-bg, var(--au-surface));
  border: 0.5px solid transparent; /* keeps geometry for the enhancing ::after ring */
  border-radius: var(--au-radius-md); /* 12px */
  box-shadow:
    0 0 0 0.5px var(--au-composer-ring),
    0 1px 2px var(--au-composer-drop-1),
    0 2px 4px var(--au-composer-drop-2);
  display: grid;
  gap: 12px;
  padding: 8px 12px 12px;
  transition:
    box-shadow var(--au-duration-fast) var(--au-ease),
    border-color var(--au-duration-fast) var(--au-ease);
}

.au-chat-input[data-au-layout="standalone"] {
  max-width: var(--au-composer-max);
}

.au-chat-input:focus-within {
  border-color: color-mix(in srgb, var(--au-fg) 14%, transparent);
  box-shadow:
    0 0 0 0.5px color-mix(in srgb, var(--au-fg) 16%, transparent),
    0 1px 2px var(--au-composer-drop-1),
    0 2px 4px var(--au-composer-drop-2);
}

/* ------------------------------------------------------------
   Field — native textarea (no-JS) + contentEditable (enhanced)
   (items 7–11, 32)
   ------------------------------------------------------------ */
.au-chat-input__fieldwrap {
  position: relative;
}

.au-chat-input__field {
  background: transparent;
  color: var(--au-fg);
  font-size: 12px;
  line-height: 18px;
  letter-spacing: -0.12px;
  margin: 0;
  min-height: 20px;
  max-height: 160px;
  outline: 0;
  overflow-y: auto;
  position: relative;
  white-space: pre-wrap;
  width: 100%;
  word-break: break-word;
}

.au-chat-input__field ::selection,
.au-chat-input__field::selection {
  background: Highlight;
  color: HighlightText;
}

.au-chat-input__field[data-empty]::before {
  content: attr(data-placeholder);
  color: var(--au-fg);
  left: 0;
  opacity: 0.5;
  pointer-events: none;
  position: absolute;
  top: 0;
}


/* Native textarea fallback keeps the no-JS path readable */
.au-chat-input__textarea {
  background: transparent;
  border: 0;
  color: var(--au-fg);
  font: 400 12px/18px var(--au-font-sans);
  letter-spacing: -0.12px;
  margin: 0;
  max-height: 160px;
  min-height: 20px;
  outline: 0;
  overflow-y: auto;
  padding: 0;
  resize: none;
  width: 100%;
}

.au-chat-input__textarea:focus-visible,
.au-chat-input__icon:focus-visible {
  box-shadow: var(--au-focus-ring);
  outline: 2px solid var(--au-focus);
  outline-offset: 2px;
}

.au-chat-input__textarea::placeholder {
  color: color-mix(in srgb, var(--au-fg-muted) 80%, transparent);
}

/* ------------------------------------------------------------
   Footer row + icon buttons (items 18, 22, 34–35)
   Fill lives on ::before so :active scales only the container.
   ------------------------------------------------------------ */
.au-chat-input__footer {
  align-items: center;
  display: flex;
  gap: 8px;
  justify-content: space-between;
  padding-inline: 4px;
}

.au-chat-input__hint {
  color: var(--au-fg-subtle);
  font: 12px/1.4 var(--au-font-sans);
}

.au-chat-input__tools,
.au-chat-input__actions {
  align-items: center;
  display: flex;
  gap: 8px;
}

.au-chat-input__icon {
  align-items: center;
  background: transparent;
  border: 0;
  color: var(--au-fg);
  cursor: pointer;
  display: inline-flex;
  flex: none;
  height: 24px;
  justify-content: center;
  padding: 0;
  position: relative;
  width: 24px;
}

@media (max-width: 640px) {
  .au-chat-input__icon {
    height: 40px;
    width: 40px;
  }
}

@media (pointer: coarse) {
  .au-chat-input__icon {
    height: 44px;
    width: 44px;
  }
}

.au-chat-input__icon::before {
  background: var(--au-composer-fill);
  border-radius: 999px;
  content: "";
  inset: 0;
  position: absolute;
  transition: background 150ms cubic-bezier(0.22, 1, 0.36, 1), transform 150ms cubic-bezier(0.22, 1, 0.36, 1);
}

.au-chat-input__icon:hover::before {
  background: var(--au-composer-item-hover);
}

.au-chat-input__icon:active::before {
  transform: scale(0.98);
}

.au-chat-input__icon > svg,
.au-chat-input__icon > span {
  position: relative;
}

.au-chat-input__plusicon {
  display: inline-flex;
  transition: transform 200ms cubic-bezier(0.35, 1.55, 0.65, 1);
}

.au-chat-input__plus[data-open]::before {
  background: var(--au-composer-fill-strong);
}

.au-chat-input__plus[data-open] .au-chat-input__plusicon {
  transform: rotate(45deg);
}

/* Send button — idle muted, active filled (item 39) */
.au-chat-input__send {
  color: var(--au-composer-muted);
}

.au-chat-input__send:disabled {
  cursor: default;
}

.au-chat-input__send:disabled:active::before {
  transform: none;
}

.au-chat-input__send[data-active] {
  color: var(--au-on-accent);
}

.au-chat-input__send[data-active]::before {
  background: var(--au-fg);
}

.au-chat-input__send[data-active]:hover::before {
  background: color-mix(in srgb, var(--au-fg) 82%, transparent);
}

/* ------------------------------------------------------------
   Streaming / disabled / error (existing contract, retuned)
   ------------------------------------------------------------ */
.au-chat-input[data-au-state="streaming"] {
  border-color: color-mix(in srgb, var(--au-action) 30%, transparent);
}

.au-chat-input[data-au-state="disabled"] {
  opacity: var(--au-disabled-opacity);
  pointer-events: none;
}

.au-chat-input[data-au-state="error"] {
  border-color: var(--au-danger);
}

@media (prefers-reduced-motion: reduce) {
  .au-chat-input,
  .au-chat-input__icon::before,
  .au-chat-input__plusicon {
    animation: none;
    transition-duration: 0.01ms;
  }
}
</style>

<div class="au-quote-preview"><p class="au-quote-preview__text">Quoted selection…</p><button type="button" class="au-btn au-btn--ghost au-quote-preview__dismiss" data-au-action="dismiss-quote" aria-label="Dismiss quote">×</button></div>

Component contract

Install
tokens.css · chat-input.css · chat-input-features.css · button.css · host-chrome.css
States
idle, streaming, disabled, error
Host
validation, drafts, uploads, and request lifecycle
For agents
chat-input.contract
# agent-ui / chat-input
component: chat-input
selector: .au-chat-input
data_au_component: chat-input
purpose: Native composer with optional quote-preview part (absorbed from quote).

install:
  - tokens.css
  - chat-input.css
  - chat-input-features.css
  - button.css for submit
  - host-chrome.css for attachments/model/voice

states: idle, streaming, disabled, error.
css_variables: Uses composer elevation tokens.
events: native form, focus, and click only — no custom events or runtime required
no_javascript: Form submits with method=post without script.
host_owns: The host owns validation, drafts, uploads, and request lifecycle.
accessibility: Label the textarea; keep submit/stop concrete.
security: Treat message and attachments as untrusted input.

rules:
  - Host owns execution, authorization, persistence, validation, and state transitions.
  - Components render host-provided state; they never execute tools or imply approval.
  - Escape untrusted model output, tool args, and tool output before insertion.
  - Use only documented data-au-* attributes and data-au-state values.
  - Keep the no-JS path usable. Do not add React, Tailwind, bundlers, or remote assets.
source: components/chat-input/chat-input.{html,css} + README.md + fixture.html