Interactive snowfall that feels at home on your website.
DOM-aware snow accumulation, playful physics, and optional Christmas effects.
Built with TypeScript. Made for your interface.
Live playground · Explore the presets · Documentation · Founder
Quick start · Features · React & Next.js · API · Contributing
Fluri is a JavaScript and TypeScript snow animation library for the web. Snow gathers on supported DOM edges, follows moving elements, and falls onto surfaces below. Visitors can brush it away, gather snowballs, watch a tiny snowplough clear a path, or invite a mischievous yeti to start a snowball fight.
Use a light dusting on a portfolio, a playful snowfall on a landing page, or warm Christmas lights on the surfaces you choose. Your content stays in ordinary HTML; Fluri draws the winter around it.
- Snow with somewhere to land. Surface-local accumulation follows supported DOM geometry, scrolling, and responsive layouts. Uneven packing and rounded shoulders give each pile its own shape.
- A tiny yeti with a plan. Opt in to an animated visitor that sneaks over, scoops real snow, packs a ball and aims at your snowman. Hits knock it apart; moving targets can make it miss. No images or animation assets to load.
- A little room to play. Brush deposits, gather and throw snowballs, build a snowman, or run a snowplough along a designated shelf.
- Your website, your forecast. Ten presets, typed overrides, adjustable wind, flake styles, snow depth, and live configuration export.
- A good guest. Pointer-transparent canvases, reduced-motion support, hidden-tab suspension, adaptive quality, and explicit cleanup.
- Local by default. The engine needs no account, API key, geolocation, analytics, cookies, or external weather service.
- Christmas, on purpose. Optional lights, garlands, ornaments, gifts, frost, icicles, wreaths, candy canes, and sparkles on explicitly marked elements.
npm install @fluritheme/fluriAlso available on jsDelivr and UNPKG. npm is the recommended way to integrate Fluri with your build tool; raw CDN files require an import map or an ESM transformation service to resolve dependencies.
Mark the elements that should collect snow:
<article data-weather-surface>Your existing content.</article>
<section data-weather-ignore>Keep important controls clear.</section>Start after the DOM is ready:
import { createWeather } from '@fluritheme/fluri';
const weather = createWeather({
preset: 'gentle-snow',
dom: { detection: 'explicit' },
});
weather.start();
// Release the effect when the page leaves.
window.addEventListener('pagehide', () => weather.destroy(), { once: true });Importing the core and creating an idle controller are safe during server rendering. Call start() in the browser after mount; autoStart defaults to false. In a component, call destroy() in its unmount cleanup.
Complete getting-started guide →
weather.setIntensity(0.65);
weather.updateConfig({
snow: {
wind: { base: 0.12, gusts: true },
accumulation: { maxDepth: 16 },
},
performance: { adaptive: true, maxDPR: 1.5 },
});Choose explicit surfaces for predictable integration. Use attributes or registerSurface() to control material, accumulation, depth, clipping, and layer. Automatic discovery is also available.
<nav data-weather-surface data-weather-snow-depth="12">Your navigation</nav>
<article data-weather-surface data-weather-material="sticky">Your card</article>const element = document.querySelector('article');
if (element) {
const unregister = weather.registerSurface(element, {
material: 'sticky',
maxSnowDepth: 12,
layer: 'content',
});
// Remove this registration when the element is no longer needed.
unregister();
}| Quiet & considered | A little atmosphere | Seasonal & playful |
|---|---|---|
gentle-snow — a soft dusting |
winter-magic — glow and deeper piles |
christmas-classic — a familiar flurry |
minimal — a restrained particle budget |
cinematic — depth and larger flakes |
christmas-night — cool, glowing snow |
frosted-glass — sparse crystals |
snowstorm — wind and denser snowfall |
north-pole — deeper snow and snowballs |
playful — snowballs, snowmen, and pointer airflow |
Presets configure the weather. Attach the optional Christmas module to add decorations.
The core is framework independent. @fluritheme/fluri-react adds a component and hook that own the controller through mount, updates, and cleanup, including React Strict Mode.
npm install @fluritheme/fluri @fluritheme/fluri-react'use client';
import { useFluri } from '@fluritheme/fluri-react';
export function WinterCard() {
const weather = useFluri({
preset: 'gentle-snow',
dom: { detection: 'explicit' },
});
return (
<article data-weather-surface>
<h1>A little winter.</h1>
<button onClick={() => weather.current?.pause()}>Pause snow</button>
</article>
);
}Prefer a decorative component? Use <Fluri options={{ preset: 'gentle-snow' }} />. It renders no React DOM and starts the effect after mount. In Next.js, keep it inside a client component.
Vanilla JavaScript, Vue, and Svelte can use the core with their own mount and cleanup hooks. Framework integrations →
Add the separate Christmas export and label the surfaces to decorate:
import { christmasPack } from '@fluritheme/fluri/christmas';
const removeChristmas = weather.use(
christmasPack({
lights: { warmth: 0.9 },
garlands: true,
frost: true,
}),
);<nav data-weather-surface data-holiday-lights data-holiday-garland>A warmer welcome.</nav>Remove it independently with removeChristmas(). Decorations use original procedural artwork; they do not fetch images or fonts. Seasonal configuration →
weather.updateConfig({
interactions: {
snowballs: { enabled: true, activation: 'press-and-drag' },
snowman: { enabled: true },
},
});
weather.trigger('gust');
weather.trigger('plough', { selector: '[data-plough-route]', speed: 90 });Brush a snowy edge with the pointer. Hold Shift and drag near deposits to gather a snowball, then release to throw. Stack three compatible resting balls on one support to make a snowman. For a plough, mark its chosen shelf with data-plough-route.
Touch behavior preserves ordinary page scrolling. Provide accessible buttons for intentional actions; the playground includes keyboard and touch controls. Gathering, transfer, and ploughing account for stored snow mass.
const weather = createWeather({
interactions: {
snowballs: { enabled: true },
snowman: { enabled: true },
yeti: { enabled: true, size: 1, cooldown: 16 },
},
});
weather.start();Give it a visible, mostly horizontal surface at least 150 px wide. Add data-weather-yeti to your preferred snowy shelf. It stays attached as the page scrolls, blinks and breathes while idle, and waits for a snowman on the same shelf. Its sequence is arrive → sneak → scoop → pack → aim → throw → celebrate (or shrug). Gathering consumes deposited snow; empty shelves cannot produce ammunition. One yeti and one projectile per instance keep the interaction bounded.
<div data-weather-surface data-weather-yeti>Room for a little rivalry.</div>// Optional demo controls. Visitors can also assemble their own snowman from three balls.
weather.addSnow(8, '[data-weather-yeti]');
weather.trigger('snowman', { selector: '[data-weather-yeti]' });
weather.trigger('yeti', { selector: '[data-weather-yeti]' });
weather.on('yeti:hit', () => console.log('Snowman down!'));
weather.updateConfig({ interactions: { yeti: { enabled: false } } });Disabled by default. Respects pause, hidden tabs and reduced-motion preferences. Try Yeti vs snowman in the playground.
| Method | Purpose |
|---|---|
start() · pause() · resume() |
Control the animation lifecycle |
stop() |
Clear visuals while keeping the instance |
destroy() |
Release canvases, observers, listeners, actors, and plugins |
setPreset(name, overrides?) · setIntensity(value) |
Change the forecast |
updateConfig(patch) · getConfig() |
Apply validated changes or read a configuration snapshot |
addSnow(depth, selector?) · clearAccumulation(selector?) |
Manage deposits |
registerSurface(element, options?) |
Register an element; returns an unregister function |
trigger(type, options?) |
Trigger a gust, burst, snowball, snowman, yeti, or plough |
use(plugin) · on(event, callback) |
Add a plugin or subscribe to events; both return cleanup functions |
getMetrics() |
Inspect actual timing, budgets, counts, and state |
Fluri respects prefers-reduced-motion by default and suspends animation in hidden tabs. Enable its optional accessible pause button with accessibility: { controls: true }, or connect your own controls. Pale snow gains subtle cool edges on light solid-color backgrounds, while custom snow colors stay yours.
Adaptive quality bounds snowfall density and reacts to slow frames. Configure particle and surface counts, pixel ratio, and FPS to fit your page. Test on your target devices: layout complexity and effects affect the cost.
Fluri is a stylized 2D simulation. Geometry and paint order approximate supported browser behavior; complex masks, perspective transforms, closed shadow-root internals, and cross-origin iframe contents have limits.
Accessibility · Performance · Browser support · Known limits
Requires Node.js 22.12+ and pnpm 10.33.2.
pnpm install --frozen-lockfile
pnpm devOpen http://127.0.0.1:5173. Explore the surface workshop, layers, real snow interactions, presets, seasonal decorations, and the 1,000-node stress scene. Export your forecast as validated JSON.
For the complete website and playground production layout:
pnpm build
pnpm previewOpen http://127.0.0.1:4173. Development and deployment →
Build and verification commands
pnpm exec playwright install chromium firefox webkit
pnpm check
pnpm test:browser
pnpm build:examples
pnpm test:frameworks
pnpm format:check
pnpm audit --audit-level high
pnpm release:checkpnpm build produces ESM and declarations in packages/*/dist, plus the website and playground in root dist/. pnpm measure records bundle sizes; pnpm benchmark runs the documented local DOM benchmark.
Website · Showcase · Playground · Docs · GitHub
Customization · Physics & layers · Troubleshooting · Changelog
Owners can register a website, verify their domain, and independently choose a public community listing or private aggregate reporting. The library has no automatic telemetry. Read the participation and privacy guide.
Found a rough edge or made something snowy? Read the contribution guide, open an issue with a useful reproduction, or share Fluri with someone building a seasonal website. Project questions: fluritheme@gmail.com. See the security policy for private vulnerability reports.
MIT — free to use, modify, and redistribute, including commercially, with the required notices. Original Fluri artwork and procedural decorations use the same license.
A little winter. Your kind of wonder. ☃️


