Nuri

Nuri is the Nuton tutor's character, a small blue bird with 13 expressions. Use it to give the AI tutor a face that matches what it is doing.

  • Beta
  • Accessibility
  • Figma Nuton Animation Asset Pack, Emotional expressions 4529:41481
  • packages/ui/src/components/Nuri

Beta until design says which expression goes with which moment (C79) and approves the motion, which is code-only (C80). The artwork itself is final and unchanged.

Preview

Pick an expression and a size, and turn on speaking. Changing the expression plays a small squash and settle; the box stays the same size, so the layout never jumps.

tsx
import { Nuri } from '@nuton/ui'

<Nuri emotion="happy" size={128} />

Installation

Availability

@nuton/ui and @nuton/tokens are private workspace packages and aren't published yet (D4, D12). Inside the Nuton monorepo, add them as workspace dependencies.

bash
pnpm add @nuton/ui @nuton/tokens @remixicon/react
tsx
import '@nuton/tokens/tokens.css'
import '@nuton/ui/styles.css'

Usage

tsx
import { Nuri } from '@nuton/ui'

<Nuri emotion="encouraging" size={64} />
tsx
// Each new correct answer gets its own sparkle, even if Nuri is already happy.
<Nuri emotion="happy" replayKey={answer.id} />
tsx
<Nuri emotion="thinking" decorative />
<p>Nuri is working on your answer…</p>
  • Choose the expression for what the tutor is doing, not for decoration. The table in Variants lists a suggested use for each.
  • Keep it beside the words it reacts to. Nuri adds feeling; the message still has to say everything on its own.
  • Use decorative when the text next to Nuri already says how it feels, so screen readers do not hear it twice.
  • Accents mark a moment. Pass a new replayKey when the moment happens again (another right answer); don't replay on a timer.
  • Turn on speaking only while the tutor's reply is arriving, and turn it off when the text is complete.
  • Pass animated={false} where many Nuris sit together, such as a history list, so only the current one moves.
  • Show Nuri on a light or mid surface. The artwork keeps its own colours in dark mode.

Variants and sizes

Thirteen expressions and six sizes. The drawings come from the Figma pack and are not changed in code.

Expressions

IdleResting between turns, waiting for the learner. No accent: resting is not a moment.
HappyA correct answer, a warm greeting. Sparkles twinkle.
ExcitedA new topic opens, a streak starts. Motion lines burst out.
CuriousAsking a follow-up, inviting a question. Perk marks flick up.
ThinkingWorking on a reply. Thought bubbles rise and the dots pulse.
ConfusedCould not understand the question, asking to rephrase. A question mark pops up.
SurprisedAn unexpected result or a big jump in score. An exclamation mark jumps.
ProudMastering a lesson or reaching a milestone. A star shines.
EncouragingAfter a wrong answer: try again, you are close. A heart floats up.
SadA streak lost or an exam missed. A tear runs down.
FrustratedRepeated errors. Use rarely, and never aimed at the learner. Stress marks flare.
SleepyIdle for a long time, or a late-night session. Zs drift away.
CelebratingFinishing a course or a perfect quiz. Confetti bursts.

The uses above are suggestions. The Figma pack names the expressions but does not say when to use them (C79). The accents are code-drawn, not from the pack (C81).

Sizes

SizeHeightGood for
32space-8Beside a message in the chat list.
48space-12Panel header, small cards.
64space-16Inline feedback after an answer.
96space-12 × 2Default. Empty states, welcome.
128space-16 × 2Onboarding steps.
160space-20 × 2Celebration and full-screen moments.

Width follows height at 343 : 409. Below 32 the face stops reading, so there is no smaller size.

States and interactions

Nuri moves its own Figma layers: the drawing is never redrawn or swapped mid-motion. Every movement is small and slow, so it reads as alive without pulling the eye from the lesson.

