Skip to main content
Blog
DesignApril 28, 2026·5 min read

The particle layer: adding motion without chaos

How we designed the animated component layer to be drop-in replacements for their static counterparts — same props, same API, zero breaking changes.

The motion problem

Most animation libraries ask you to make a commitment upfront. Framer Motion wraps your components in <motion.div>. GSAP requires refs and imperative gsap.to() calls scattered through your component tree. Lottie needs a JSON animation file and a separate player component. All of them are powerful — and all of them require you to restructure your code around motion rather than adding motion to existing code.

This creates a painful dynamic in real projects. A designer asks for a subtle glow effect on the hero card. A developer looks at the component tree, sees it would require wrapping three layers of components in motion primitives, and either spends a day refactoring or files the request under “nice to have.” Motion becomes a negotiation rather than a natural part of the toolkit. The animation library that was supposed to make things delightful ends up making things difficult.

There is also a performance trap. Many animation libraries default to JavaScript-driven animations that run on the main thread, block layout recalculations, or cause jank on lower-powered devices. Reaching for a powerful library does not automatically mean getting performant animations — it often means inheriting the library's performance model whether you understand it or not.

ParticleUI's constraint: drop-in compatible

ParticleUI's animated components follow one rule: every animated component accepts the same interface as its static equivalent. GlowCard wraps any children — you do not rewrite the children, you swap the container. Marquee is just a <div> with children; you move your list inside it. Beam is a single line element you place where you would place an <hr> or a decorative rule.

In practice, switching from a plain card to GlowCard looks like this:

// Before
<div className="rounded-xl border border-border bg-surface-1 p-6">
  <h3>Your content</h3>
  <p>Nothing changes here.</p>
</div>

// After — add the glow, keep everything else identical
<GlowCard className="rounded-xl border border-border bg-surface-1 p-6">
  <h3>Your content</h3>
  <p>Nothing changes here.</p>
</GlowCard>

No new props are required. No ref forwarding. No animation state to manage. GlowCard tracks mouse position using a pointermove listener and updates a single CSS custom property that drives the radial gradient — the rest is CSS. If you decide the effect is not right for a particular screen, you swap back in 10 seconds.

Implementation: CSS over JS

The particle components are deliberately CSS-first. JavaScript is used only when CSS alone cannot express the behavior — mouse position for GlowCard, for example. Everything else is pure CSS running on the GPU compositor thread.

Marquee is the clearest example. The scrolling track is a single CSS animation with a CSS custom property for duration:

/* marquee.css (generated from component) */
@keyframes marquee-left {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

.marquee-track {
  display: flex;
  width: max-content;
  /* Duration controlled by CSS var — no JS needed for speed changes */
  animation: marquee-left var(--marquee-duration, 30s) linear infinite;
}

.marquee-track:hover {
  animation-play-state: paused;
}

The Beam component is a linear-gradient on a positioned element that animates its background-position. The GradientText component is a CSS gradient on a clipped text element. Meteors are absolutely-positioned elements with a CSS diagonal translate animation. None of these use requestAnimationFrame. None use JavaScript timers. They are all GPU-composited properties: transform, opacity, background-position.

Performance details

A few specific choices keep the particle layer lightweight even when multiple animated components appear on the same page.

will-change is applied surgically. Only the scrolling track in Marquee carries will-change: transform — not every item in the list. Applying will-change too broadly creates unnecessary GPU layers and increases memory consumption. One layer per scrolling track is the correct scope.

Reduced motion is respected unconditionally. Every animated component wraps its keyframe definitions in a @media (prefers-reduced-motion: no-preference) block. Users who have enabled the system accessibility setting see static equivalents automatically, with zero JavaScript involvement. This is not an afterthought — it is part of the component spec.

@media (prefers-reduced-motion: no-preference) {
  .marquee-track {
    animation: marquee-left var(--marquee-duration, 30s) linear infinite;
  }
}

Zero layout thrashing. Components that track mouse position (GlowCard, SpotlightCard) read getBoundingClientRect() inside the event handler and write a single CSS custom property. They never read layout properties after writing — the classic source of forced synchronous layout. The write is batched through a single style.setProperty() call per event.

When NOT to use particles

Motion is a form of attention. Used well, it directs focus and rewards interaction. Used carelessly, it competes with the content it should be highlighting. There are three situations where ParticleUI's animated components consistently make things worse, not better.

Forms. Users filling out forms are in task mode — goal-oriented, heads down. A glowing input border or a marquee of testimonials nearby creates peripheral distraction during a concentration task. Keep form UIs static.

Data tables and dashboards. Dense information displays require sustained reading. Animation in or near a table — even a subtle one — draws the eye away from the data. The motion that felt delightful on a marketing page feels hostile in an analytics dashboard. Reserve particle components for marketing, landing pages, and showcase sections.

Above-the-fold on first load. If your hero section uses a Meteors background or a Beam animation, those animations begin immediately on page load. On slow connections or low-powered devices, this competes with the content loading and can make the page feel slower than it is. Consider deferring particle animations below the fold, where they begin only when the user scrolls to them — an IntersectionObserver and a CSS class toggle is all you need.

The rule of thumb: particles are reward and delight, not decoration. They should appear where users arrive after completing something, or where you want to highlight something worth noticing. Not on every page and not in every component.

Starting small

The best way to evaluate whether particle components fit your project is to replace exactly one element. Pick a card on your marketing page and swap it for GlowCard. Load it in your browser. Move your mouse over it. Either it fits your brand or it does not — you will know in thirty seconds. If it fits, expand. If it does not, the swap-back is just as fast.

Every ParticleUI component works with zero props. <GlowCard>, <Marquee>, <Beam /> — all have sensible defaults. There is no required configuration, no theme provider to wrap, no initialization call. The zero-config rule is part of the design: if you have to read documentation before seeing something move, the component has failed its first job.

Start with one. It is enough to know if the direction is right.

The particle layer: adding motion without chaos — ParticleUI Blog | ParticleUI