Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/apursley2012/witch-dev/llms.txt

Use this file to discover all available pages before exploring further.

BackgroundEffects is a purely decorative, non-interactive background layer that sits beneath all page content at z-0. It combines three visual layers — a tiled pentagram SVG texture, animated fog blobs, and 40 floating acid-green particles — to build the atmospheric, otherworldly environment that defines witch-dev’s visual identity.

Layer composition

The component renders a single fixed inset-0 container with pointer-events-none and overflow-hidden, ensuring no visual element ever captures clicks or extends beyond the viewport. Inside that container, three layers stack from bottom to top:
LayerTechniqueZ-order
Pentagram tileSVG data-URI background-imageBottom
Fog blobsBlurred div circles + CSS animationMiddle
Floating particlesFramer Motion motion.div elementsTop

Pentagram tile

The lowest layer tiles a small pentagram SVG across the entire background using a data:image/svg+xml URI as a CSS background-image. The tile repeats at 200px × 200px intervals.
<div
  style={{
    backgroundImage: `url("data:image/svg+xml,...pentagram SVG...")`,
    backgroundSize: '200px 200px',
  }}
  // opacity ~3%, mix-blend-mode: screen
/>
At roughly 3% opacity with mix-blend-screen, the pattern is nearly subliminal — it adds subtle visual texture without competing with content or the brighter particle effects above it.
mix-blend-screen means the pentagram only brightens pixels beneath it, never darkens them. On the near-black bg-coven-black base this keeps the texture invisible in dark regions and faintly visible over slightly lighter surfaces.

Fog blobs

Three oversized, heavily blurred div circles are wrapped in a container that is scaled to inset-[−50%] (150% of the viewport in each direction) so the blurred edges never produce a visible hard boundary. The wrapper animates continuously with the custom animate-fog-drift keyframe class.
<div className="absolute -inset-[50%] opacity-30 animate-fog-drift">
  {/* Blob 1 — large purple cloud */}
  <div className="... w-[60vw] h-[60vw] rounded-full
                  bg-coven-purple-900/40 blur-[120px] mix-blend-screen" />

  {/* Blob 2 — dark dampener */}
  <div className="... w-[50vw] h-[50vw] rounded-full
                  bg-coven-dark/80 blur-[100px] mix-blend-multiply" />

  {/* Blob 3 — lighter purple accent */}
  <div className="... w-[40vw] h-[40vw] rounded-full
                  bg-coven-purple-800/20 blur-[100px] mix-blend-screen" />
</div>
  • Blob 1 (60vw, blur-[120px], mix-blend-screen) — the dominant purple haze that warms the background.
  • Blob 2 (50vw, blur-[100px], mix-blend-multiply) — a dark blob that selectively deepens shadows, adding depth contrast to the bright purple.
  • Blob 3 (40vw, blur-[100px], mix-blend-screen) — a softer secondary accent that shifts subtly against Blob 1 as animate-fog-drift runs.
The animate-fog-drift keyframes (defined in the Tailwind config) slowly translate and rotate the entire wrapper, giving the impression that the atmosphere is slowly breathing.

Floating particles

Forty acid-green particles float upward from random positions across the viewport, fading in and drifting out the top of the screen in a continuous loop.

Particle configuration

The particle array is generated once with useMemo so it is never recalculated on re-render:
const particles = useMemo(() =>
  Array.from({ length: 40 }).map((_, i) => ({
    id: i,
    x: Math.random() * 100,        // horizontal start position (%)
    y: Math.random() * 100,        // vertical start position (%)
    size: Math.random() * 4 + 1,   // diameter: 1–5px
    duration: Math.random() * 10 + 10, // animation loop: 10–20s
    delay: Math.random() * 5,      // stagger start: 0–5s
  })),
[]);
Each property is randomised so particles are naturally distributed and no two follow the same path or timing.

Framer Motion animation

animate={{
  y: ['0vh', '-100vh'],                          // float upward off screen
  x: ['0vw', `${Math.random() * 20 - 10}vw`],   // ±10vw horizontal drift
  opacity: [0, 0.8, 0],                          // fade in, hold, fade out
}}
transition={{
  duration: particle.duration,   // 10–20s
  repeat: Infinity,
  delay: particle.delay,         // 0–5s stagger
  ease: 'linear',
}}
The opacity keyframe array [0, 0.8, 0] creates a natural fade-in/fade-out arc over the full duration. ease: 'linear' keeps vertical movement at a constant rate so the particles feel like floating embers rather than spring-driven objects. Each particle is styled as a tiny rounded circle with an acid-green glow:
<motion.div
  key={p.id}
  className="absolute rounded-full bg-coven-green-400"
  style={{
    width: p.size,
    height: p.size,
    left: `${p.x}%`,
    top: `${p.y}%`,
    boxShadow: '0 0 8px 2px rgba(163, 230, 53, 0.6)',
  }}
  animate={...}
  transition={...}
/>

Performance considerations

BackgroundEffects is designed to run continuously without impacting page interactivity:
  • pointer-events: none — the entire component is removed from the hit-testing tree; clicks pass straight through to page content.
  • z-index: 0 — sits below all page content (z-10 and above) and the navigation sidebar (z-50).
  • useMemo — the particle configuration array is computed once on mount. Random values are not regenerated on every render.
  • mix-blend-screen / mix-blend-multiply — GPU-composited blend modes avoid expensive JavaScript repaints for the fog layer.
  • Framer Motion animation loop — particle animations are handled by Framer Motion’s internal RAF loop and run on the compositor thread where possible, keeping the main thread free.

Customizing particle count

Change the number 40 in Array.from({ length: 40 }) to any value:
// Reduce to 20 for a lighter atmospheric effect
Array.from({ length: 20 }).map((_, i) => ({ ... }))

// Increase to 60 for a denser particle field
Array.from({ length: 60 }).map((_, i) => ({ ... }))
Each particle is an independent Framer Motion motion.div with its own animation subscription. Significantly increasing the count (e.g. beyond 80–100) may introduce frame-rate pressure on lower-powered devices. Test on target hardware before shipping a higher value.

Build docs developers (and LLMs) love