AI

AI Orb Face Mouth

A character that wears an orb: nine moods, gaze, blinking, and a mouth that talks over any of them.

A character — an orb with a face, and a face that has a mouth.

The material lives in AI Orb and the state-driven eyes live in AI Orb Face. This component is the third reading of the same character: the eyes stay, and a mouth joins them. That mouth changes the design, because once one is present the eyes can no longer carry the expression alone. So a mood picks one eye treatment and one mouth at the same time, and the two are tuned against each other so neither can win:

MoodEyesMouthAccent
idleopen ovalsgentle smile—
happyarches (^ ^)open smile—
excitedtaller ovals, liftedlaugh—
thinkingnarrowed, tiltedflat linetrailing dots
surprisedwide ovals, raised browsopen o—
sleepingshut lidssmall orising zs
sadlowered, worried browsfrown—
confusedone eye squintedwavy line?
talkingopen ovalsmoving mouth—

Moods are the public unit because they are how a character is described in conversation — happy, confused, asleep — while state is how a program is described. AI_ORB_FACE_MOUTH_STATE_MOOD maps one onto the other, so an assistant still gets a face for free:

StateMood
idleidle
listeningexcited
thinkingthinking
streaminghappy
donehappy
errorsad

streaming also turns talking on, so the mapped mood supplies the eyes and the speaking mouth supplies the motion.

Talking is the one modifier. The talking prop replaces only the mouth, so the eyes keep whatever the mood says and happy, thinking and confused can all speak. The talking row above is the standalone form — idle eyes with the speaking mouth — for when there is no mood to layer onto. It never applies while the character is asleep: sleeping wins.

Usage

Set mood and leave the rest alone: eyes, mouth, brows and accent all come from it. The second face below wears the same mood with talking on, which is the one modifier this component ships.

Assistant
Assistant, talking
<template>
  <div class="flex w-full max-w-lg flex-col gap-6">
    <div class="flex flex-wrap items-center justify-center gap-6 rounded-2xl border border-default p-8">
      <MAiOrbFaceMouth
        :mood="mood"
        aria-label="Assistant"
      />
      <MAiOrbFaceMouth
        :colors="orbFaceColors"
        :mood="mood"
        :size="96"
        talking
        aria-label="Assistant, talking"
      />
    </div>

    <div class="flex flex-wrap items-center gap-2">
      <UButton
        v-for="value in moods"
        :key="value"
        :color="mood === value ? 'primary' : 'neutral'"
        :label="value"
        size="sm"
        :variant="mood === value ? 'solid' : 'outline'"
        @click="mood = value"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
type DemoMood = 'idle' | 'happy' | 'excited' | 'thinking' | 'surprised' | 'sleeping' | 'sad' | 'confused' | 'talking'

const moods: DemoMood[] = ['idle', 'happy', 'excited', 'thinking', 'surprised', 'sleeping', 'sad', 'confused', 'talking']
const mood = ref<DemoMood>('happy')

const orbFaceColors = {
  body: 'secondary',
  bodyEdge: 'color-mix(in oklab, var(--ui-secondary), white 78%)',
  feature: 'color-mix(in oklab, var(--ui-secondary), black 82%)',
}
</script>

Customize

Customize

Aria Label
Amplitude
Gaze
MoodHappy
Painted
Size128
StateIdle
Talking

Props