Resting (default)Breathes from the feet, tilts its head about the neck, sways its wings about the shoulders, blinks every few seconds and glances around. Each Nuri starts at a different point, so a page of them never moves in step.
SpeakingThe lower beak drops and the mouth opens in an uneven rhythm. Use it while a reply streams in.
Big momentsExcited and Celebrating flap three times when shown, and Celebrating hops; then they settle into the resting loop.
Per expressionSleepy breathes slowly and dozes forward; Thinking and Confused tilt and hold; Sad slows down. Thinking, Surprised and Confused keep the gaze the artist drew; Encouraging does not blink, because it winks.
AccentEach expression except Idle brings a small accent (Variants lists them). It plays when the expression appears or replayKey changes, then fades away. Thinking and Sleepy repeat a few times, because they describe a state, and then rest.
Stillanimated={false}: nothing moves and there are no accents. With reduced motion, Nuri is still and each accent only fades in and out where it stands.
MotionLayerTiming
BreatheNuton_Root, from the feet3.2 s each way, ease in-out
Head tilt ±1.5°Head_Rig, about the neck6.4 s each way
Wing sway 3°Wing-L, Wing-R, about the shoulders3.2 s each way
BlinkOpen eyeEvery 5.2 s, about 150 ms closed
GlancePupil rig / movable, inside the sclera clip9 s loop, quick moves and long holds
TalkLower beak, Mouth interior0.9 s uneven loop
New expressionWhole drawingDuration/Slower (500 ms), Ease/Emphasized
AccentIts own layer over the drawingOnce, 1.2–2.6 s, then gone. Thinking 3 cycles of 2.4 s, Sleepy 2 rounds of 3 s, Sad 2 tears

Composition examples

In the AI Tutor: attentive Nuri

useNuriAttention makes Nuri pay attention to the learner. Put Nuri where it is always in view (here, above the composer; in the welcome before the first question) and tell the hook what the tutor is doing.

Chapter 2 · Business structures

Move the pointer over the tutor, type a question, or tap Nuri. On a phone or tablet, tap anywhere in the panel.

Nuri, your AI tutor

Here to help

Hey Ada 👋

I've read this whole chapter with you. What's not clicking yet?

tsx
import { ChatPanel, ChatComposer, Nuri, useNuriAttention } from '@nuton/ui'

const panel = useRef<HTMLElement>(null)
// 'thinking' while the reply is generated, 'responding' when it appears, then 'idle'.
const nuri = useNuriAttention({ area: panel, state: tutorState })

<ChatPanel ref={panel} footer={<><Nuri ref={nuri} emotion={emotion} size={64} decorative /><ChatComposer … /></>}>
  …
</ChatPanel>
MomentWhat Nuri does
Pointer moves over the panelPupils follow it inside the eye whites; the head leans in up to 2.5°. Far points ease out instead of snapping.
Pointer leaves, or rests for 4 sEyes drift back to the learner.
Learner typesLooks calmly at the text box and keeps blinking. It does not follow keystrokes.
Tutor thinkingLooks up and to one side, switching sides every couple of seconds, until the reply is ready.
Reply appearsLooks at the new reply for 1.6 s, then back at the learner.
Expression changes (celebrating, encouraging…)Faces the learner for 1.5 s while the accent plays, then carries on.
IdleBlinks, and every few seconds glances a little away and back.
Tap on a touch screenGlances at the tap for 0.9 s. Taps in text fields are ignored, and taps closer than 0.4 s apart count once.
Tap on NuriA wiggle, a blink and a small wave. Taps during it are ignored, so they never stack.
ScrollingGaze holds still; it moves again on the next real pointer move.

Empty chat

Ask me anything about this lesson.

Feedback after an answer

Not quite. Look at the second step again.

API reference

Props

