moodi
Tiny 3D mood buddies for your UI.
A soft 3D character you drop into any page with one line. Its face shows what is happening. No models or textures to download; three.js is a peer dependency. One shared WebGL context for every buddy on the page, rendering pauses off-screen, and reduced motion is respected.
Install
npm i moodi threeUsage
React
import { Moodi } from 'moodi/react';
<Moodi shape="blob" mood={isLoading ? 'loading' : 'idle'} color="#8fb996" />Web Component
<moodi-buddy shape="ghost" mood="happy" color="#6c5ce7" size="160"></moodi-buddy>
<script type="module">import 'moodi';</script>CDN, no build step
<script type="module" src="https://esm.sh/moodi"></script>
<moodi-buddy mood="thinking"></moodi-buddy>The React component is safe to render on the server. three.js is loaded lazily in the browser, so importing moodi/react in Next.js never runs WebGL code on the server.
Props
Booleans on the Web Component take "true" / "false", e.g. sound="true".
React only
Sound
Off by default. Turn it on and every mood change plays a short sound effect, synthesized in the browser (no audio files). It also plays once when the buddy first appears. Browsers only allow audio after a click, tap or key press, so a sound requested before that waits a few seconds for the first one.
<Moodi mood="happy" sound />
<moodi-buddy mood="happy" sound="true"></moodi-buddy>Settle
Some moods keep moving: happy hops, error shakes, love floats with hearts. With settle the buddy plays the mood for that many milliseconds, then its body calms down to gentle idle breathing and the extras fade out, while the face keeps the expression. Every new mood (or reaction) starts the timer again.
<Moodi mood={done ? 'happy' : 'idle'} settle={3000} />
<moodi-buddy mood="happy" settle="3000"></moodi-buddy>Fast first paint
Give the buddy a still image and let three.js load on the first interaction. The page paints the image instantly and the live 3D buddy takes over without a visible swap.
<Moodi poster="/ghost-idle.webp" loading="interaction" />
<!-- Web Component: any child is shown until the first 3D frame -->
<moodi-buddy><img src="/ghost-idle.webp" alt="" /></moodi-buddy>Moods
- idleBreathing softly. Blinks now and then.
- thinkingEyes drift up. Little thoughts pop.
- loadingHalf-lidded, wobbling patiently.
- listeningAll eyes. Nods along to every word.
- speakingChatty mouth, bouncing to the rhythm.
- happyBounces with joy. Cheeks glow.
- surprisedWide eyes, tiny o. Did not see that coming.
- sadSlumps a little. Needs a minute.
- errorSomething broke. Shakes it off.
- sleepingDeep, slow breaths. Do not disturb.
- loveHeart eyes, floating on air.
Shapes and colors
Shapes: Ghost, Pebble, Blob, Drop, Bean, Cube. Preset palettes are exported as PALETTES:
Imperative API and events
const el = document.querySelector('moodi-buddy');
el.mood = 'thinking';
el.react('happy', { duration: 1200 }); // temporary reaction, then back to the current mood
el.sound = true;
el.addEventListener('moodchange', (e) => console.log(e.detail));
// e.detail: { mood, previous, reaction }
el.addEventListener('ready', () => {}); // first 3D frame drawn; the element also gets [ready]In React, pass a ref to reach the element, or use onMoodChange and onReady.











