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.
| Category | Feedback |
|---|---|
| Type | Web Component (<mv-thinking-orb>) |
| Status | beta |
| Keywords | ai, 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
- 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/marvelousAI 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
| Name | Type | Default | Description |
|---|---|---|---|
state | idle | listening | thinking | speaking | success | error | idle | Displayed state; parameters (size, warp, swirl, glow, satellites, hue) are smoothly interpolated. |
label | string | Assistant | Name announced with the state (“Assistant: thinking”). |
colors | list of CSS colors | --mv-thinking-orb-1/2/3 | Three material colors (tokens accepted, follow the theme). |
level | number 0–1 | Manual voice level (listening / speaking). With no level or mic, “speaking” simulates a voice. | |
mic | boolean | The orb becomes a toggle button that opens the microphone on click (getUserMedia) and switches to listening. | |
speed | number | 1 | Overall animation speed. |
Properties
| Name | Type | Description |
|---|---|---|
level | number | null | Voice level 0–1 (update every frame if needed, without an attribute). |
stream | MediaStream | null | External audio stream to visualize (not stopped by the orb). |
listeningMic | boolean | True while the orb’s microphone is open. |
Methods
| Name | Description |
|---|---|
listenMic() | Opens the microphone (call it from a user gesture); returns a Promise<MediaStream>. |
stopMic() | Stops the orb’s microphone. |
Events
| Name | Description |
|---|---|
mv-mic | detail: { active, stream?, error? } — microphone opened, closed or denied. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-thinking-orb-size | 10rem | Size (the glow and satellites fit inside the box). |
--mv-thinking-orb-1 / -2 / -3 | accent, sky blue, pink | Default 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.