Prop Type Default Description
mood'idle' | 'happy' | 'excited' | 'thinking' | 'surprised' | 'sleeping' | 'sad' | 'confused' | 'talking'-Sets the character directly and ignores state. Every mood is a complete read — eyes, mouth, optional brows and optional accent — so no combination can land as an accidental expression.
talkingboolean-Swap the mouth for a speaking one without changing the rest of the face, so happy, thinking and confused can all talk. Defaults to on for the streaming state, and never applies while the character is asleep.
state'idle' | 'listening' | 'thinking' | 'streaming' | 'done' | 'error''idle'The assistant state. Used only when mood is absent, and mapped through AI_ORB_FACE_MOUTH_STATE_MOOD. It also selects a motion profile and a saturation.
expressionAiOrbFaceMouthMood-Deprecated alias for mood.
paintedbooleanfalsePaint the sphere as a gradient instead of mounting the WebGL orb. A browser keeps only about sixteen live contexts per page, so a grid of faces — a gallery, an assistant list — runs out of them. The painted sphere looks the same at small sizes and costs no GPU.
sizenumber | string128Pixels when a number, any CSS length when a string.
ariaLabelstring-Names the character for assistive technology. Without it the SVG stays decorative (aria-hidden, no role); with it the SVG becomes role="img" with an <title>.
gazebooleantrueFollow the pointer. Turn it off where the face is decorative and the pointer is busy — a page of six faces all tracking one cursor is noise, not life.
colorsPartial<{ body, bodyEdge, feature }>-Overrides any of the three colours. Each value accepts a Nuxt UI color token (e.g. primary) or any CSS color. Defaults follow the theme: the body is var(--ui-primary), the edge is mixed toward white and the features toward black, so the marbled interior reads as a sphere and the face stays legible against it.
amplitudenumber | MotionValue<number>-A 0–1 speech level. Stirs the interior while talking. A MotionValue is preferred: it updates outside the render cycle, so a 60fps audio signal never triggers a re-render. For a microphone signal see useAudioAmplitude; for a demo, useSimulatedAmplitude.
classstring-Merged onto the outer element, which also carries relative block.

UI

This component's theme exposes the slots below. Override any of them per instance with the ui prop, or app-wide under ui.aiOrbFaceMouth in app.config.ts.

Prop Type Default Description
uiobject-Per-slot classes merged over the theme defaults.
Slot Props Description
root-relative block
body-absolute
understudy-absolute rounded-full
orb-size-full
face-absolute inset-0 block size-full overflow-visible
mouth-``

Accessibility

  • Pass ariaLabel when the character carries meaning. The SVG then renders as role="img" with a <title>, and the wrapper SVG stops being aria-hidden. Omit it and the whole component is decorative, which is the right default for a purely ornamental orb.
  • The accessible name describes what the character is, not what mood it is in. Mood is visual and fast-changing; announcing it on every transition would be noise. If a mood change matters to a reader, surface it as text nearby.
  • The SVG uses viewBox="0 0 100 100" and overflow-visible, so the sprite scales with the box and the accents are not clipped. Gaze travel is expressed in viewBox units, which means it stays proportional at any size.
  • Blinks are random per instance and never announced; they are read as life, not as an event.
  • Under prefers-reduced-motion: reduce the springs, blinks, saccades, mood loops, accents and the speaking mouth all resolve to their resting state. The face still shows the correct mood, and it is never blank — a static character is a character. A talking face under reduced motion holds an open mouth rather than a closed one, so it still reads as speaking.
  • The moods are never distinguished by colour alone: eye shape, mouth shape and the accent all differ, so the read survives a monochrome or low-contrast theme.

Performance

  • The eyes are painted imperatively rather than animated as motion values. On an SVG node, motion-v promotes style keys to attributes, and the gaze rides in as x and y — the same keys the eye uses for its own geometry, so a spring on the gaze would overwrite the shape it is meant to move. Composing the transform by hand keeps both, and the springs still drive every frame because they notify on change.
  • A mood change is a CSS transition, not a JavaScript tween. The browser runs it while the gaze springs are busy, and it costs no frame budget on the main thread.
  • Every mouth shape is mounted and cross-faded, because an SVG path cannot tween its d through a CSS transition. Cross-fading nine paths is cheaper than interpolating a path string per frame. The speaking mouth is the one shape that moves on its own, and its keyframes are only attached while it is the visible mouth, so the other faces do not run an invisible animation.
  • amplitude is normalised once through useAmplitudeValue, which reads a getter rather than a plain value — a prop read at setup would freeze at mount and a later change would be silently ignored. Orb energy is quantised to four steps before it crosses into a render.
  • A radial-gradient circle sits directly under the shader as a painted understudy. On pages running many WebGL surfaces the browser reclaims the oldest contexts, and a reclaimed orb would leave a bare canvas — which turns the character into a pair of floating eyes. The orb also listens for webglcontextlost and swaps itself for the same gradient, because a lost canvas is not empty: Chrome paints it opaque white. Use painted to skip the context altogether when a page shows many faces at once.
  • The states carry their own saturation, applied as a saturate() filter on the root. A faulted assistant visibly cools down next to the rest of the interface instead of staying cheerful.
Copyright © 2026