npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

smokey-fluid-cursor

v2.1.0

Published

A customizable fluid cursor effect for Vanilla JavaScript/TypeScript applications running on the web. Enhance user experience with smooth, interactive cursor animations. Perfect for modern web designs.

Readme


Why

A real-time Navier–Stokes fluid solver running in WebGL, wired to your pointer. It ships as ESM, CJS and a plain <script> IIFE build, so it drops into a bundled app or a single HTML file equally well — and it degrades quietly on devices without WebGL instead of taking the page down.

Using React or Next.js? See react-smokey-fluid-cursor.

Installation

npm install smokey-fluid-cursor
yarn add smokey-fluid-cursor
pnpm add smokey-fluid-cursor
bun add smokey-fluid-cursor

No peer dependencies.

Upgrading from 1.x

2.0.0 adds mounting, placement, lifecycle and palette APIs. Most projects need no changes, but four behaviours differ:

| Change | Impact | What to do | | --- | --- | --- | | Placement is applied inline, not through an injected <style> block | CSS you wrote against #smokey-fluid-canvas to override position, z-index or size no longer wins | Use the position, zIndex, pointerEvents and className options instead, or add !important to your rules | | The canvas is created for you when none matches | Calling initFluid() with no canvas in the DOM used to do nothing; it now appends one to <body> | Pass canvas or container to control where it goes | | Device pixel ratio is capped at 2 (maxDpr) | Slightly softer rendering on 3x displays, markedly better frame rate and battery | Set maxDpr: Infinity for the old behaviour | | prefers-reduced-motion is honoured | Visitors who asked for reduced motion get a still canvas | Set respectReducedMotion: false to opt out |

initFluid also now returns a handle rather than nothing — purely additive, but you should start calling dispose() in single-page apps:

- initFluid();
+ const fluid = initFluid();
+ // on teardown:
+ fluid.dispose();

Quick start

import { initFluid } from "smokey-fluid-cursor";

const fluid = initFluid();

That is the whole integration. With no options it creates a full-viewport canvas in <body>, positioned fixed, pointer-events: none and behind your content, then starts the simulation.

initFluid returns a handle. Keep it if you need to stop, pause or retune the effect later:

fluid.pause();
fluid.resume();
fluid.setConfig({ curl: 30 });
fluid.dispose(); // stops the loop and detaches every listener

Calling dispose() is required in any single-page app — otherwise each navigation leaks a whole simulation.

Bring your own canvas

<canvas id="my-canvas"></canvas>
initFluid({ canvas: "#my-canvas" });

Scope it to one section

Give it a container and switch to absolute, and the effect stays inside that element instead of covering the page:

initFluid({
  container: "#hero",   // element or selector
  position: "absolute",
  zIndex: 0,
});

The container needs its own positioning context (position: relative) and overflow: hidden if you want the fluid clipped to it.

Without a bundler

A minified IIFE build is published for no-build pages:

<script src="https://unpkg.com/smokey-fluid-cursor"></script>
<script>
  var fluid = SmokeyFluid.initFluid();
</script>

The handle

| Method | Description | | --- | --- | | dispose() | Stop the loop, detach listeners, release the GL context, remove any canvas this call created. Safe to call twice. | | pause() | Freeze the simulation, leaving the canvas visible. | | resume() | Resume after pause(). | | isPaused() | Whether the simulation is currently stopped. | | setConfig(partial) | Retune in place — no remount. Resolution changes reallocate framebuffers; everything else applies next frame. | | splat(x, y, color?) | Inject a splash at a point, in CSS pixels relative to the canvas. Drive the effect from something other than the pointer. | | canvas | The canvas being rendered into. |

The effect is invisible? Check your page background

This is the single most common integration problem, and it looks like the package is broken when it is not.

The canvas defaults to z-index: -9999 so it sits behind your content. Per the CSS painting order, a negatively-stacked element paints above the root background but below the background of block-level descendants — so this extremely common setup hides the effect completely:

/* ✗ body's background paints straight over the canvas */
body { background: #0b0f17; }

Put the page background on <html> instead:

/* ✓ the canvas paints above the root background, below your content */
html { background: #0b0f17; }
body { background: transparent; }

Alternatively, lift the canvas above your background and push your content above the canvas:

initFluid({ zIndex: 0 });
main { position: relative; z-index: 1; }

In development the package detects this and warns in the console rather than leaving you with a blank screen.

Presets

100 ready-made looks ship with the package — every colour palette crossed with every motion character, named "<Palette> <Character>":

import { presets } from "smokey-fluid-cursor";

initFluid(presets["Ocean Swirl"]);

| | | | --- | --- | | Palettes (20) | Spectrum, Sunset, Ocean, Mono, Aurora, Ember, Lagoon, Candy, Toxic, Royal, Sakura, Mint, Copper, Ultraviolet, Ice, Magma, Forest, Dusk, Cyber, Pastel | | Characters (5) | Calm, Flow, Swirl, Storm, Wisp |

So "Magma Storm" is the magma palette at storm intensity, "Ice Wisp" is pale and sparse, and so on.

Presets only set appearance and physics — never mounting or placement — so they compose with your own container, zIndex and the rest:

initFluid({ ...presets["Cyber Swirl"], container: "#hero", position: "absolute" });

Browse all 100 on the live demo.

Helpers

| Export | Type | Description | | --- | --- | --- | | presets | Record<PresetName, Preset> | Every preset, keyed by name. | | presetNames | PresetName[] | All 100 names, in order. | | paletteNames | PaletteName[] | The 20 palettes. | | characterNames | CharacterName[] | The 5 motion characters. | | getPreset(name) | Preset \| undefined | Safe lookup for a user-supplied name. |

Configuration

Every option is optional.

Mounting & placement

| Option | Type | Default | Description | | --- | --- | --- | --- | | canvas | HTMLCanvasElement \| string | — | Render into an existing canvas (element or selector). Wins over id/container. | | container | HTMLElement \| string | document.body | Where to create the canvas when none exists. | | id | string | "smokey-fluid-canvas" | Id used to find, or assign to, the canvas. | | position | "fixed" \| "absolute" \| "relative" \| "static" | "fixed" | absolute confines the effect to a positioned container. | | zIndex | number | -9999 | Stacking order. | | pointerEvents | boolean | false | Whether the canvas swallows clicks. | | className | string | — | Extra class on the canvas. |

Performance & accessibility

| Option | Type | Default | Description | | --- | --- | --- | --- | | maxDpr | number | 2 | Caps the device pixel ratio. Uncapped, a 3x phone renders nine times the pixels of a 1x display for a decorative effect. | | pauseOnHidden | boolean | true | Stop the loop while the tab is in the background. | | respectReducedMotion | boolean | true | Start paused when the visitor has prefers-reduced-motion: reduce. | | paused | boolean | false | Start frozen. |

Appearance

| Option | Type | Default | Description | | --- | --- | --- | --- | | palette | string[] | null | Hex colours to draw from, e.g. ["#ff4ecd", "#4ea8ff"]. Omit for the full random hue range. | | colorIntensity | number | 0.15 | Brightness multiplier. Raise for a bolder trail. | | backColor | { r, g, b } | { r: 0, g: 0, b: 0 } | Canvas background. | | transparent | boolean | true | Blend with the page background. | | shading | boolean | true | Lighting, for a sense of depth. | | colorUpdateSpeed | number | 10 | How fast the palette rotates. |

Simulation

| Option | Type | Default | Description | | --- | --- | --- | --- | | simResolution | number | 128 | Velocity/pressure grid. Lower is faster and coarser. | | dyeResolution | number | 1440 | Colour buffer resolution. The main quality/cost dial. | | densityDissipation | number | 3.5 | How fast colour fades. | | velocityDissipation | number | 2 | How fast motion slows. | | pressure | number | 0.1 | Initial pressure multiplier. | | pressureIteration | number | 20 | Jacobi iterations. Higher is more accurate, slower. | | curl | number | 10 | Vorticity confinement — the swirliness. | | splatRadius | number | 0.5 | Size of each pointer splat. | | splatForce | number | 6000 | Force applied per splat. |

Performance

The defaults target a modern desktop GPU. The big levers, in order of impact:

  1. maxDpr — already capped at 2. Drop to 1 for the weakest devices.
  2. dyeResolution — 512 is noticeably cheaper and still looks good.
  3. pressureIteration — 10 roughly halves the solver cost.
initFluid({ maxDpr: 1, dyeResolution: 512, pressureIteration: 10 });

Quality is also lowered automatically when the GPU lacks linear filtering for float textures.

Accessibility

A full-screen animation is a real problem for people with vestibular disorders. By default this package honours prefers-reduced-motion: reduce by starting paused, and reacts if the preference changes while the page is open. Opt out with respectReducedMotion: false only if you have a good reason.

Package size

All builds are minified and the package ships no sourcemaps. Shader source is additionally minified at build time — GLSL lives in template literals, which JS minifiers cannot touch, and it is ~40% of this bundle. That kept the JavaScript payload flat at ~21 kB even though this release adds mounting, placement, palette, lifecycle and handle APIs.

Browser support

Requires WebGL (WebGL 2 when available, with a WebGL 1 fallback). Without it initFluid logs a warning and returns a handle whose dispose() still works — it never throws, so a decorative effect cannot take down your app.

Contributing

Issues and pull requests are welcome.

git clone https://github.com/faraasat/smokey-fluid-cursor.git
cd smokey-fluid-cursor
npm install
npm test          # vitest unit tests
npm run typecheck # tsc --noEmit
npm run build     # tsup

End-to-end tests run against the built demo in a real browser (desktop and mobile viewports), and cover the things unit tests cannot: layout, CSS and keyboard behaviour.

npm run build && npm --prefix example install && npm --prefix example run build
npm run test:e2e      # playwright
npm run test:e2e:ui   # interactive

To run the demo site against your local build:

npm run example:dev

Releases are manual — nothing publishes on a push to main. Maintainers run the Release workflow from the Actions tab.

Privacy

The published package contains no telemetry. The demo site at faraasat.github.io/smokey-fluid-cursor uses Google Analytics and Aptabase; the library itself never phones home.

License

MIT © Farasat Ali