Nuri props
PropTypeDefaultDescription
emotion"idle" | "happy" | "excited" | "curious" | "thinking" | "confused" | "surprised" | "proud" | "encouraging" | "sad" | "frustrated" | "sleepy" | "celebrating""idle"Which expression to show.
size32 | 48 | 64 | 96 | 128 | 16096Height in px. Width follows.
labelstring"Nuri, <emotion>"Accessible name. Pass a translated string for other languages.
decorativebooleanfalseHides Nuri from assistive technology. Use when nearby text already carries the meaning.
animatedbooleantrueBreathing, blinking, glancing and small head and wing movements. Always off under reduced motion.
speakingbooleanfalseMoves the beak as if talking, e.g. while a reply streams in.
effectsbooleantrueThe accent each expression brings (sparkles, a question mark, Zs…). Plays once, then fades. Needs animated.
replayKeystring | number—Change it to play the accent again without changing the expression, e.g. the id of each new correct answer.
loopbooleanfalseRepeats the accent for as long as the expression shows, instead of once. Used beside quiz verdicts and on the score card. Needs animated; with reduced motion it stays one gentle fade.

useNuriAttention

useNuriAttention options
PropTypeDefaultDescription
areaRefObject<HTMLElement>the pageWhere the pointer is followed and taps are noticed. Usually the ChatPanel.
state"idle" | "thinking" | "responding""idle"What the tutor is doing. Typing is detected from focus in a text field inside area.
responseTarget() => Element | nulllast assistant messageWhere the new reply is, for the look when state becomes "responding".
enabledbooleantrueTurns the behaviour off without unmounting.

It returns a ref for <Nuri ref={…} />. It never re-renders the app: it writes three CSS variables on Nuri and runs its frame loop only while the eyes are moving. It does nothing with reduced motion or when Nuri is not animated.

Other span attributes (className, style, data-*) are passed through. role is set by the component.

Accessibility

Keyboard

KeyBehaviour
TabNuri is not interactive and is skipped. Wrap it in a Button or Link if it should be pressable.

Screen readers

An image named "Nuri, happy" and so on. The drawing inside is hidden from assistive technology. With decorative, Nuri is not announced at all.

Focus

Nothing to focus. A focus ring comes from the Button or Link around it.

Contrast and colour

Nuri is an illustration, so text contrast rules do not apply to it. The expression is never the only way to learn something: the message beside it must say the same thing in words.

Touch and pointer

Not a target. When wrapped in a control, the control must be at least 24 × 24 and, in the app, 44 × 44.

Attention

The hook only listens (passive listeners, no preventDefault), so text selection, scrolling and chat controls work as before. Touch screens get tap glances, never a fake cursor.

Motion

Under prefers-reduced-motion Nuri stops moving and accents only fade in and out. Nothing flashes, accents end by themselves, and none carries meaning the text does not.

You must: translate label when the interface is not in English, use decorative when the text beside Nuri already says how it feels, and turn speaking off when the reply is complete.

Figma mapping

FigmaCode
Emotion = Idle, Happy, Excited, Curious, Thinking, Confused, Surprised, Proud, Encouraging, Sad, Frustrated, Sleepy, Celebrating (13 components, 340–343 × 400)emotion, same names in lower case. Artwork is the Figma export with only its ids rewritten (C78).
Layer names (Nuton_Root, Head_Rig, Wing-L, Wing-R, Open eye, Pupil rig / movable, Beak_Rig)Kept as data-layer; the motion moves these layers (C80).
Rive notes: head pivot, shoulders, pupils inside the sclera clipUsed, rescaled to the 343 × 409 box and found from the art (NURI_RIG). Lower-jaw rotation is a drop and mouth stretch instead.
Separated parts, wing poses, eye gazes, beak and mouth variantsNot used. Swapping parts would need them redrawn to match each expression (C80).

Guidelines and related components

Use Nuri for

  • Giving the AI tutor a recognisable face in chat, empty states and feedback.
  • Marking moments in a lesson: a right answer, a streak, a finished course.

Don't use Nuri for

  • A person's profile picture. Use Avatar.
  • Errors and warnings. Use Alert, so the status is not left to a drawing.
  • Decoration on pages with no tutor, or as a logo.

Do and don't

DoMatch the expression to the moment Encouraging after a wrong answer tells the learner to keep going.
Don'tShow a negative face at the learner Frustrated or sad beside a wrong answer reads as blame. Keep those for the tutor's own moments.