Blob is a zero-dependency TypeScript companion that lives in the corner of your page,
speaks through a pixel bubble, travels to elements, and morphs around them.
Every option on this page is live: the Blob in the corner reacts to the
Try it buttons in each section.
GETTING STARTED
Call createBlob() after the document body exists.
With no arguments you get an idle, bobbing purple blob in the bottom-right corner —
every option below is optional.
import { createBlob } from 'blob';
const blob = createBlob(); // idle companion, nothing else
blob.say('Hello!'); // one-off speech line
Clicking Blob itself pokes it (a squishy ripple), advances speech while it is talking,
or starts its story if one is waiting. Dragging it is on by default and it springs back home.
ALL OPTIONS
The full BlobOptions object. Everything is optional;
defaults are shown. Each group links to its own section below.
Imperative behavior after mount: subscribe to events, drive the controller. Return a cleanup function if needed.
BODY
The soft body is a ring of spring-driven points. More points mean a smoother outline;
fewer make it lumpier and cheaper.
createBlob({
body: {
color: '#0ea5e9', // any CSS color
size: 32, // resting radius in px (default 48)
points: 48, // physics points around the body, min 3 (default 48)
},
});
PHYSICS
Movement runs on damped springs; idle life comes from a bob and a breathe cycle.
Pass physics: false to turn all motion off — Blob then
positions instantly, which is also what prefers-reduced-motion does automatically.
createBlob({
physics: {
stiffness: 170, // spring stiffness; higher = snappier travel
damping: undefined, // defaults to critical damping for the stiffness
bobAmplitude: 6, // idle up/down float in px
bobFrequency: 1.8, // idle float speed
breatheAmplitude: 0.06,// idle outline wobble, fraction of body size
breatheFrequency: 2.4, // outline wobble speed
pokeStrength: 180, // ripple strength when clicked
landingSquish: 0.25, // squash on arrival, in the travel direction
},
});
createBlob({ physics: false }); // no animation at all
BUBBLE, FONTS & CLASSES
Every visual of the speech bubble is an option. Sizes accept a number (px) or any CSS
length string. maxWidth is automatically clamped to the
viewport, so long lines wrap instead of overflowing small screens.
createBlob({
bubble: {
background: '#ffffff',
color: '#1f1235', // text color
borderColor: '#1f1235',
borderWidth: 4,
shape: 'square', // 'square' | 'rounded' | 'circle' preset
borderRadius: 14, // overrides the shape preset
padding: 12,
fontFamily: "'Blob Pixel', monospace", // bundled pixel font, or any font you load
fontSize: 10,
lineHeight: 1.7,
maxWidth: 280, // clamped to the viewport automatically
shadow: '4px 4px 0 #8b5cf6',
tail: true, // the pointer under the bubble
tailSize: 8,
gap: 16, // px between Blob and the bubble
margin: 8, // min px from the viewport edge
characterDelay: 35, // ms per typed character
autoAdvance: 800, // dwell ms after typing; omit = click to advance
ariaLabel: 'Advance speech',
},
});
createBlob({ bubble: false }); // Blob never speaks visually
Using your own font
Load the font however your page already does (Google Fonts <link>,
@font-face, anything) and name it in
fontFamily. Blob ships with the pixel font
'Blob Pixel' and uses it by default.
circle() expands Blob's whole body into a ring that
wraps an element. The outline works on every renderer — canvas and SVG draw the springy
outline, the CSS renderer draws a hollow bordered box.
blob.circle('#target', {
shape: 'rounded', // 'circle' | 'square' | 'rectangle' | 'rounded'
padding: 14, // px between the target and the outline
radius: 20, // corner radius for 'rounded'
strokeColor: '#f472b6', // defaults to the body color
strokeWidth: 5, // defaults to body size * 0.55 (min 8)
lineCap: 'round',
lineJoin: 'round',
});
blob.detach(); // release and return home
Morph target
Use the buttons to wrap me.
ATTACHMENT
attachTo() makes Blob travel to an element and rest
against one of its edges. It follows the element while the page scrolls or resizes, and
detaches by itself if the target disappears.
blob.attachTo('#target', {
side: 'top', // 'nearest' | 'top' | 'right' | 'bottom' | 'left'
gap: 10, // extra px between Blob and the target
});
Attachment target
Blob rests on the side you pick.
STORY
A story is an ordered list of steps. Fields combine within one step: it sleeps, runs your
code, travels, then speaks. Speech waits for a click to advance unless
bubble.autoAdvance is set.
createBlob({
autoStart: true,
story: [
{ sleep: 600 }, // wait 600 ms
{ say: 'Welcome!' }, // speak a line
{ attachTo: '#nav', attach: { side: 'bottom' }, say: 'Start here.' },
{ run: () => openMenu() }, // your own page action
{ circle: '#projects', morph: { shape: 'rectangle' }, say: 'The work.' },
{ moveTo: { x: 200, y: 300 } }, // viewport coordinate
{ detach: true }, // release, go home
],
});
No-script stories with data attributes
Elements marked with data-blob-order are compiled into
story steps and appended after options.story, ordered by that number.
<section
data-blob-order="1"
data-blob-sleep="500"
data-blob-say="This is the hero section."
data-blob-action="circle"
data-blob-detach>
...
</section>
data-blob-order — position in the story (required).
data-blob-sleep — optional wait in ms before the step.
data-blob-say — line to speak at this element.
data-blob-action="circle" — wrap the element; omit to attach beside it instead.
data-blob-detach — release and go home after this step.
With autoStart, a completed story is remembered per visitor under
storageKey and will not replay on the next visit; an aborted one gets another chance.
CONTROLLER API & EVENTS
createBlob() returns a controller. The
script option receives the same controller right after mount.
Method
What it does
start()
Play the story from the beginning.
pause()
Pause the story after the current step.
skip()
Jump to the end of the story.
say(text)
Speak one line outside the story; resolves when advanced.
moveTo({ x, y })
Travel to a viewport coordinate.
attachTo(target, options?)
Travel to an element (selector or element) and rest on its edge.
circle(target, options?)
Morph into a ring around an element.
detach()
Release the current target and return home.
destroy()
Remove Blob and everything it created from the DOM.
on(event, handler) / off(event, handler)
Subscribe to events (below).
Event
Payload
Fires when
start
—
The story begins.
step
StoryStep
Each story step begins (great for scrolling the page).
say
string
A speech line starts.
attach / circle
HTMLElement
Blob lands on / wraps its target.
detach
—
Blob releases a target.
end
—
The story finishes or is skipped.
dismiss
—
The visitor hides Blob.
warn
string
A non-fatal problem, e.g. a story target no longer exists.
createBlob({
script(blob) {
blob.on('step', (step) => {
if (typeof step.circle === 'string') {
document.querySelector(step.circle)?.scrollIntoView({ behavior: 'smooth' });
}
});
blob.on('end', () => void blob.say('That is the tour!'));
return () => console.log('cleanup on destroy');
},
});
Reusable characters
A character is just an options object. defineBlobCharacter
adds TypeScript autocomplete and nothing else — plain JavaScript works the same.
Three built-ins — try them with the selector in the top bar. canvas2d (default)
and svg draw the full springy outline; css is a lightweight
div-based fallback. All three support the morph ring.
A custom renderer is the escape hatch for a fully bespoke character. It receives a
SoftBodyState every frame — center, perimeter points,
color, and the morph stroke settings — and owns no physics or behavior.
Speech is mirrored to a polite aria-live region, complete lines at a time, so screen readers hear the text without the typewriter noise.
Blob and its bubble are real buttons with accessible labels — keyboard users can focus and advance them; override the labels via labels and bubble.ariaLabel.
prefers-reduced-motion disables all animation by default: Blob positions instantly, speech renders whole lines, and auto-advance dwell is scaled up so lines stay readable. Blob announces this once (customize with reducedMotionNotice).
The dismiss X hides Blob entirely and is remembered per visitor; a small chip restores it.
On slow devices the typewriter reveals by elapsed time, so phrases always finish and are never skipped mid-line.
Back to the live demo ·
Blob is GPL-3.0 and dependency-free.