p-arndt/mascbob

An animated SVG mascot for Svelte 5 that blinks, reacts to boops and lip-syncs. Zero dependencies.

TypeScript

0

90 commits

updated Sep 30, 2026

See the code

See what people are saying

SourceMessageScoreDate

I made a little mascot for Svelte because apparently everything needs a personality now (r/SideProject)

I kinda wanted a small mascot for some AI stuff, because apparently every AI thing needs a little creature now. Thats why I made mascbob. It's just a Svelte component, pure SVG. It can blink, look around, have different moods, react when you click/pet/tickle it and there's a bunch of customization…

1

Oct 1, 2026

README

mascbob, a capsule-shaped robot mascot that hops through moods and outfits

A friendly, animated and highly customizable SVG mascot for Svelte 5.

A matte, streetwear-styled capsule bot whose face is screen-printed onto its shell, with an accent plate that never quite lines up and halftone cheeks. It blinks, breathes, follows the cursor, reacts to boops, switches moods and lip-syncs to your voice.


Svelte 5 TypeScript Zero dependencies Pure SVG License: MIT

Install · Quick start · Gallery · Props · Voice · Development


Why mascbob?

🎭 Moods that morphEvery mood is one outline that tweens into the next, so faces morph instead of crossfading.
👀 Alive by defaultBlinks, breathes, glances around, follows the cursor and gets bored when you leave it alone.
🫳 Reacts to youPet it, boop it, tickle it, pull its limbs like a puppet. Every reaction fires an event.
🎙️ Lip-syncFeed it a mic level and it talks along, or let it babble on its own.
👟 Dress it upSneaker-drop colorways, head shapes, eye styles, builds, outfits, shoes and accessories.
🪶 Tiny footprintPure SVG + CSS. No runtime dependencies besides svelte. Respects prefers-reduced-motion.

Install

pnpm add mascbob

Quick start

<script>
	import { Mascot } from 'mascbob';
	let mood = $state('idle');
</script>

<Mascot {mood} theme="og" onboop={() => (mood = 'love')} />

