beta

AI Chat — <mv-ai-chat>

AI chat interface: streaming replies with a caret, shimmering “thinking” state, safe Markdown rendering (lists, bold, code with a copy button), auto-growing composer with attachments, model picker, suggestions, stop / regenerate, and auto-scroll that yields to the user.

CategoryBlocks
TypeWeb Component (<mv-ai-chat>)
Statusbeta
Also installsbutton, kbd, shimmer-text, textarea
Keywordsai, chat, assistant, llm, streaming, markdown, composer, conversation

When to use

Avoid when

Install

node scripts/add.mjs ai-chat --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["ai-chat"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, components/ai-chat/ai-chat.js, components/ai-chat/ai-chat.css, components/button/button.css, components/kbd/kbd.css, components/shimmer-text/shimmer-text.css, components/textarea/textarea.css, components/textarea/char-count.js.

Usage

Canonical markup — start from it and customize with attributes, data-* and CSS variables:

<mv-ai-chat id="aic-demo" label="Nova" model="nova-pro" style="height:680px;border:1px solid var(--mv-border);border-radius:var(--mv-radius-xl);overflow:hidden">
  <header class="mv-ai-chat-header">
    <mv-menu placement="bottom-start">
      <button class="mv-button mv-ai-chat-model" data-variant="ghost" data-size="sm" type="button">
        <span data-ai-chat-model-label>Nova Pro</span>
        <svg 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>
      </button>
      <div data-content style="--mv-menu-width:17rem">
        <div class="mv-menu-label">Model</div>
        <div role="group" data-name="model">
          <button role="menuitemradio" value="nova-pro" data-label="Nova Pro" aria-checked="true"><span>Nova Pro<span class="mv-ai-chat-menu-desc">Deep reasoning</span></span></button>
          <button role="menuitemradio" value="nova-flash" data-label="Nova Flash"><span>Nova Flash<span class="mv-ai-chat-menu-desc">Fast everyday answers</span></span></button>
          <button role="menuitemradio" value="nova-code" data-label="Nova Code"><span>Nova Code<span class="mv-ai-chat-menu-desc">Built for coding</span></span></button>
        </div>
      </div>
    </mv-menu>
    <div class="mv-ai-chat-header-end">
      <button class="mv-button" data-variant="ghost" data-size="sm" type="button" data-ai-chat-reset>
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.4 3.6a2 2 0 0 1 2.9 2.9L7 18.8 3 20l1.2-4z"/></svg>
        <span class="mv-ai-chat-header-hide-sm">New chat</span>
      </button>
    </div>
  </header>

  <div class="mv-ai-chat-empty">
    <span class="mv-ai-chat-empty-mark" aria-hidden="true"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.5c.4 3.9 1.9 6.6 4.2 8 1.3.8 2.9 1.3 4.8 1.5-3.9.4-6.6 1.9-8 4.2-.8 1.3-1.3 2.9-1.5 4.8-.4-3.9-1.9-6.6-4.2-8-1.3-.8-2.9-1.3-4.8-1.5 3.9-.4 6.6-1.9 8-4.2.8-1.3 1.3-2.9 1.5-4.8z"/></svg></span>
    <h2>Hi Maya, ready to dive in?</h2>
    <p>Ask a question, attach a file or start from a suggestion.</p>
    <div class="mv-ai-chat-suggestions">
      <button class="mv-ai-chat-suggestion" type="button" data-prompt="Write a follow-up email about Ms. Chen’s quote"><strong>Write a follow-up</strong><span>about Ms. Chen’s quote</span></button>
      <button class="mv-ai-chat-suggestion" type="button" data-prompt="Summarize September sales"><strong>Summarize sales</strong><span>for September, in 5 points</span></button>
      <button class="mv-ai-chat-suggestion" type="button" data-prompt="Explain Web Components with an example"><strong>Explain Web Components</strong><span>with a code example</span></button>
    </div>
  </div>

  <form class="mv-ai-chat-composer">
    <div class="mv-textarea-group">
      <ul class="mv-ai-chat-attachments" aria-label="Attachments" hidden></ul>
      <textarea class="mv-textarea" rows="1" placeholder="Message Nova…" aria-label="Message Nova" aria-keyshortcuts="Enter"></textarea>
      <div class="mv-textarea-toolbar">
        <label class="mv-button mv-ai-chat-attach" data-variant="ghost" data-size="icon" title="Attach a file">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m21 11-8.6 8.6a5.5 5.5 0 0 1-7.8-7.8l8.6-8.6a3.7 3.7 0 0 1 5.2 5.2l-8.6 8.6a1.8 1.8 0 0 1-2.6-2.6L15 7"/></svg>
          <input type="file" multiple aria-label="Attach files">
        </label>
        <span class="mv-textarea-toolbar-spacer"></span>
        <button class="mv-button" data-size="icon" type="submit" data-ai-chat-send aria-label="Send message">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"/></svg>
          <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>
        </button>
      </div>
    </div>
    <p class="mv-ai-chat-hint"><kbd class="mv-kbd" data-size="sm">Enter</kbd> to send · <kbd class="mv-kbd" data-size="sm">Shift</kbd> + <kbd class="mv-kbd" data-size="sm">Enter</kbd> for a new line · Nova can make mistakes. Check important info.</p>
  </form>

  <script type="application/json">[
    { "role": "user", "content": "How do I build a Web Component that shows a counter?", "files": [{ "name": "counter-brief.pdf", "size": 184320 }] },
    { "role": "assistant", "content": "Here’s a minimal, dependency-free counter:\n\n```js\nclass MvCounter extends HTMLElement {\n  #count = 0;\n  connectedCallback() {\n    this.innerHTML = `<button type=\"button\">0</button>`;\n    this.querySelector(\"button\").addEventListener(\"click\", (e) => {\n      e.currentTarget.textContent = ++this.#count;\n    });\n  }\n}\ncustomElements.define(\"mv-counter\", MvCounter);\n```\n\nA few best practices:\n\n- prefix the tag name (`mv-`) to avoid collisions\n- clean up your listeners in `disconnectedCallback()`\n- expose state through **attributes** to stay compatible with every framework" }
  ]</script>
</mv-ai-chat>

API

Attributes

NameTypeDefaultDescription
labelstringAssistantAssistant name (accessible labels, announcements).
modelstringCurrent model; synced with a radio menu data-name="model" and passed to the responder.
user-labelstringYouAccessible name for the user’s messages.
thinking-labelstringThinkingText for the waiting state.
data-statethinking | streamingSet while generating.
data-emptybooleanSet when the conversation is empty.

Properties

NameTypeDescription
responder(messages, { signal, model }) => AsyncIterable<string> | Promise<string> | stringResponse source. Default: local simulation (canned English replies, no network). Honor signal for stopping.
messages{ role, content, files? }[]Reads/writes the history. Also settable via a child <script type="application/json">.
busybooleanRead-only: a response is in progress.

Methods

NameDescription
send(text)Sends a message (with pending attachments) and generates the response.
stop()Stops generating (also Escape in the field, or the send button while it reads Stop).
regenerate()Replaces the last response.
reset()Clears the conversation.
renderMarkdown(source, element)Export: lightweight, safe Markdown rendering (textContent only).

Events

NameDescription
mv-sendCancelable. detail: { content, files }.
mv-responseResponse finished. detail: { content, stopped }.
mv-stop / mv-reset / mv-errorStop, reset, responder error (detail.error).

Content structure

NameDescription
.mv-ai-chat-headerFree-form header; [data-ai-chat-model-label] shows the selected model, [data-ai-chat-reset] clears the conversation.
.mv-ai-chat-emptyEmpty state (hidden after the first message); any [data-prompt] sends its text.
form.mv-ai-chat-composerComposer: textarea (Enter sends, Shift + Enter = new line), input[type=file], ul.mv-ai-chat-attachments, [data-ai-chat-send] button with two icons (send / stop).

CSS classes

NameDescription
mv-ai-chat-log / -thread / -message / -content / -code / -actionsGenerated parts (message: data-role user | assistant, data-pending).
mv-ai-chat-suggestions / -suggestion / -empty-markSuggestion grid and empty-state badge.
mv-ai-chat-chip / -attach / -hint / -model / -menu-descAttachment, file button, hint, model trigger and model description.

CSS variables

NameDefaultDescription
--mv-ai-chat-max-width46remReading width of the thread and composer.
--mv-ai-chat-user-bgBackground of the user’s bubbles.

Accessibility

Thread with role=log (aria-busy while generating); each message is a named <article> (“You” / assistant name). Streaming isn’t read word by word: a role=status region announces “… is writing a reply”, then the final response (code is summarized). Send button: label toggles between Send / Stop. Focusable code blocks (horizontal scrolling by keyboard), named Copy / Regenerate buttons. Reduced motion: no typing animation or blinking caret.