@klex/mascot
v0.6.1
Published
The animated Klex mascot: a React component, its GSAP motion engine, body presets, and SVG helpers.
Readme
@klex/mascot
The animated Klex mascot: a React component, its GSAP motion engine, body presets, and dependency-free SVG helpers. Klex Cloud and the Klex website render the same character from this package.
Install
pnpm add @klex/mascot @stagewise/ui gsap motion react react-domreact and react-dom (19.2+), motion, gsap, and @stagewise/ui are peer dependencies. The package ships TypeScript and CSS sources, so the app's bundler (Vite or similar) compiles them. Component CSS is imported by the modules themselves.
The component uses Tailwind utilities and @stagewise/ui theme tokens. Import the @stagewise/ui stylesheet and let Tailwind scan the package source. @source takes a path relative to the CSS file:
@import "tailwindcss";
@import "@stagewise/ui/style.css";
@source "../node_modules/@klex/mascot/src";Public API
| Export | Purpose |
| --- | --- |
| Klex, KlexHandle, KlexProps, EmoteId | The animated character and its ref commands. |
| BODY_SHAPES, BodyShape, PALETTES, KlexPalette | Built-in silhouettes and colors. |
| DEFAULT_GLIDE, MOVEMENT_STYLES, MovementMode | Movement settings for crawl, fly, and hop. |
| ACTIVITIES, KlexActivity | Resting activities: idle, sleeping, working, calling, recalling, learning. |
| EMOTES, EMOTE_CATEGORIES, Emote, emotePose, REST_EMOTE | Emote catalog and poses. |
| EXPRESSIONS, Expression, KlexEyes | Facial expressions and the eye renderer. |
| EXTRAS, Extra, KlexExtras | Extras such as wave, phone, sparkles, sleep, and thought. |
| KlexGroundGrid | Perspective floor grid for scenes. |
| klexBody, KlexPosition, fitGaze, gazeRoom, waveAnchor, phoneAnchor | Geometry helpers for editors and static previews. |
| observeVisibility | Shared IntersectionObserver that Klex uses to pause offscreen characters. |
| popTransition | The spring used by bubbles and extras. |
| roundedPath, getKlexEyeColor, klexSvg | Static SVG output. Also available from @klex/mascot/svg without React, GSAP, or a DOM. |
Usage
Templates and saved drawings both supply a BodyShape with an outline and a face anchor in the shared 160 × 140 coordinate frame.
import { useRef } from 'react';
import { BODY_SHAPES, Klex, type KlexHandle } from '@klex/mascot';
function Mascot() {
const klex = useRef<KlexHandle>(null);
return (
<>
<Klex
ref={klex}
shape={BODY_SHAPES[0]} // Or a saved drawing.shape
color="#ffbd91"
size={80}
width="100%"
initialExpression="neutral"
/>
<button onClick={() => klex.current?.moveTo('right', 'crawl')}>
Come over
</button>
<button onClick={() => klex.current?.emote('jump')}>Jump</button>
<button onClick={() => klex.current?.setExpression('happy')}>Happy</button>
<button onClick={() => klex.current?.setExtra('sparkles')}>Sparkles</button>
</>
);
}Props and sizing
shape, color and size can change without restarting an animation. The eye color is always derived from the body color to maintain contrast. The component defaults to the original white Klex at 160 pixels.
The component reserves size pixels of height. width sets the whole movement area and should be at least size. Without width, the component is a square avatar with no sideways travel:
<Klex ref={avatar} shape={savedShape} color="#d9c4ff" size={40} />For a 2D area, place the track along its bottom edge. Give moveTo an x value from -50 at the left edge to 50 at the right edge, plus a y pixel offset from the ground line. Negative y moves the character higher in the area. initialPosition places it before the first animation. The body keeps its crawl, hop, or fly motion while moving diagonally.
When several characters share an area, use onDepthChange to set each track wrapper's z-index. Higher y values are closer to the viewer. The callback reports the ground position, so jumping and flying do not change which character stands in front.
<Klex ref={avatar} width="100%" initialPosition={{ x: -30, y: -40 }} />
avatar.current?.moveTo({ x: 20, y: -100 }, 'crawl');Use snapTo({ x, y }) to restore a scene position immediately. It cancels the current trip without firing arrival or completion callbacks. face('left' | 'right') sets the direction without moving. For responsive layouts, shiftBy({ x, y }) offsets both the current position and destination while preserving travel, airborne hops, and callback timing. Its x offset uses track percentage points; y uses pixels.
Custom outlines can start at any contour point. Turning aligns the mirrored contour before blending it with the original, keeping custom silhouettes from collapsing during a direction change.
name adds a badge above the character and supplies its accessible name. An optional role appears on a second line, for example <Klex name="Kristine" role="Product Manager" />. Omit name to hide the badge.
Emotes, including jumps and shaking, scale down with the avatar. The ground is the bottom edge of the component. Overflow stays visible for jumping and extras; a parent can clip it with overflow-hidden when a strict avatar crop is needed.
idle defaults to true. Resting characters breathe gently, sway their heads, blink and occasionally glance around without changing their expression or position. Every instance has its own breathing pace and independently randomized blink and glance pauses. Each glance also varies in direction and duration. The idle pose fades out for travel and emotes, then returns automatically. Flying characters keep their hover. Use idle={false} for static avatars or long lists that do not need continuous animation.
activity selects a persistent animation and defaults to 'idle'. 'sleeping' closes the eyes, lowers and tilts the head, and breathes slowly. 'calling' holds a bundled Twemoji 📞 SVG against the head, mirrored toward the face and held by a small hand in the body color. A fixed graphic avoids platform-dependent emoji font shapes and baselines. The earpiece follows the deformed silhouette slightly above the eyes, including custom shapes. Each avatar alternates attentive listening, irregular conversational emphasis, and a friendly double nod, with natural blinks and pauses. These are ambient gestures, not speech detection; reduced motion keeps a static listening pose. No laptop is shown. 'working' places a laptop in front of the character, with its back facing the viewer. Each avatar independently alternates between reading across successive lines, typing in irregular bursts, and looking up to think with a question mark overhead. Phase order, durations, and pauses are randomized anew throughout the animation. 'recalling' shows a thought bubble with pulsing dots above the head. The character alternates quiet pondering toward the bubble, quick searching glances, and a short happy nod; these are ambient gestures and never claim a memory was found. 'learning' holds an open book and writes into it with a pencil. Ink fills the right-hand page line by line in uneven bursts, the pen lifts while returning to the start of a line, and the character pauses to reread or think. A full page turns over and writing starts on a fresh page. Travel and emotes take precedence, then the latest activity resumes. A changed activity blends in, so an awake avatar closes its eyes as it falls asleep; a freshly mounted one starts in its activity's pose, and held props still spring in. Pass activityPending while activity is only a placeholder for a status that is still loading: the first real activity shows at once as well. Once one has shown, every change blends, including a return to a placeholder. idle={false} disables only the default idle animation; an explicit sleeping, working, calling, recalling or learning activity still runs.
<Klex activity="sleeping" size={40} />
<Klex activity="working" size={40} />
<Klex activity="calling" size={40} />
<Klex activity="recalling" size={40} />
<Klex activity="learning" size={40} />Change the prop to switch activities without remounting the character. These are visual states; the component does not fetch agent status.
Sleeping automatically shows three rising Zs that drift, grow and fade in a staggered loop. They pause with the character and become a static trail with reduced motion. extrasEnabled={false} hides them. The same animation is available separately as the sleep extra.
The minimal, flat-colored laptop floats in front of the body and gently bobs. During typing it settles into a steady hold with small taps, then smoothly resumes floating. It scales with the avatar, adapts to the face height, springs into view, and shrinks away when working stops. extrasEnabled={false} hides it and the thinking question mark. Pause freezes the phase, laptop, and gaze together; reduced motion switches visibility immediately and uses a static working pose without phase changes or a question mark.
initialExpression and initialExtra are starting values. Later changes go through the ref methods. Reaction emotes temporarily override this base look, then restore the latest chosen values. extrasEnabled={false} hides both the selected extra and reaction extras.
settings accepts advanced movement settings such as MOVEMENT_STYLES[n].settings. A new settings object replaces a mode previously chosen by moveTo. Ordinary appearance updates preserve that mode. movementMode locks a character to crawl, fly, or hop: it picks that style's settings when settings is omitted, and moveTo ignores its mode argument.
Inflated look (experimental)
appearance="inflated" shades the body as a soft 3D form. The engine's deformed outline is rasterized each frame, a jump-flood distance field turns it into a height map, and a fragment shader lights it. Eyes, extras and the laptop stay SVG overlays. All characters on a page share one WebGL2 context and atlas, and each copies its tile into its own canvas, so z-order and clipping keep working.
<Klex appearance="inflated" material="plush" />
<Klex appearance="inflated" material="jelly" inflate={{ puffiness: 0.9, edgeProfile: 'gummy' }} />In this look the character behaves like a real 3D body: walking the other way turns it round with its nose towards the viewer, gaze turns mostly the head, "Turn away" shows its other side and "Spin" makes a full turn. Both sides carry the face. turn={{ yaw, pitch, headYaw, headPitch, lookAround }} (degrees) adds a turn of the whole body or only the head, and lookAround adds slow glances. material is plush, rubber or jelly. Shapes can set puffiness (0 = cookie, 1 = marshmallow), edgeProfile (pillow, dome, gummy) and up to four bumps; inflate overrides them per character. Without WebGL2 float render targets, with reduced motion, or in the svg layout, Klex falls back to the flat SVG. klexSvg is unaffected.
Klex Cloud compares the looks, materials and form settings at /dev/klex.
Scenes
Characters pause while offscreen. When a scene clips its characters, mark the clipping element with data-klex-viewport; every Klex inside then follows that element's visibility, so characters entering from outside the visible area keep animating.
layout="svg" renders the character as native SVG groups for a parent <svg> scene, in the same 160-unit coordinate frame as BODY_SHAPES. Position and scale it with a transform on a wrapping <g>. WebKit mispaints transformed HTML inside foreignObject, so SVG scenes should use this layout instead of embedding the HTML layouts. It omits the name badge, speech bubbles, and children; size and width do not apply. The figure has no track width, so keep x at 0 and move it with the wrapping <g>; y offsets use SVG units.
<svg viewBox="0 0 800 400" data-klex-viewport="">
<g transform="translate(320 180) scale(0.8)">
<Klex layout="svg" activity="working" movementMode="fly" />
</g>
</svg>Ref methods
| Method | Behavior |
| --- | --- |
| moveTo('left', 'crawl') | Move to the left edge of this component's track. |
| moveTo('right', 'fly') | Move to the right edge and keep hovering there. |
| moveTo(0, 'hop') | Hop to the center. Numbers range from -50 to 50 and are clamped. |
| snapTo({ x: 20, y: -100 }) | Set the position immediately and cancel the current trip. |
| shiftBy({ x: 5, y: -20 }) | Offset position and destination without interrupting travel. |
| face('left') | Set the direction without moving. |
| stopMoving() | Stop at the current position without firing onMoveComplete. |
| emote('jump') | Play one emote. Returns false if another is active or reduced motion is enabled. |
| setExpression('angry') | Change the persistent face. |
| setExtra('question') | Show one extra. setExtra('none') hides it. |
| cancelEmote() | End the emote and restore the base look. Does not cancel travel. |
| pause() / resume() | Freeze or continue idle, travel, hovering and the active emote. |
Movement modes are crawl, fly, and hop. Omitting the mode keeps the current one. A new destination replaces the previous destination, preserving momentum. Movement and emotes can run together. Accepted moveTo and emote commands also resume a paused character.
All emotes, expressions and extras are typed IDs. The catalogs are EMOTES, EXPRESSIONS, and EXTRAS. For example, shiver, wink, look-left, look-right, look-up, look-down, and thanks are emotes; happy, angry, sleepy, and hearts are expressions.
onMoveArrival fires once near the latest destination, before the body finishes settling, so a conversation or scene action can begin promptly. onMoveComplete fires after settling. Both fire without animated travel when reduced motion is enabled; stopping or snapping cancels pending callbacks. onEmoteComplete fires when an emote finishes or is cancelled. No callbacks fire after unmount. Each component owns one controller for activities, movement and emotes and releases its ticker on pause or unmount. Grounded characters with idle={false} and no sleeping, working, calling, recalling or learning activity release the ticker once their actions settle.
Speech bubbles animate out and back in when their text changes. Bubbles and extras share the same spring transition. KlexGroundGrid accepts optional fade values, so its parent can own the mask, and floor adjusts its geometry and line thickness for a plane tilted back by 60 degrees.
The component respects the system's reduced-motion preference: idle and emotes are skipped and destinations are applied without animated travel. Sleeping, working, calling, recalling and learning retain a static pose without a continuous ticker. Facial expressions and extras remain available.
License
MIT. The phone extra embeds the Twemoji v17.0.3 telephone receiver (U+1F4DE), © Twitter, Inc. and other contributors, licensed under CC BY 4.0 and included unchanged apart from its placement. See LICENSE-TWEMOJI.txt.
