Installation
tokens.css + chat-input.css + chat-input-features.css + button.css + host-chrome.csshead.html
<link rel="stylesheet" href="tokens.css">
<link rel="stylesheet" href="components/chat-input/chat-input.css">Basic
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
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