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 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.
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.
pnpm add @nuton/ui @nuton/tokens @remixicon/reactnpm install @nuton/ui @nuton/tokens @remixicon/reactyarn add @nuton/ui @nuton/tokens @remixicon/reactimport '@nuton/tokens/tokens.css'
import '@nuton/ui/styles.css'Usage
import { Nuri } from '@nuton/ui'
<Nuri emotion="encouraging" size={64} />// Each new correct answer gets its own sparkle, even if Nuri is already happy.
<Nuri emotion="happy" replayKey={answer.id} /><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
decorativewhen 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
replayKeywhen the moment happens again (another right answer); don't replay on a timer. - Turn on
speakingonly 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
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
<Nuri size={32} />
<Nuri size={48} />
<Nuri size={64} />
<Nuri size={96} />
<Nuri size={128} />
<Nuri size={160} />| Size | Height | Good for |
|---|---|---|
| 32 | space-8 | Beside a message in the chat list. |
| 48 | space-12 | Panel header, small cards. |
| 64 | space-16 | Inline feedback after an answer. |
| 96 | space-12 × 2 | Default. Empty states, welcome. |
| 128 | space-16 × 2 | Onboarding steps. |
| 160 | space-20 × 2 | Celebration 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.
replayKey changes, then fades away. Thinking and Sleepy repeat a few times, because they describe a state, and then rest.animated={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.| Motion | Layer | Timing |
|---|---|---|
| Breathe | Nuton_Root, from the feet | 3.2 s each way, ease in-out |
| Head tilt ±1.5° | Head_Rig, about the neck | 6.4 s each way |
| Wing sway 3° | Wing-L, Wing-R, about the shoulders | 3.2 s each way |
| Blink | Open eye | Every 5.2 s, about 150 ms closed |
| Glance | Pupil rig / movable, inside the sclera clip | 9 s loop, quick moves and long holds |
| Talk | Lower beak, Mouth interior | 0.9 s uneven loop |
| New expression | Whole drawing | Duration/Slower (500 ms), Ease/Emphasized |
| Accent | Its own layer over the drawing | Once, 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?
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>| Moment | What Nuri does |
|---|---|
| Pointer moves over the panel | Pupils 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 s | Eyes drift back to the learner. |
| Learner types | Looks calmly at the text box and keeps blinking. It does not follow keystrokes. |
| Tutor thinking | Looks up and to one side, switching sides every couple of seconds, until the reply is ready. |
| Reply appears | Looks 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. |
| Idle | Blinks, and every few seconds glances a little away and back. |
| Tap on a touch screen | Glances 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 Nuri | A wiggle, a blink and a small wave. Taps during it are ignored, so they never stack. |
| Scrolling | Gaze holds still; it moves again on the next real pointer move. |
Empty chat
Feedback after an answer
API reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
emotion | "idle" | "happy" | "excited" | "curious" | "thinking" | "confused" | "surprised" | "proud" | "encouraging" | "sad" | "frustrated" | "sleepy" | "celebrating" | "idle" | Which expression to show. |
size | 32 | 48 | 64 | 96 | 128 | 160 | 96 | Height in px. Width follows. |
label | string | "Nuri, <emotion>" | Accessible name. Pass a translated string for other languages. |
decorative | boolean | false | Hides Nuri from assistive technology. Use when nearby text already carries the meaning. |
animated | boolean | true | Breathing, blinking, glancing and small head and wing movements. Always off under reduced motion. |
speaking | boolean | false | Moves the beak as if talking, e.g. while a reply streams in. |
effects | boolean | true | The accent each expression brings (sparkles, a question mark, Zs…). Plays once, then fades. Needs animated. |
replayKey | string | number | — | Change it to play the accent again without changing the expression, e.g. the id of each new correct answer. |
loop | boolean | false | Repeats 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
| Prop | Type | Default | Description |
|---|---|---|---|
area | RefObject<HTMLElement> | the page | Where 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 | null | last assistant message | Where the new reply is, for the look when state becomes "responding". |
enabled | boolean | true | Turns 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
| Key | Behaviour |
|---|---|
| Tab | Nuri 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
| Figma | Code |
|---|---|
| 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 clip | Used, 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 variants | Not 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.