Cloud Particles
Volumetric drifting clouds as a decorative backdrop. Each cloud is a radial-gradient ellipse pushed through a five-pass SVG turbulence filter — body, cool underside, soft shadow, deep shadow — and translated across `130vw`.
Preview
Rendered from the story the storybook (a11y) CI gate runs axe against — the preview can't show something that hasn't been verified.
Install
Two equivalent paths — the URL works in any shadcn-CLI setup; the alias works once you've registered @interlace in your components.json.
npx shadcn@latest add https://ds.interlace.tools/r/cloud-particles.jsonnpx shadcn@latest add @interlace/cloud-particlesHistory
This component is at v1.1.0, first shipped in @interlace/ui 1.0.0. The version is stamped as a banner into the file the install writes, so the copy in your tree says which one you have — compare it with the number above before deciding whether to re-run the install.
@interlace/ui 1.1.01 entry
- Changedpatch
Six defects found by upgrading a real consumer, plus five found by reading the components closely enough to document them. **
Meteors' glow never painted.** It readvar(--color-meteor-glow)while itscssVarsdeclare--meteor-glow;--color-*is the Tailwind@themenamespace and onlycssVars.themepopulates it, so the wholebox-shadowwas invalid at computed-value time. **This was broken only for registry consumers** — our own docs site hand-declares the--color-form. **ArticleCardcropped 28% off every cover.**h-44is 176px; at a ~302px tile that is a 1.72:1 box against a 2.381:1 image. Nowaspect-[1000/420]— the ratio the card already declared on its<img>. It also gains arenderImageslot, because every Next.js consumer was re-patching the same line to usenext/imageand the design system cannot depend on it. **BorderBeamandStarsBackgroundhad noaria-hiddenat all** — six purely decorative nodes a screen reader walked. **CloudParticlesdefaultedbodyColortocurrentColor**, painting volumetric clouds in the inherited text colour. **NumberTickergainsnotation**, because six-figure metrics overflow a tile at 320px. Also:SheetComposeandDialogComposeeach mounted a second backdrop, so a composed dialog dimmed the page twice as much as the hand-composed tree the docs show;AccordiondroppedclassNameon the animated Panel;Tooltipaccepteddelayand discarded it;PopoverAnchorwas a second trigger. **useReducedMotionwas one frame late.** The canonicaluseState(false)plus effect returnsfalseon the first render, so every gated component painted one frame of exactly the motion the user turned off.useSyncExternalStorereads during render and closes that on client renders; on hydration the server cannot know the preference, which is what the stylesheet reset is for.Badgedrops'use client'— verified with a real server-component build.
Import
Single named export.
import { CloudParticles } from '@/components/ui/aceternity/cloud-particles';Anatomy
Extracted from the primitive's JSDoc header. The source is the only documentation that can't drift.
CloudParticles (div — data-slot="cloud-particles",
aria-hidden, pointer-events-none)
├─ style (per-instance `{filterId}-drift` keyframe)
├─ svg (data-slot="cloud-particles-filter" —
│ 2× feTurbulence, then 4 displaced layers
│ composited through feMerge)
└─ div ×count (data-slot="cloud-particles-cloud")
└─ div (data-slot="cloud-particles-shape")
Layout is a deterministic golden-ratio walk (`buildClouds`), not
`Math.random()`, so server and client agree and the same props always
produce the same field. `count` is clamped to `mobileCount` below
`mobileBreakpoint`. Both the keyframe and the clouds render only after
mount, so SSR emits the filter and nothing else.API reference
Parsed from the type declarations in the source — the same file the install writes into your tree, so this table can't drift from the component you get.
CloudParticlesProps
| Prop | Type | Description |
|---|---|---|
| count | number | Number of cloud particles to render. On viewports narrower than `mobileBreakpoint` this is clamped to `mobileCount` to protect the GPU budget on phones. @default 3 |
| mobileCount | number | Maximum cloud count on viewports narrower than `mobileBreakpoint`. @default 2 |
| mobileBreakpoint | number | Viewport width (px) below which `mobileCount` applies. @default 768 |
| minSpeed | number | Slowest drift duration, in seconds (each cloud picks a value in `[minSpeed, maxSpeed]`). Larger = slower. @default 150 |
| maxSpeed | number | Fastest drift duration, in seconds. @default 250 |
| minScale | number | Smallest cloud scale (1 = native 320×140px). @default 0.5 |
| maxScale | number | Largest cloud scale. @default 0.9 |
| bodyColor | string | Main cloud-body color. Any CSS color is valid; defaults resolve through a CSS custom property so the design system owns the palette. Defaults to `--scrim-foreground` — white in both schemes — because a volumetric fill is a material, not a mark, and must not invert with the surrounding text colour. Pass `currentColor` explicitly if you genuinely want the clouds to track the inherited foreground; see the file header for why that is the wrong default. @default "var(--cloud-body-color, var(--scrim-foreground))" |
| undersideColor | string | Cool underside tint that reads as light-from-above. Falls back to the theme's muted-foreground token. @default "var(--cloud-underside-color, var(--muted-foreground, currentColor))" |
| shadowColor | string | Soft drop-shadow color beneath each cloud. @default "var(--cloud-shadow-color, var(--muted-foreground, currentColor))" |
| undersideOpacity | number | Opacity of the underside tint layer (0–1). @default 0.08 |
| shadowOpacity | number | Opacity of the soft-shadow layer (0–1). @default 0.12 |
| deepShadowOpacity | number | Opacity of the deep-shadow layer (0–1). @default 0.08 |
Accessibility
Every story for this component is rendered headlessly and checked with axe-core (wcag2aa, wcag22aa, best-practice, ACT) on every PR. That gate has no continue-on-error, so what ships has zero known violations.
What follows is what static analysis can see. Axe cannot press a key and never sees an overlay open, so the operable-without-a-mouse claim lives in Behavior instead, where the keyboard path is replayed step by step.
- Focus + keyboard behaviour
- Native element semantics — no interaction layer to get wrong.
- Focus ring (WCAG 2.2 SC 2.4.13)
- Not focusable — no focus indicator required.
- Reduced motion
- Animation is gated on prefers-reduced-motion.
- ARIA in the source
- None — semantics come from the element or the Base UI primitive rather than hand-written ARIA.
Examples
3 more states from the same story file.
Dependencies
- Base UI primitive
- Native / no Base UI dependency
- Lucide icons
- none
- NPM dependencies
- none
- Registry dependencies
Source
The full implementation — components/ui/aceternity/cloud-particles.tsx once installed.
"use client";
import { ComponentPropsWithoutRef, useEffect, useId, useState } from "react";
// @interlace/cloud-particles v1.1.0 — Interlace design system.
// Docs, props and live preview: https://ds.interlace.tools/c/cloud-particles
// What changed since: https://ds.interlace.tools/c/cloud-particles#history
// Generated banner — keep it, the upgrade diff reads this version.
/**
* @interlace/ui — CloudParticles
*
* Volumetric drifting clouds as a decorative backdrop. Each cloud is a
* radial-gradient ellipse pushed through a five-pass SVG turbulence filter —
* body, cool underside, soft shadow, deep shadow — and translated across
* `130vw`.
*
* It is an `absolute inset-0` overlay: `aria-hidden`, `pointer-events-none`,
* and reserving no flow space, so the consumer owns the positioned ancestor.
*
…449 more lines…Registry JSON
The raw registry record — what the shadcn CLI fetches.