Build with Dots.
One swarm. Shapes, solid interfaces, and the motion between them. Start with a component, or let your coding agent handle the integration.
Installation
Install the core package in your React app. Use the equivalent command if your project runs pnpm, Yarn, or Bun.
npm install dots-swarmReact and React DOM 18 or 19 are peer dependencies. The UI entry point uses Radix; the particle renderer does not. APIs are currently alpha, so keep your lockfile and use the types shipped with your installed version.
For UI primitives, import dots-swarm/ui.css once in your app entry or Next.js root layout. Put interactive examples behind a "use client" boundary and give each swarm an explicit height.
Installing ProPrivate registry · $149 USD once
Purchase Pro + Studio, then create an install token in your account. Add these lines to your project’s .npmrc, keeping any existing settings:
@dots-swarm:registry=https://dotsui.dev/api/registry/
//dotsui.dev/api/registry/:_authToken=${DOTS_TOKEN}Set DOTS_TOKEN privately in your shell or CI secrets, then install:
npm install dots-swarm @dots-swarm/proCommit the placeholder .npmrc and lockfile, never the token. Only the @dots-swarm scope uses our registry. Other packages still come from npm. Tokens can be rotated from your account; deployed animations do not contact a license server.
Describe the behavior. Keep control.
The skill teaches your agent the package boundaries and integration patterns. A copied prompt brings the exact effect you picked. Use either, or both.
Install the playbook
Run this from your project and select your coding agent. The skill covers Free and Pro; it doesn’t grant access to the private package.
npx skills add hamza-al/dots-next --skill dotsuiGive it a job, not just an effect
Name the component, the real events that drive it, and what should stay the same. Here’s a brief you can adapt:
Use the Dots skill to replace my upload spinner with a particle swarm. Use cloud while idle, loader during the real upload, and check after the request succeeds. Keep a visible error message if it fails. Keep the same DotSwarm mounted and use our existing theme colors and layout. Use the free dots-swarm package. Keep the upload logic unchanged. Respect reduced motion and preserve accessible status text.
Keep the handoff small
Use Copy prompt on a shape, primitive, demo, or Studio export. It includes the install command, relevant APIs, and your selected configuration. Custom SVG artwork stays in a separate asset file.
- Read the installed types before adding props or inventing shape names.
- Keep one swarm mounted; preserve real form handlers, status, and focus.
- Use named sequences for built-in shapes. Use separate assets for custom geometry.
- Respect reduced motion and the project’s verification preferences.
Machine-readable references
All resourcesRead only what the task needs. Visit the Skill page for live examples and ready-to-copy prompts.
Your first swarm.
Changing shape starts a morph. Keep the same component mounted so particles retain their positions and momentum.
"use client";
import { useState } from "react";
import { DotSwarm, type Shape } from "dots-swarm";
export default function FirstSwarm() {
const [shape, setShape] = useState<Shape>("cloud");
return <>
<DotSwarm shape={shape} count={900} color="#eeeeee"
transitionDuration={1.4}
style={{ width: "100%", height: 320 }}
label={shape === "cloud" ? "Particle cloud" : "Check mark"} />
<button type="button" onClick={() =>
setShape(current => current === "cloud" ? "check" : "cloud")
}>
{shape === "cloud" ? "Make a check mark" : "Back to cloud"}
</button>
DotSwarm reference
| Prop | Default | What it does |
|---|---|---|
shape | "cloud" | One of the 53 built-in formations. |
points | undefined | Custom Point[] geometry; takes priority over shape. Memoize arrays to avoid unnecessary rematching. |
motion | undefined | For custom points: rotate, swirl, wave, pulse, or orbit. |
transitionDuration | 2.4 | Morph duration in seconds at 1× speed. Use 0 when an external animation already interpolates points. |
count | 900 | Particle count. Clamped to 24–2,400. |
color | "#eef0e9" | Any Canvas-compatible CSS color. |
speed | 1 | Animation speed, from 0.1× to 3×. |
choreography | "flow" | "flow", "scatter", or "direct". |
dotSize | 1.25 | Dot radius in CSS pixels, scaled for small canvases. Range 0.4–4. |
spread | 1 | Formation scale, from 0.4 to 1.5. |
interactive | false | Particles respond to a nearby pointer. |
paused | false | Freeze the animation. A new selected shape still renders. |
reducedMotion | false | Force a static pose. The OS preference is always respected. |
label | Auto-generated | Accessible description of the canvas. |
style / className | — | Size and style the canvas without a stylesheet dependency. |
onTransitionChange | undefined | Receives true when a morph begins and false after settling. |
Shape catalog
cloudorbhalohelixinfinityatomflowerboltmoonequalizerarrowglobeloadercheckheartwavegridsparkrouteturn-leftturn-rightstraightu-turnroundaboutarrivepincurrent-locationcompassmapcarwalkinglatticeframe-tunnelwire-spherelayer-stackmicrophoneheadphonesthought-bubblecalendartodo-listnotemessagemailsendplaypausemusiccartcredit-cardpackagebar-chartline-chartlockNeed the raw coordinates? Import getShapePoints from dots-swarm/shapes. It takes a shape, particle count, and optional animation time in seconds.
Timed sequences
DotsSequence describes the journey as data: named shapes or UI scenes, individual clip lengths, transition paths, and appearance. Studio exports a short TypeScript module or JSON definition. The package owns the clock and the renderer owns the particles.
"use client";
import { useMemo } from "react";
import { DotsSequence, DotsSequencePlayer } from "dots-swarm";
export function AssistantLoop() {
const sequence = useMemo(() => new DotsSequence([
{ shape: "halo", duration: 4 },
{ shape: "heart", duration: 6, color: "#ffa9bd",
transition: { duration: 1.4, path: "flow" } },
{ shape: "check", duration: 3 },
], { loop: true, defaults: { count: 1000, color: "#ffffff" } }), []);
return <>
<DotsSequencePlayer sequence={sequence} animate style={{ height: 360 }} />
<button type="button" onClick={() => sequence.restart()}>Replay</button>
</>;
}
// Playback API: sequence.play(), pause(), restart(), seek(seconds).
Clip duration includes its arriving transition. The remaining time holds the finished formation. Add animate to the player to keep its native motion running during the hold. Per-element fields include color, count, speed, dotSize, spread, interactive, background, and transition duration/path. Defaults apply wherever an element omits a field. Keep the sequence instance stable across renders, using useMemo per component instance. Share an instance only when you intentionally want consumers to share a playhead.
"use client";
import { useMemo } from "react";
import { DotsSequence, useDotsSequence } from "dots-swarm";
import { SwarmUI, SwarmButton } from "dots-swarm/ui";
import "dots-swarm/ui.css";
const scene = { position: "absolute" as const, inset: 16,
display: "grid", placeItems: "center" };
export function Onboarding({ onContinue }: { onContinue: () => void }) {
const sequence = useMemo(() => new DotsSequence([
{ element: "cloud", duration: 4 },
{ element: "continue", duration: 6 },
], { loop: false }), []);
const { element } = useDotsSequence(sequence);
return <SwarmUI.Root active={element.element!} transition={false}
style={{ position: "relative", height: 320 }}>
useDotsSequence exposes the current element, index, elapsed clip time, total time, progress, and playing state. It shares one clock across consumers, stops when unmounted, respects reduced motion, and suspends in a hidden tab. The pure class is also available from dots-swarm/sequence, without importing React.
const sequence = DotsSequence.fromJSON(definition);
const json = JSON.stringify(sequence); // compact, serializable data
// Private Pro package: names resolve through the registry.
import { proShapes } from "@dots-swarm/pro";
<DotsSequencePlayer sequence={sequence} shapes={proShapes} />
// Only genuinely custom geometry needs a separate asset file.
import assets from "./my-swarm.assets.json";
<DotsSequencePlayer sequence={sequence} assets={assets} />
Core and Pro catalog shapes export by name. Imported SVG geometry is downloaded separately as an assets file, and the timeline refers to an asset key. Studio backup preserves editor settings for later import. Pro artwork lives in its private package, available from your account after purchase.
Keyframe choreography
SwarmSequence is included in the free core. Drive its progress from a slider, scrolling, requestAnimationFrame, or an animation library. Seeking backward retraces the same paths; no timer or GSAP dependency is required.
"use client";
import { useState } from "react";
import { SwarmSequence, type SwarmKeyframe } from "dots-swarm";
// Keep keyframes outside the component, or memoize them.
const keyframes: SwarmKeyframe[] = [
{ at: 0, shape: "cloud", density: 0.4, dispersion: 0.7 },
{ at: 0.5, shape: "wire-sphere", density: 1, dispersion: 0,
color: "#b6a1ff", rotation: 0.5, scale: 1.1,
ease: "smooth", path: "flow", stagger: 0.2 },
{ at: 1, shape: "check", color: "#ffffff", rotation: 0,
scale: 0.9, dotSize: 1.8, opacity: 1 },
];
export default function Choreography() {
const [progress, setProgress] = useState(0);
return (
Keyframes start at 0 and end at 1, in strictly increasing order. Animate shape or custom points, color (#RGB or #RRGGBB), density (0–1), dispersion (0–2), turbulence (0–1), speed (turbulence frequency, 0–5), dotSize (0.2–6), opacity (0–1), scale (0.1–2), rotation (radians), and x/y position (normalized units). Omitted properties carry forward.
Each destination keyframe controls the arriving segment: ease is smooth, linear, or elastic; path is flow, direct, or scatter; stagger ranges from 0 to 0.8. Repeat a formation at a later position to hold it. Keep count fixed and animate density to fade particles without rebuilding the pool.
Add animate to keep native shape motion playing while progress stays still. paused freezes that clock, and animationSpeed controls its rate (0–5). A keyframe’s rainbow value (0–1) blends its color toward a smooth 14-second spectrum cycle. The live clock stops offscreen, in hidden tabs, and under reduced motion.
The renderer draws only when needed, pauses offscreen, and snaps between keyframe poses under reduced motion. For scroll experiences, provide a useful static layout when reduced motion is enabled. Studio exports named DotsSequence definitions; use SwarmSequence for multi-property keyframes authored in code.
Scroll & triggers
Use useSwarmScroll to drive progress, fire threshold callbacks, or bind a DotsSequence. Native sticky panels and optional proximity snapping keep wheel, touch, and keyboard scrolling in the browser. These are the APIs used by this site’s hero and shape chapters.
"use client";
import { useRef } from "react";
import {
useSwarmScroll, useSwarmScrollSnap, SwarmScrollPin,
SwarmScrollStops, SwarmSequence, type SwarmKeyframe,
} from "dots-swarm";
const frames: SwarmKeyframe[] = [
{ at: 0, shape: "halo" },
{ at: 0.25, shape: "halo" }, // hold the finished, animated shape
{ at: 0.55, shape: "heart" }, // leave room for the morph
{ at: 0.8, shape: "heart" },
{ at: 1, shape: "check" },
];
function ScrollStory() {
const section = useRef<HTMLElement>(null);
const { progress, scrollTo } = useSwarmScroll({
Start/end pairs align a target edge with a viewport edge. Edges accept top, center, bottom, or a fraction. Offset can be a function for responsive headers. Pass a container ref for an overflow scroller. Call refresh() after external layout changes; resizing and font loading refresh automatically. Pinning uses the parent’s height as its scroll distance. Snap markers match the default top/top to bottom/bottom range in a window scroller.
Set scrub to a number of seconds for softened catch-up, or true for direct progress. onEnter, onLeave, onEnterBack, and onLeaveBack follow boundary crossings. Triggers take an at threshold, onCross, optional direction (forward by default), and once. Keep a once-trigger object stable with useMemo. Triggers do not fire retroactively on mount.
const triggers = useMemo(() => [{
at: 0.6,
direction: "forward" as const,
onCross: () => surface.current?.act(swarmActions.explode({ strength: 900 })),
}], []);
useSwarmScroll({ target: section, triggers });
Pass sequence to take over its clock and seek from scroll progress. On disconnect, normal playback resumes if enabled. Use DotsSequencePlayer animate to keep formation motion alive even for a timeline created with autoplay disabled. System reduced motion disables scrubbing; stage buttons still select a destination. The renderer controls its own motion policy. Snapping defaults to proximity and does not intercept upward scrolling.
Custom geometry
Pass an array of { x, y, z } points. Use roughly −1 to 1 for x and y; z controls apparent depth. Geometry is resampled to the requested count. Changing the points array morphs to the new formation.
import { DotSwarm, type Point } from "dots-swarm";
// Normalized coordinates, centered around { x: 0, y: 0 }.
// Use Studio's SVG importer to generate detailed geometry.
const points: Point[] = Array.from({ length: 600 }, (_, i) => {
const angle = (i / 600) * Math.PI * 2;
return { x: Math.cos(angle) * 0.8, y: Math.sin(angle) * 0.8, z: 0 };
});
<div style={{ width: 400, height: 400 }}>
<DotSwarm points={points} count={600} />
</div>
The Studio’s SVG importer supports paths, circles, ellipses, rectangles, lines, and polygons, including group transforms. Outline mode follows their contours. Fill mode respects compound-path holes and fill rules. Convert text, symbols, masks, and clips into paths before importing. SVG files are processed entirely in your browser.
Download an example SVGUI primitives
Import working controls from dots-swarm/ui. Button, Select, Slider, Checkbox, and Switch assemble from the same particle pool, then become solid DOM controls. The controls use Radix for focus, keyboard interaction, disabled states, and form behavior. These primitives are included in the free core package.
"use client";
import { useState } from "react";
import { SwarmUI, SwarmButton, SwarmSwitch } from "dots-swarm/ui";
import "dots-swarm/ui.css";
export function Settings() {
const [scene, setScene] = useState("button");
return (
<>
<SwarmUI.Root active={scene} style={{ height: 260 }}>
<SwarmUI.Scene name="button" style={{ position: "absolute", inset: 80 }}>
<SwarmButton onClick={() => setScene("settings")}>
Open settings
</SwarmButton>
</SwarmUI.Scene>
<SwarmUI.Scene name="settings" style={{ position: "absolute", inset: 80 }}>
<SwarmSwitch.Root aria-label="Notifications" defaultChecked>
<SwarmSwitch.Thumb />
Root owns the pool. Scene names a formation. Change active to morph. Inactive scenes stay mounted to preserve their values, but are hidden and inert. Use absolute or grid positioning to place scenes in the same space.
| Export | Parts / behavior |
|---|---|
SwarmUI | Root, Scene, Part; one pool, named formations, custom targets. |
SwarmButton | Native button, with asChild for your own ref-forwarding element. |
SwarmSelect | Root, Trigger, Value, Icon, Portal, Content, Viewport, Item, ItemText, ItemIndicator, Group, Label, Separator, ScrollUpButton, ScrollDownButton. |
SwarmSlider | Root, Track, Range, Thumb; controlled or uncontrolled, multiple thumbs and vertical orientation. |
SwarmCheckbox | Root, Indicator; checked, unchecked, indeterminate. |
SwarmSwitch | Root, Thumb; controlled or uncontrolled. |
// Part adds swarm behavior to your own Radix or shadcn component.
// The child must forward its ref and spread received props onto its DOM node.
<SwarmUI.Scene name="custom">
<SwarmUI.Part asChild radius={12}>
<YourButton onClick={save}>Save changes</YourButton>
</SwarmUI.Part>
</SwarmUI.Scene>
// Or begin as a shape, then change Root's active scene to a UI scene.
<SwarmUI.Scene name="cloud">
<SwarmUI.Part shape="cloud" style={{ width: 240, height: 240 }} />
</SwarmUI.Scene>
All controls forward refs and accept their underlying Radix props. They also work without a swarm root. Add visible labels or accessible names as you would with any control. Avoid nesting a Part around a primitive that already registers its own parts.
Import dots-swarm/ui.css once for the default design and overlay presence lifecycle. Customize with className and style on each part, or set --swarm-ui-bg, --swarm-ui-fg,--swarm-ui-muted, and --swarm-ui-accent on your root. Portalled content lives outside the root, so apply theme tokens to Content or a common ancestor too.
Root accepts surface options such as count, color, paused, reducedMotion, colliders, spring, and damping. Set transition to false for a direct morph or pass burst options to customize the scatter. Its ref exposes act(), burst(), and refresh(). Prefer explicit actions when the return destination can change. Reduced motion reveals the finished controls immediately.
Forms, overlays & layout
The UI subpath includes inputs, forms, navigation, overlays, and layout primitives. Interactive compound components retain Radix’s API: Root, Trigger, Content, Portal, and the component-specific parts. Inputs and layout elements use native HTML props.
SwarmInput, SwarmTextarea, SwarmLabel, SwarmCard, SwarmBadge, SwarmSeparator, SwarmSkeleton, SwarmDialog, SwarmAlertDialog, SwarmPopover, SwarmDropdownMenu, SwarmContextMenu, SwarmTabs, SwarmAccordion, SwarmRadioGroup, SwarmToggle, SwarmToggleGroup, SwarmTooltip, SwarmProgress, SwarmScrollArea, SwarmToast, SwarmNavigationMenu, SwarmForm, SwarmTable.
import { SwarmUI, SwarmLabel, SwarmInput, SwarmButton } from "dots-swarm/ui";
import "dots-swarm/ui.css";
<SwarmUI.Root active="profile" style={{ minHeight: 300 }}>
<SwarmUI.Scene name="profile">
<form onSubmit={onSubmit}>
<SwarmLabel htmlFor="email">Email</SwarmLabel>
<SwarmInput id="email" name="email" type="email" required />
<SwarmButton type="submit">Save</SwarmButton>
</form>
</SwarmUI.Scene>
</SwarmUI.Root>
Every primitive works outside a swarm. Inside a scene, controls register their surfaces automatically. SwarmCard.Root registers the entire card as one material, including its background and content. Nested controls stay functional but do not compete for the same particle pool. For your own composite block, use SwarmUI.Part with contain and asChild. Tables remain layout containers until you wrap them in a Part.
Use Portal/Overlay/Content for dialogs and provide Title and Description. Retain accessible labels for inputs, sliders, and icon-only buttons. Components do not infer application labels. The built-in overlays manage their own local particle entrance and exit, including inside portals. Use a viewport surface when you deliberately want particles to travel across the page. Theme with --swarm-ui-bg, --swarm-ui-fg, --swarm-ui-border, --swarm-ui-accent, and --swarm-ui-muted. Add those tokens to portal content or a document-level theme as needed.
Component state changes
Button feedback and layout changes can use the same particle handoff as a scene change. SwarmMorph wraps one ref-forwarding element and animates when its state key changes. It retains keyboard focus and does not delay application state updates.
import { SwarmButton, SwarmMorph, SwarmCard } from "dots-swarm/ui";
// saveState and saveProject come from your real submit handler.
<SwarmButton morphKey={saveState} onClick={saveProject}
disabled={saveState === "pending"}>
{saveState === "saved" ? "Saved ✓" : saveState === "pending" ? "Saving…" : "Save project"}
</SwarmButton>
<SwarmMorph state={selectedTab}>
<SwarmCard.Root>{/* your changing content */}</SwarmCard.Root>
</SwarmMorph>
Select, dropdown-menu, context-menu, popover, and dialog content animate their own particle entrance and exit, including when rendered in portals. Radix continues to own focus, keyboard behavior, and dismissal. Dialog scrims fade in and out; the panel itself assembles from particles. Import ui.css for the presence lifecycle. Reduced motion skips the particle handoff. Scrolling the page or resizing cancels a transient handoff rather than leaving particles behind at stale positions.
Use morphKey or SwarmMorph for discrete changes, such as submitted → saved or one tab → another. Continuous editing—typing and dragging a slider—stays immediate. The primitive presets demonstrate these behaviors and include dialog and menu examples.
UI sequences
SwarmPrimitive supplies compact, editable defaults for thirteen common primitives. Compose their underlying parts directly for custom applications. Studio exports named UI definitions alongside your DotsSequence; no DOM coordinates are baked into it.
import { DotsSequence } from "dots-swarm";
import { DotsUISequencePlayer } from "dots-swarm/ui";
import "dots-swarm/ui.css";
const sequence = new DotsSequence([
{ shape: "cloud", duration: 3 },
{ element: "welcome", duration: 7 },
{ shape: "halo", duration: 3 },
]);
const elements = {
welcome: { kind: "card", label: "Your next idea", detail: "Make a little room." },
} as const;
<DotsUISequencePlayer sequence={sequence} elements={elements} />;
DotsUISequencePlayer handles mixed UI and shape sequences. Shape-only sequences can continue using DotsSequencePlayer. Named shapes and registered custom point motion stay animated in the mixed player. SwarmComposition offers the same surface with an externally controlled active scene. For a custom compound component, contain prevents duplicate registration of its child surfaces.
<SwarmUI.Part contain asChild radius={12}>
<YourCard>{/* real inputs, labels, and buttons */}</YourCard>
</SwarmUI.Part>
For the Studio sequence JSON export, download its elements JSON companion. The TypeScript export includes both objects. The Studio backup includes the full editable project.
Measure once. Form in place.
SwarmSurface keeps one particle pool across formations, falling motion, and real HTML elements. Give each target a stable ID and an element ref. The swarm measures its position inside the surface and springs into place.
"use client";
import { useMemo, useRef, useState } from "react";
import { SwarmSurface, type SwarmTarget } from "dots-swarm";
export function LivingButton() {
const shape = useRef<HTMLDivElement>(null);
const button = useRef<HTMLButtonElement>(null);
const [gathered, setGathered] = useState(false);
const targets = useMemo<SwarmTarget[]>(() => gathered
? [{ id: "button", kind: "element", ref: button, radius: 28 }]
: [{ id: "shape", kind: "shape", shape: "helix", ref: shape }],
[gathered]);
return (
<SwarmSurface targets={targets} count={900}
style={{ height: 400, color: "white", background: "black" }}>
<div ref={shape} style={{ height: 280 }} aria-hidden="true" />
<button ref={button} onClick={() => setGathered(!gathered)}
Use multiple targets to split the swarm. Optional weight sets each target’s share of the fixed particle count. Targets support shape (core formations), points (custom normalized geometry), element (your actual HTML component), and avatar. Elements and avatars default to appearance: "solid". Particles fill the body as they gather, then disappear into a seamless finish.
Solid elements reveal their real DOM, so your fills, typography, borders, shadows, focus rings, and hover styles remain exactly as authored. Style the element normally. The surface temporarily controls its inline opacity and restores it when the target leaves or the surface unmounts. Avoid a separate CSS opacity transition on that element. Use appearance: "dots" for the original particle treatment; element targets then use radius and thicknessto form a dotted border. Optional target color colors the particles and solid avatars.
Keep the surface mounted. Resizing and font loading refresh its measurements. Call the ref’s refresh() after position-only layout changes or scrolling inside a nested container. Refs must point to elements inside the surface. Target and collider geometry should be untransformed; animate particle positions through targets rather than CSS transforms on the canvas.
The canvas is decorative and ignores pointer events. Your buttons, links, labels, focus management, and click handlers stay real HTML. The component does not automatically make a target interactive.
Explode. Rain. Reform somewhere else.
A target describes where the dots belong. An action changes what happens to the existing particles. Explode and rain leave the dots under physics. Reform is independent: the next destination can be a different avatar expression, a shape, or a real button.
import { useRef } from "react";
import { SwarmSurface, swarmActions, type SwarmSurfaceHandle } from "dots-swarm";
// Inside your component, with these refs attached to your surface and targets:
const surface = useRef<SwarmSurfaceHandle>(null);
const avatar = useRef<HTMLDivElement>(null);
const button = useRef<HTMLButtonElement>(null);
// Each function can be a separate click or scroll handler.
const explode = () => surface.current?.act(swarmActions.explode({
anticipation: 1.5, strength: 1800, spread: 0.95,
gravity: 170, duration: 2.7,
}));
const becomeButton = () => surface.current?.act(swarmActions.reform({
duration: 2,
targets: [{ id: "continue", ref: button, kind: "element", radius: 24 }],
}));
Use act() on a SwarmSurface or SwarmUI.Root ref. Colliders, viewport mode, pause, and reduced motion apply to actions. A new action replaces the previous one while retaining particle positions and velocities. Explicit reform targets remain active until the declarative targets prop changes.
<SwarmSurface ref={surface} targets={targets} colliders={colliders}
viewport
onActionPhaseChange={(phase, action) => {
// charging → scattered, or gathering → settled
}}
onActionComplete={(action) => {
if (action.type === "explode") {
surface.current?.act(swarmActions.reform({
duration: 4.2,
targets: [{ id: "friend", ref: avatar, kind: "avatar",
expression: "dizzy", appearance: "solid" }],
}));
}
}}>
<div ref={avatar} style={{ width: 240, height: 240 }} />
</SwarmSurface>
Explosion duration is the time after release until a completion cue fires; it does not reassemble the particles. Rain has no cue unless a duration is supplied. Reform ramps attraction over its duration and completes upon arrival. To use another UI scene, update SwarmUI.Root active and call reform; set transition={false} when actions own the transition. The older burst() method retains its automatic return.
Physics & text collisions
Switch to mode="physics" to release the particles, then return to mode="form" to gather them. Position and velocity carry through both modes. For event-driven releases, prefer rain and reform actions. Physics is a live simulation; scrubbing a scroll position does not replay a recorded trajectory.
const surface = useRef<SwarmSurfaceHandle>(null);
const headline = useRef<HTMLHeadingElement>(null);
const colliders = useMemo(() => [
{ ref: headline, kind: "text" as const }
], []);
<SwarmSurface ref={surface} targets={targets}
colliders={colliders} gravity={460} wind={28} restitution={0.64}>
<h1 ref={headline}>Small dots. Big possibilities.</h1>
{/* Your measured targets and other real UI. */}
</SwarmSurface>
// Connect these to separate application events or click handlers.
const letItRain = () => surface.current?.act(
swarmActions.rain({ gravity: 460, wind: 28 })
);
const gather = () => surface.current?.act(
swarmActions.reform({ duration: 2 })
);
Text colliders rasterize the actual glyphs, including wrapped lines; box colliders use rectangular bounds. The solver sub-steps and sweeps particles against those masks, then reflects their velocity. It supports gravity, horizontal wind, damping, springs, and boundary bounce. The core solver does not simulate dot-to-dot collisions or rigid-body rotation. Pro adds optional particle collisions and force fields. Neither solver reproduces arbitrary DOM artwork as a rigid body. Text masks support horizontal text without CSS transforms; remeasure after changing its content.
| Control | Default | Units / behavior |
|---|---|---|
| count | 1000 | 24–2400. Fixed until explicitly changed. |
| gravity / wind | 650 / 0 | CSS pixels per second². Applied in physics mode. |
| restitution | 0.55 | 0–1. Energy retained after collision. |
| spring / damping | 70 / 13 | Formation attraction and velocity damping. |
| dotSize / color | 1.2 / white | Radius in pixels (0.4–4), Canvas color. |
| rainbow | false | Continuous 15-second color cycle. |
| interactive | false | Nearby pointer repels dots in form mode. |
| paused / reducedMotion | false / OS | Freeze playback / snap directly to targets. |
For your own renderer, import SwarmPhysics and createRectCollider from dots-swarm/physics. The solver has no React or DOM dependency. The root export also supplies createTextCollider and sampleSurfaceGeometry for browser-based renderers.
Core avatars
const avatar = useRef<HTMLDivElement>(null);
const targets = useMemo<SwarmTarget[]>(() => [{
id: "assistant", ref: avatar, kind: "avatar",
appearance: "solid",
expression: "curious", // also: neutral, happy, surprised, excited, dizzy
}], []);
<SwarmSurface targets={targets} count={1800} color="#b9d4ff">
<div ref={avatar} style={{ height: 360 }} aria-hidden="true" />
<p>Your assistant is ready.</p>
</SwarmSurface>
Solid avatars have a flat, softly lit body and large pill eyes. They breathe, blink, and glance toward the pointer. Use swarmActions.explode() to scatter the body and a separate reform() action to gather it again. Update expression to change their mood: excited opens the eyes; dizzy adds a sway and wandering eyes. Set the returning expression before calling reform, for example from onActionComplete. Pro’s SwarmAvatar adds adjustable silhouettes, eyes, and speech amplitude. Reduced motion makes them still. Keep meaningful assistant status in visible text as well.
Viewport effects
Add viewport to let particles leave their component and bounce off the page’s text and controls. The canvas passes clicks through to your UI. Selectors measure visible obstacles across the document; the target stays anchored to its original element.
<SwarmSurface ref={surface} viewport targets={targets}
colliders={[
{ selector: "h1, h2", kind: "text" },
{ selector: "header a, button", kind: "box" },
]}
gravity={170} restitution={0.82}
onActionComplete={action => {
if (action.type === "explode") {
setExpression("dizzy");
surface.current?.act(swarmActions.reform({ duration: 4.2 }));
}
}}>
<div ref={avatar} style={{ height: 400 }} aria-hidden="true" />
</SwarmSurface>
// On click: build tension, then release.
surface.current?.act(swarmActions.explode({
anticipation: 2.1, strength: 1900, spread: 0.95,
onActionPhaseChange receives charging, scattered, gathering, then settled once the particles arrive. The sequence follows the surface’s clock, including pause, visibility, and reduced motion. Action coordinates are viewport CSS pixels in this mode; omit them to explode from the particle centroid. Keep colliders stable and narrowly scoped to the UI the particles should hit. Use separate actions when the return destination or timing depends on your application.
Pro coordination systems
The private Pro package supplies timeline, event, transition, formation, physics, interaction, and avatar modules. Core remains the renderer and primitive layer. Pro + Studio is $149 USD once. Install it with npm using the private registry setup. Create your install token in your account after purchase. The showcases are open to everyone.
Multi-track timelines
import { DotsTimeline } from "@dots-swarm/pro/timeline";
import { DotsTimelinePlayer, useDotsTimeline } from "@dots-swarm/pro/react";
const timeline = new DotsTimeline({
version: 1, duration: 8, loop: false,
labels: { success: 5, retry: 0 },
tracks: [{ id: "main", clips: [
{ id: "a", at: 0, duration: 5, value: { shape: "cloud" } },
{ id: "b", at: 4, duration: 4, value: { shape: "check" } },
] }],
gates: [{ at: 4, event: "result", branches: { ok: "success", error: "retry" } }],
cues: [{ at: 2, event: "action", payload: {
target: "avatar", action: { type: "explode", strength: 900 },
} }],
});
// In an effect: timeline.play(); return () => timeline.pause();
<DotsTimelinePlayer timeline={timeline} surfaces={{ avatar: avatarRef }} />
Tracks have stable IDs; clips carry at, duration, and a value object. The supplied player renders named shape tracks and blends two overlapping clips in one pool per track. Register Pro artwork with its shapes prop. For UI or other custom value types, use useDotsTimeline and map the sampled tracks to your component props. Cues execute during forward playback; seeking samples state without replaying side effects. Action cue targets refer to the player’s named surface refs. Reform destinations that contain DOM refs should be resolved in your own event handler, not serialized into JSON.
play, pause, restart, seek, emit, sample, subscribe, and toJSON are available. Multiple consumers share one clock. Gates suspend progression until their event arrives; branch values map to named labels. Reduced motion disables automatic playback in the React hook. Large frame gaps are clamped; this is an animation timeline, not a wall-clock scheduler.
Event graphs
import { DotsEventGraph } from "@dots-swarm/pro/events";
const graph = new DotsEventGraph({ version: 1, nodes: [{
id: "open", event: "click", delay: 0.2, reentry: "replace",
commands: [{ target: "panel", method: "open" }], emit: "opened",
}] });
const unregister = graph.register("panel", async (method, value, signal) => {
if (signal.aborted) return;
if (method === "open") openPanel();
});
graph.emit("click");
// Cleanup: unregister(); graph.dispose();
Nodes connect an event to registered commands, then optionally emit another event. Reentry can replace pending runs, ignore repeats, or allow parallel runs. Delays are cancellable; async handlers receive an AbortSignal and must cooperate with cancellation. Graph cycles stop within one dispatch chain. Unknown target handlers report errors rather than evaluating arbitrary code. Use cancel(nodeId) or cancel() for pending runs.
UI flows
import { SwarmFlow, useSwarmFlow } from "@dots-swarm/pro/react";
<SwarmFlow initial="button">
<SwarmUI.Scene name="button"><Launcher /></SwarmUI.Scene>
<SwarmUI.Scene name="settings"><SettingsForm /></SwarmUI.Scene>
</SwarmFlow>
// Inside Launcher:
const { go, active, busy } = useSwarmFlow();
<SwarmButton onClick={() => go("settings")}>Open settings</SwarmButton>
A flow explicitly explodes, changes its active scene, then reforms. New destinations replace pending ones during interruption. Scene state stays mounted and the core scene layer handles focus leaving inactive content. Layouts and their content remain your React components.
Formations and transitions
import { createFormation } from "@dots-swarm/pro/formations";
import { transitionPoints, partitionPoints } from "@dots-swarm/pro/transitions";
const ribbon = createFormation({ kind: "ribbon", turns: 3, twist: 2,
thickness: 0.18, count: 1200, time });
const destination = partitionPoints([
{ points: heart, x: -0.5, scale: 0.45 },
{ points: check, x: 0.5, scale: 0.45 },
], 1200);
const points = transitionPoints(ribbon, destination, progress, {
wave: "radial", stagger: 0.3,
path: [{ x: 0, y: 0, z: 0 }, { x: 0, y: -0.6, z: 0.4 }, { x: 0, y: 0, z: 0 }],
});
<DotSwarm points={points} transitionDuration={0} />
Formations support rings, ribbon, and knot families with count, radius, thickness, turns, twist, and time. Transitions are stateless and seekable. Paths specify piecewise-linear normalized offsets with a zero envelope at either endpoint. Waves use x, y, or radial ordering; a custom mask callback can supply per-particle order. Partitions preserve index ranges when group order and weights stay fixed.
Physics and gestures
import { ProSwarmPhysics, circleCollider, polygonCollider } from "@dots-swarm/pro/physics";
import { useSwarmGesture } from "@dots-swarm/pro/interactions";
const { field, gesture } = useSwarmGesture(containerRef, { mode: "vortex" });
const createPhysics = useCallback(points => new ProSwarmPhysics(points, () => ({
fields: field.current ? [field.current] : [],
collisions: true, particleRadius: 2,
colliders: [circleCollider(movingX.current, 250, 30)],
constraints: [{ index: 0, x: 200, y: 80, length: 50, stiffness: 0.8 }],
})), [field]);
<SwarmSurface createPhysics={createPhysics} targets={targets} />
The solver reads configuration each step, so fields, obstacles, and constraint anchors can move. Fields attract, repel, or create vortices. Optional particle collisions use a spatial grid; keep particle counts modest when enabling them. Constraints attach particle indices to an anchor or maximum distance. Solver coordinates are local CSS pixels, or viewport pixels for viewport surfaces. Keep the factory stable and read changing values through refs to preserve the particle pool. For example, read a moving collider’s position from a ref inside the config getter rather than capturing a stale render value.
useSwarmGesture provides pointer capture, drag offsets, two-pointer scale/rotation, and a mutable field ref. It temporarily owns touch-action on its target, restoring it on cleanup; attach it to the interactive stage rather than the whole page. Gestures report values—the application chooses how to map scale or rotation to its elements.
Avatars
import { SwarmAvatar } from "@dots-swarm/pro/avatar";
<SwarmAvatar ref={surface} silhouette="squircle" expression="curious"
eyeScale={1.3} eyeSpacing={0.3} color="#78adff"
speech={normalizedAudioLevel} style={{ height: 320 }} />
Silhouettes are orb, squircle, and pebble. Eye size, spacing, body and eye colors are configurable. Six expressions blend their eye/pose parameters; speech (0–1) drives mouth movement alongside the expression. The component accepts the surface action ref and physics props. It does not record audio or connect a speech service. Supply amplitude from your own audio pipeline. createAvatarRenderer is available for custom targets through the core renderer extension.
Composition Studio
Studio mixes shapes and real UI primitives in one timeline. Use the UI library beside Presets and Shapes, then edit the label and description in the inspector. SVG import and existing shape presets remain available. Save a Studio backup to retain UI definitions and timeline settings together. Advanced choreography adds overlapping tracks, wait gates, avatar action cues, and editable event connections. Projects and graphs can be saved locally, imported, and exported as JSON. Public showcases are separate experiences, with compact API examples rather than editor controls.
Timeline JSON is loaded with DotsTimeline.fromJSON; event JSON is passed to DotsEventGraph. Register event handlers and named surface refs in your app. Advanced local save stores the timeline and graph together. Main Studio keeps its own presets and draft. Use Save to cloud and Open from cloud for private projects tied to your Pro account, or share exported files to move work between devices. Shared editing and collaboration are not included.
Studio exports & agent handoff
Arrange up to 12 formations, customize motion and color, then copy an agent prompt or export a compact DotsSequence module or JSON definition. A separate Studio backup preserves editor settings for later import.
The MIT core includes everyday assistant, calendar, messaging, media, commerce, and data/security essentials. The Pro collection adds 118 formations across assistant, productivity, communication, media, commerce, data/security, and abstract sculpture families, plus 7 editable sequences. Pro + Studio costs $149 USD as a one-time purchase, with permanent access and future updates included.
Pro artwork is separate from the MIT core package. Choose a Pro shape or recipe in Studio and its export refers to a named shape from @dots-swarm/pro. Only custom SVG artwork needs a separate geometry asset. Sculptures retain rotation, swirl, wave, pulse, or orbit motion through the registry.
Drafts and local presets stay in this browser. Use Save to cloud in either Studio workspace to keep projects in your account and open them on another device. JSON exports remain available for portable backups.
Open StudioAccessible by design
Dots automatically respects prefers-reduced-motion, renders an immediate static pose, and disables pointer forces. It suspends animation outside the viewport and while the page is hidden.
Use a meaningful label, visible text for important status, and an accessible pause control for persistent animation. Don’t communicate a critical event through particles alone.