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.

How it works

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>)
Statusstable
Keywordsai, assistant, orb, voice, microphone, audio, loader, agent, canvas, status

When to use

  • A voice or AI assistant needs a visual presence that reflects idle, listening, thinking and speaking states
  • The assistant should react to microphone input or an audio stream in real time
  • A single orb doubles as a mic toggle button for a voice interface

Avoid when

  • A lightweight inline thinking indicator is enough, such as in a chat message → use Pixel Loader instead
  • Several instances appear on one page or the target is low-end mobile; each orb runs its own canvas
  • A generic loading state with no assistant persona is needed → use Spinner instead

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/i18n.js, core/motion.js, components/thinking-orb/thinking-orb.js, components/thinking-orb/thinking-orb.css.

Usage

Canonical markup, to start from 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); }
  @media (forced-colors: active) { [aria-label="Assistant state"] .mv-button[aria-pressed="true"] { forced-color-adjust: none; background: Highlight; border-color: Highlight; color: HighlightText; } }
</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.
stringsPartial<Record<string, string>>Overrides for the default text (en-US): status (live status text, {label} and {state}), idle, listening, thinking, speaking, success, error (state words; success reads “done”), name (assistant name used when label is absent), mic (microphone button label, {label}), micName (name in that label when label is absent). App-wide translations: setStrings() from core/i18n.js, under the key "thinking-orb". Can be set before the element is defined.

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.