beta

Thinking Orb — <mv-thinking-orb>

Canvas AI assistant orb: a glossy blob that warps, swirls and changes material with its state (idle, listening, thinking with orbiting satellites, speaking, success, error), with smooth transitions between states. Reacts to voice: manual level, MediaStream or a user-enabled microphone.

CategoryFeedback
TypeWeb Component (<mv-thinking-orb>)
Statusbeta
Keywordsai, assistant, orb, voice, microphone, audio, loader, agent, canvas, status

When to use

Avoid when

Install

node scripts/add.mjs thinking-orb --out ./src/marvelous

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

Files copied (dependencies included): tokens/tokens.css, core/base.css, core/canvas.js, core/dom.js, core/element.js, core/motion.js, components/thinking-orb/thinking-orb.js, components/thinking-orb/thinking-orb.css.

Usage

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

<div style="display:grid;justify-items:center;gap:1.1rem;width:100%;max-width:560px">
  <mv-thinking-orb id="mv-thinking-orb-demo" state="idle" label="Assistant" mic style="--mv-thinking-orb-size:12rem"></mv-thinking-orb>
  <p id="mv-thinking-orb-demo-caption" style="margin:0;min-height:1.4em;font-size:.9rem;color:var(--mv-fg-muted)">Tap the orb to talk, or pick a state.</p>
  <div role="group" aria-label="Assistant state" style="display:flex;flex-wrap:wrap;justify-content:center;gap:.4rem">
    <button class="mv-button" data-size="sm" data-variant="outline" data-state="idle" aria-pressed="true">Idle</button>
    <button class="mv-button" data-size="sm" data-variant="outline" data-state="listening" aria-pressed="false">Listening</button>
    <button class="mv-button" data-size="sm" data-variant="outline" data-state="thinking" aria-pressed="false">Thinking</button>
    <button class="mv-button" data-size="sm" data-variant="outline" data-state="speaking" aria-pressed="false">Speaking</button>
    <button class="mv-button" data-size="sm" data-variant="outline" data-state="success" aria-pressed="false">Success</button>
    <button class="mv-button" data-size="sm" data-variant="outline" data-state="error" aria-pressed="false">Error</button>
  </div>
  <div style="display:flex;gap:1.5rem;align-items:center;margin-top:.4rem">
    <mv-thinking-orb state="thinking" label="Search" style="--mv-thinking-orb-size:3.5rem"></mv-thinking-orb>
    <mv-thinking-orb state="speaking" label="Reading" colors="oklch(0.78 0.16 160), oklch(0.72 0.14 200), oklch(0.85 0.12 110)" style="--mv-thinking-orb-size:3.5rem"></mv-thinking-orb>
    <mv-thinking-orb state="listening" label="Dictation" colors="oklch(0.72 0.19 30), oklch(0.8 0.15 70), oklch(0.66 0.2 350)" style="--mv-thinking-orb-size:3.5rem"></mv-thinking-orb>
  </div>
</div>
<style>
  [aria-label="Assistant state"] .mv-button[aria-pressed="true"] { background: var(--mv-accent-subtle); border-color: var(--mv-accent); color: var(--mv-accent-fg); }
</style>
<script type="module">
  const orb = document.getElementById("mv-thinking-orb-demo");
  const caption = document.getElementById("mv-thinking-orb-demo-caption");
  const texts = {
    idle: "Tap the orb to talk, or pick a state.",
    listening: "I’m listening…",
    thinking: "Looking up flights to Lisbon in October…",
    speaking: "Three nonstop flights on Oct 12, from $89.",
    success: "Booking confirmed.",
    error: "Couldn’t reach the server. Try again.",
  };
  const buttons = [...document.querySelectorAll("[data-state]")].filter((b) => b.closest("[role=group]"));
  const sync = () => {
    for (const b of buttons) b.setAttribute("aria-pressed", String(b.dataset.state === orb.state));
    caption.textContent = texts[orb.state] ?? "";
  };
  for (const b of buttons) b.addEventListener("click", () => { orb.state = b.dataset.state; sync(); });
  orb.addEventListener("mv-mic", (e) => {
    if (e.detail.error) caption.textContent = "Microphone unavailable (" + e.detail.error + ").";
    else sync();
  });
</script>

API

Attributes

NameTypeDefaultDescription
stateidle | listening | thinking | speaking | success | erroridleDisplayed state; parameters (size, warp, swirl, glow, satellites, hue) are smoothly interpolated.
labelstringAssistantName announced with the state (“Assistant: thinking”).
colorslist of CSS colors--mv-thinking-orb-1/2/3Three material colors (tokens accepted, follow the theme).
levelnumber 0–1Manual voice level (listening / speaking). With no level or mic, “speaking” simulates a voice.
micbooleanThe orb becomes a toggle button that opens the microphone on click (getUserMedia) and switches to listening.
speednumber1Overall animation speed.

Properties

NameTypeDescription
levelnumber | nullVoice level 0–1 (update every frame if needed, without an attribute).
streamMediaStream | nullExternal audio stream to visualize (not stopped by the orb).
listeningMicbooleanTrue while the orb’s microphone is open.

Methods

NameDescription
listenMic()Opens the microphone (call it from a user gesture); returns a Promise<MediaStream>.
stopMic()Stops the orb’s microphone.

Events

NameDescription
mv-micdetail: { active, stream?, error? } — microphone opened, closed or denied.

CSS variables

NameDefaultDescription
--mv-thinking-orb-size10remSize (the glow and satellites fit inside the box).
--mv-thinking-orb-1 / -2 / -3accent, sky blue, pinkDefault material colors.

Accessibility

Decorative canvas (aria-hidden); the state is exposed as visually hidden text in a polite live region (“Assistant: listening”). With mic: toggle button with a constant name and aria-pressed, visible focus. The microphone is never opened without a user gesture. Reduced motion: a representative still frame for each state. Pauses when off-screen.