It reacts to the pointer: its head tilts toward the cursor, stroke over its head to pet it, circle around it to make it dizzy, boop it over and over to tickle it (keep going once it's grumpy and its head bursts into confetti), grab its head, an arm or a leg and pull it around like a puppet (it stretches like rubber the further you pull and snaps back with a wobble when you let go), or leave it alone until it gets bored. follow, pet, dizzy, tickle, explode, grab and bored are on by default; startle and shy are opt-in. A plain click is still a boop; dragging only starts after a few pixels. The grab event carries the part (one of GRAB_PARTS); in head-only mode (body={false}) only the head can be grabbed:

<Mascot reactions={['pet', 'shy', 'startle']} onreaction={(e) => console.log(e.type)} />

The full figure is 2:3 (size is the width). Dress it with outfit, shoes and build, or use body={false} for a square head-only avatar:

<Mascot outfit="hoodie" shoes="hightops" theme="bred" mood="happy" size={220} />
<Mascot body={false} size={64} />

Moods

Head-only mascots showing the idle, happy, love, surprised, thinking, wink, sleepy, grumpy, sad and laughing moods

idle happy love surprised thinking wink sleepy grumpy sad laughing, plus listening, talking, shy, waving, focused, curious and nervous.

Colorways

The same mascot in every built-in colorway

og volt ice lilac mocha bred noir mint sunset bubblegum forest midnight shadow, or bring your own palette through theme or CSS variables.

Wardrobe

Mascots in overalls, a jersey, a cape, a bowtie, a tie and as a blob, with matching shoes and accessories
<Mascot theme="mocha" build="chubby" outfit="overalls" shoes="boots" accessories={['sprout']} />
<Mascot theme="noir" outfit="tie" shoes="hightops" accessories={['shades']} />
<Mascot theme="bubblegum" build="blob" mood="love" accessories={['halo']} />

Props

The value lists grow over time, so the library exports them. Import MOODS, SHAPES, EYE_STYLES, ACCESSORIES, OUTFITS, SHOES, BUILDS and THEMES (an object keyed by theme name) to see every option or to build your own pickers:

import { MOODS, SHAPES, EYE_STYLES, ACCESSORIES, OUTFITS, SHOES, BUILDS, THEMES } from 'mascbob';
PropTypeDefault
moodone of MOODSidle
themea key of THEMES, or { base?, bodyLight, bodyMid, bodyDark, visor, eye, cheek, accent }og
shapeone of SHAPES (head silhouette)capsule
eyesone of EYE_STYLESround
accessoriesarray of ACCESSORIES[]
bodyfull figure with arms and legs, 2:3 (size is the width); false shows just the headtrue
outfitone of OUTFITS; only visible with bodynone
shoesone of SHOES; only visible with bodynone
buildone of BUILDS (standard, chubby, lanky, chibi, blob); only visible with bodystandard
handsfloating hands that gesture with the mood (head-only mode)true
lookAtpointer wander none or { x, y } in -1..1pointer
levelmouth opening 0..1 while talking (e.g. mic amplitude); omit for automatic lip movement–
sizepx number or any CSS length160
floatidle hover animation (head-only mode; the full figure stands)true
effectsparticles around the head: mood effects (sparkles, hearts, zzz) and boop burststrue
motionauto (respects prefers-reduced-motion), full, reducedauto
interactiverender as a button that reacts to clickstrue
reactionspointer reactions: true (defaults), false, a list of REACTIONS, or { shy: true, bored: false }true
grabhow grabbing feels: { follow, lean }, follow 0..1 (how far the head follows), lean 0..3 (body lean){ follow: 1, lean: 1 }
labelaccessible nameMascbob
onboopclick/tap handler–
onreactioncalled with a ReactionEvent (pet, startle, dizzy, shy, tickle, explode, grab, bored, wake)–
accessorysnippet ({ top, halfWidth }) drawing custom SVG in the head's 200×200 viewBox–

Styling with CSS

Every color is also a CSS variable, which wins over the theme prop:

.brand {
	--mascbob-body-light: #fff;
	--mascbob-body-mid: #ffe1f0;
	--mascbob-body-dark: #ffb3d9;
	--mascbob-visor: #1b1030;
	--mascbob-eye: #00ffc6;
	--mascbob-cheek: #ff7ab8;
	--mascbob-accent: #ff4fd8;
	--mascbob-sprout: #6fdc8c;
}

Custom accessories

<Mascot>
	{#snippet accessory({ top })}
		<circle cx="100" cy={top - 10} r="8" fill="gold" />
	{/snippet}
</Mascot>

Voice / lip-sync

Set mood="talking" and feed an amplitude in 0..1 into level. With the Web Audio API that is a few lines:

<script>
	import { Mascot } from 'mascbob';
	let level = $state(0);

	async function listen() {
		const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
		const ctx = new AudioContext();
		const analyser = ctx.createAnalyser();
		ctx.createMediaStreamSource(stream).connect(analyser);
		const samples = new Float32Array(analyser.fftSize);
		const tick = () => {
			analyser.getFloatTimeDomainData(samples);
			const rms = Math.sqrt(samples.reduce((sum, s) => sum + s * s, 0) / samples.length);
			const target = Math.min(1, Math.max(0, (rms - 0.01) * 7));
			// Open fast, close slower, so the mouth doesn't flicker between syllables.
			level += (target - level) * (target > level ? 0.5 : 0.2);
			requestAnimationFrame(tick);
		};
		tick();
	}
</script>

<button onclick={listen}>Talk</button>
<Mascot mood="talking" {level} />

Stop the stream's tracks and close the AudioContext when you are done. Without level, talking animates the mouth on its own.

Development

  • just lists all recipes
  • just dev starts the showcase at http://localhost:5173
  • just ci runs type check, lint, tests and the package build
  • just readme-art re-renders the images in this README from the real component
  • just release cuts a release

License

MIT

Made with 🧡 and a slightly misaligned print plate.
mascot
svelte
svg

p-arndt/mascbob

An animated SVG mascot for Svelte 5 that blinks, reacts to boops and lip-syncs. Zero dependencies.

TypeScript

0

90 commits

updated Sep 30, 2026

See the code

See what people are saying

SourceMessageScoreDate

I made a little mascot for Svelte because apparently everything needs a personality now (r/SideProject)

I kinda wanted a small mascot for some AI stuff, because apparently every AI thing needs a little creature now. Thats why I made mascbob. It's just a Svelte component, pure SVG. It can blink, look around, have different moods, react when you click/pet/tickle it and there's a bunch of customization…

1

Oct 1, 2026

README

mascbob, a capsule-shaped robot mascot that hops through moods and outfits

A friendly, animated and highly customizable SVG mascot for Svelte 5.

A matte, streetwear-styled capsule bot whose face is screen-printed onto its shell, with an accent plate that never quite lines up and halftone cheeks. It blinks, breathes, follows the cursor, reacts to boops, switches moods and lip-syncs to your voice.


Svelte 5 TypeScript Zero dependencies Pure SVG License: MIT

Install · Quick start · Gallery · Props · Voice · Development


Why mascbob?

🎭 Moods that morphEvery mood is one outline that tweens into the next, so faces morph instead of crossfading.
👀 Alive by defaultBlinks, breathes, glances around, follows the cursor and gets bored when you leave it alone.
🫳 Reacts to youPet it, boop it, tickle it, pull its limbs like a puppet. Every reaction fires an event.
🎙️ Lip-syncFeed it a mic level and it talks along, or let it babble on its own.
👟 Dress it upSneaker-drop colorways, head shapes, eye styles, builds, outfits, shoes and accessories.
🪶 Tiny footprintPure SVG + CSS. No runtime dependencies besides svelte. Respects prefers-reduced-motion.

Install

pnpm add mascbob

Quick start

<script>
	import { Mascot } from 'mascbob';
	let mood = $state('idle');
</script>

<Mascot {mood} theme="og" onboop={() => (mood = 'love')} />

It reacts to the pointer: its head tilts toward the cursor, stroke over its head to pet it, circle around it to make it dizzy, boop it over and over to tickle it (keep going once it's grumpy and its head bursts into confetti), grab its head, an arm or a leg and pull it around like a puppet (it stretches like rubber the further you pull and snaps back with a wobble when you let go), or leave it alone until it gets bored. follow, pet, dizzy, tickle, explode, grab and bored are on by default; startle and shy are opt-in. A plain click is still a boop; dragging only starts after a few pixels. The grab event carries the part (one of GRAB_PARTS); in head-only mode (body={false}) only the head can be grabbed:

<Mascot reactions={['pet', 'shy', 'startle']} onreaction={(e) => console.log(e.type)} />

The full figure is 2:3 (size is the width). Dress it with outfit, shoes and build, or use body={false} for a square head-only avatar:

<Mascot outfit="hoodie" shoes="hightops" theme="bred" mood="happy" size={220} />
<Mascot body={false} size={64} />

Moods

Head-only mascots showing the idle, happy, love, surprised, thinking, wink, sleepy, grumpy, sad and laughing moods

idle happy love surprised thinking wink sleepy grumpy sad laughing, plus listening, talking, shy, waving, focused, curious and nervous.

Colorways

The same mascot in every built-in colorway

og volt ice lilac mocha bred noir mint sunset bubblegum forest midnight shadow, or bring your own palette through theme or CSS variables.

Wardrobe

Mascots in overalls, a jersey, a cape, a bowtie, a tie and as a blob, with matching shoes and accessories
<Mascot theme="mocha" build="chubby" outfit="overalls" shoes="boots" accessories={['sprout']} />
<Mascot theme="noir" outfit="tie" shoes="hightops" accessories={['shades']} />
<Mascot theme="bubblegum" build="blob" mood="love" accessories={['halo']} />

Props

The value lists grow over time, so the library exports them. Import MOODS, SHAPES, EYE_STYLES, ACCESSORIES, OUTFITS, SHOES, BUILDS and THEMES (an object keyed by theme name) to see every option or to build your own pickers:

import { MOODS, SHAPES, EYE_STYLES, ACCESSORIES, OUTFITS, SHOES, BUILDS, THEMES } from 'mascbob';
PropTypeDefault
moodone of MOODSidle
themea key of THEMES, or { base?, bodyLight, bodyMid, bodyDark, visor, eye, cheek, accent }og
shapeone of SHAPES (head silhouette)capsule
eyesone of EYE_STYLESround
accessoriesarray of ACCESSORIES[]
bodyfull figure with arms and legs, 2:3 (size is the width); false shows just the headtrue
outfitone of OUTFITS; only visible with bodynone
shoesone of SHOES; only visible with bodynone
buildone of BUILDS (standard, chubby, lanky, chibi, blob); only visible with bodystandard
handsfloating hands that gesture with the mood (head-only mode)true
lookAtpointer wander none or { x, y } in -1..1pointer
levelmouth opening 0..1 while talking (e.g. mic amplitude); omit for automatic lip movement–
sizepx number or any CSS length160
floatidle hover animation (head-only mode; the full figure stands)true
effectsparticles around the head: mood effects (sparkles, hearts, zzz) and boop burststrue
motionauto (respects prefers-reduced-motion), full, reducedauto
interactiverender as a button that reacts to clickstrue
reactionspointer reactions: true (defaults), false, a list of REACTIONS, or { shy: true, bored: false }true
grabhow grabbing feels: { follow, lean }, follow 0..1 (how far the head follows), lean 0..3 (body lean){ follow: 1, lean: 1 }
labelaccessible nameMascbob
onboopclick/tap handler–
onreactioncalled with a ReactionEvent (pet, startle, dizzy, shy, tickle, explode, grab, bored, wake)–
accessorysnippet ({ top, halfWidth }) drawing custom SVG in the head's 200×200 viewBox–

Styling with CSS

Every color is also a CSS variable, which wins over the theme prop:

.brand {
	--mascbob-body-light: #fff;
	--mascbob-body-mid: #ffe1f0;
	--mascbob-body-dark: #ffb3d9;
	--mascbob-visor: #1b1030;
	--mascbob-eye: #00ffc6;
	--mascbob-cheek: #ff7ab8;
	--mascbob-accent: #ff4fd8;
	--mascbob-sprout: #6fdc8c;
}

Custom accessories

<Mascot>
	{#snippet accessory({ top })}
		<circle cx="100" cy={top - 10} r="8" fill="gold" />
	{/snippet}
</Mascot>

Voice / lip-sync

Set mood="talking" and feed an amplitude in 0..1 into level. With the Web Audio API that is a few lines:

<script>
	import { Mascot } from 'mascbob';
	let level = $state(0);

	async function listen() {
		const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
		const ctx = new AudioContext();
		const analyser = ctx.createAnalyser();
		ctx.createMediaStreamSource(stream).connect(analyser);
		const samples = new Float32Array(analyser.fftSize);
		const tick = () => {
			analyser.getFloatTimeDomainData(samples);
			const rms = Math.sqrt(samples.reduce((sum, s) => sum + s * s, 0) / samples.length);
			const target = Math.min(1, Math.max(0, (rms - 0.01) * 7));
			// Open fast, close slower, so the mouth doesn't flicker between syllables.
			level += (target - level) * (target > level ? 0.5 : 0.2);
			requestAnimationFrame(tick);
		};
		tick();
	}
</script>

<button onclick={listen}>Talk</button>
<Mascot mood="talking" {level} />

Stop the stream's tracks and close the AudioContext when you are done. Without level, talking animates the mouth on its own.

Development

  • just lists all recipes
  • just dev starts the showcase at http://localhost:5173
  • just ci runs type check, lint, tests and the package build
  • just readme-art re-renders the images in this README from the real component
  • just release cuts a release

License

MIT

Made with 🧡 and a slightly misaligned print plate.
mascot
svelte
svg

Languages

TypeScript

51.2%

Svelte

45.8%

CSS

1.2%

Just

1.1%