DOCUMENTATION

Everything Blob can do.

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.

Option Type Default What it does
renderer'canvas2d' | 'svg' | 'css' or a custom object'canvas2d'How the body is drawn. See Renderers.
body{ color, size, points }#8b5cf6, 48, 48Color, resting radius in px, and number of physics points (min 3).
color / sizestring / numbersameShorthand for body.color / body.size; body wins if both are set.
physicsobject | falsesprings onTune springs and idle motion, or false for instant, static positioning. See Physics.
bubbleobject | falsepixel bubbleSpeech bubble style, fonts and timing, or false to mute visuals. See Bubble.
attachment{ gap, side }0, 'nearest'Default resting spot next to attached elements. See Attachment.
morphobjectrounded ringDefault outline drawn when Blob wraps an element. See Morph.
autoStartbooleanfalsePlay the story on load instead of waiting for a click. Plays once per visitor (see storageKey).
draggablebooleantrueLet visitors drag Blob around; it springs back home when released.
glitchbooleanfalseRandom corrupted-signal bursts over Blob's visuals. Disabled under reduced motion.
dismissiblebooleantrueShow an X to hide Blob, persisted per visitor, with a restore chip.
respectReducedMotionbooleantrueDisable animation for visitors with prefers-reduced-motion. See Accessibility.
reducedMotionNoticestring | falsebuilt-in textThe line Blob speaks when reduced motion turns animation off; pass a string to localize or false to stay quiet.
storageKeystring'blob'localStorage namespace for "dismissed" and "story already played".
labels{ guide, dismiss, restore }EnglishAccessible names for Blob's controls; override to localize.
zIndexnumber2147483000Stacking level of Blob's fixed layer.
storyStoryStep[][]The guided tour. See Story.
script(blob) => cleanup?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.

<link href="https://fonts.googleapis.com/css2?family=Inter&display=swap" rel="stylesheet">

createBlob({ bubble: { fontFamily: 'Inter, sans-serif', fontSize: 14 } });

Restyle it entirely with a class

When the built-in options are not enough, pass bubble.className. The class is kept on the bubble element, so your stylesheet can take over completely.

/* your CSS */
.docs-neon-bubble {
  background: linear-gradient(135deg, #0f172a, #1e1b4b) !important;
  color: #a5f3fc !important;
  border: 1px solid #22d3ee !important;
  border-radius: 18px !important;
  box-shadow: 0 0 18px rgba(34, 211, 238, 0.45) !important;
}

createBlob({ bubble: { className: 'docs-neon-bubble' } });

MORPH

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>

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.

MethodWhat 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).
EventPayloadFires when
startThe story begins.
stepStoryStepEach story step begins (great for scrolling the page).
saystringA speech line starts.
attach / circleHTMLElementBlob lands on / wraps its target.
detachBlob releases a target.
endThe story finishes or is skipped.
dismissThe visitor hides Blob.
warnstringA 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.

// characters/guide.ts
import { defineBlobCharacter } from 'blob';
export default defineBlobCharacter({ body: { color: '#0ea5e9' }, story: [ /* ... */ ] });

// app.ts
import { createBlob } from 'blob';
import guide from './characters/guide';
createBlob(guide);

RENDERERS

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.

const sprite = {
  mount(host: HTMLElement) { /* create your DOM/canvas inside host */ },
  render(state: SoftBodyState) {
    // state.center, state.points, state.color,
    // state.shape ('solid' | 'ring'), state.strokeWidth, state.strokeColor,
    // state.morphShape, state.morphRadius
  },
  resize() { /* optional viewport-resize hook */ },
  destroy() { /* remove everything mount() created */ },
};

createBlob({ renderer: sprite });

ACCESSIBILITY

Back to the live demo · Blob is GPL-3.0 and dependency-free.