Mantine Scene

Logo

@gfazioli/mantine-scene

A composable decorative background component for React applications built with Mantine. Layer gradients, glow blobs, dot grids, mesh gradients, and noise textures to create rich atmospheric backgrounds.

Installation

yarn add @gfazioli/mantine-scene

After installation import package styles at the root of your application:

import '@gfazioli/mantine-scene/styles.css';

You can import styles within a layer @layer mantine-scene by importing @gfazioli/mantine-scene/styles.layer.css file.

import '@gfazioli/mantine-scene/styles.layer.css';

Usage

Scene is a decorative background container that composes multiple visual layers: gradients, glow blobs, dot grids, mesh gradients, noise textures, star fields, shooting stars, snow, and auroras. Place it behind your content to create rich, atmospheric backgrounds.

The root element has aria-hidden="true" since it is purely decorative.

Fullscreen

Set fullscreen to cover the entire viewport with position: fixed. The scene sits behind all content at z-index: 0 by default (configurable via the zIndex prop).

See the fullscreen demo page →

Decorative background

Scene renders behind your content

Interactive easing
import { Box, Text } from '@mantine/core';
import { Scene } from '@gfazioli/mantine-scene';

function Demo() {
  return (
    <Box pos="relative" h={300} style={{ borderRadius: 'var(--mantine-radius-md)', overflow: 'hidden', background: 'var(--mantine-color-body)' }}>
      <Scene>
        <Scene.Gradient from="violet" fromOpacity={0.15} />
        <Scene.Glow color="violet" size={400} blur={120} opacity={0.3} top="20%" left="30%" />
        <Scene.DotGrid color="gray" opacity={0.3} spacing={24} />
        <Scene.Noise opacity={0.025} />
      </Scene>
      <Box pos="relative" style={{ zIndex: 1 }} p="xl">
        <Text size="xl" fw={700} c="white">Decorative background</Text>
        <Text c="dimmed" mt="sm">Scene renders behind your content</Text>
      </Box>
    </Box>
  );
}

Reduced Motion

Scene respects prefers-reduced-motion by default. Use the reducedMotion prop to control behavior:

  • 'auto' (default) — disables animations when the user has enabled reduced motion in their OS settings
  • 'always' — always disables all animations
  • 'never' — keeps animations regardless of user preference

Interactive

Set interactive on the root Scene to enable mouse tracking. Sub-components that support it will react to the cursor position automatically:

  • Scene.StarWarp — focal point follows the mouse
  • Scene.Glow — blob position follows the mouse
  • Scene.Gradient — radial/conic gradient center follows the mouse

When the mouse leaves the scene, components fall back to their default position props.

onMousePosition callback

When interactive is enabled, pass onMousePosition to receive every smoothed cursor update. The callback receives { x, y } as percentages (0–100) relative to the scene container — useful for coordinating external UI elements with the scene's tracking.

x: y:
import { useState } from 'react';
import { Scene } from '@gfazioli/mantine-scene';
import { Box, Code, Group, Stack } from '@mantine/core';

function Demo() {
  const [pos, setPos] = useState<{ x: number; y: number } | null>(null);

  return (
    <Stack gap="md">
      <Group>
        <Code>x: {pos ? pos.x.toFixed(1) : '—'}</Code>
        <Code>y: {pos ? pos.y.toFixed(1) : '—'}</Code>
      </Group>

      <Box pos="relative" h={300} style={{ borderRadius: 'var(--mantine-radius-md)', overflow: 'hidden', background: 'var(--mantine-color-body)' }}>
        <Scene interactive onMousePosition={setPos}>
          <Scene.Gradient
            type="radial"
            colors={['rgba(0, 200, 255, 0.2) 0%', 'transparent 70%']}
          />
          <Scene.Glow color="cyan" size={300} blur={120} top="50%" left="50%" />
        </Scene>
      </Box>
    </Stack>
  );
}

Lazy mode

Set lazy on the root Scene to pause all child animations and the internal mouse-tracking requestAnimationFrame loop when the scene leaves the viewport. Animations resume automatically when the scene re-enters.

This uses IntersectionObserver under the hood and is opt-in (default false) to preserve backwards compatibility. Tune the visibility threshold via lazyThreshold (0–1).

<Scene lazy lazyThreshold={0.1}>
  <Scene.StarField count={120} />
  <Scene.Glow color="violet" size={400} />
</Scene>

When paused, the root receives a data-paused attribute and a CSS rule sets animation-play-state: paused on every child element — no React re-render or remount required.

Scroll the page — when this scene leaves the viewport, all child animations and the mouse-tracking rAF loop pause. They resume automatically when it re-enters.

import { Scene } from '@gfazioli/mantine-scene';
import { Box, Stack, Text } from '@mantine/core';

function Demo() {
  return (
    <Stack gap="md">
      <Text size="sm" c="dimmed">
        Scroll the page — when this scene leaves the viewport,
        all child animations and the mouse-tracking rAF loop pause.
        They resume automatically when it re-enters.
      </Text>

      <Box pos="relative" h={300} style={{ borderRadius: 'var(--mantine-radius-md)', overflow: 'hidden', background: 'var(--mantine-color-body)' }}>
        <Scene lazy>
          <Scene.Gradient
            type="radial"
            colors={['rgba(120, 0, 255, 0.3) 0%', 'transparent 70%']}
          />
          <Scene.StarField count={120} twinkle />
          <Scene.Glow color="violet" size={400} blur={140} top="50%" left="50%" />
        </Scene>
      </Box>
    </Stack>
  );
}

Responsive Props

Some props accept Mantine's StyleProp<T> — either a static value or a breakpoint object (e.g. { base: 30, md: 100 }). CSS-derivable values (sizes, spacings) are emitted as scoped CSS variables with @media queries via Mantine's InlineStyles, so they update with zero React re-renders on viewport changes — the same pattern used by Mantine core's SimpleGrid.

Supported responsive props:

  • Scene.Glowsize, blur (CSS-driven, no re-render)
  • Scene.DotGridspacing (CSS-driven, no re-render)
  • Scene.StarFieldcount (resolved in JS — particles re-seed at breakpoints)
  • Scene.StarWarpcount (resolved in JS)
  • Scene.Snowcount (resolved in JS)
  • Scene.Raincount (resolved in JS)
  • Scene.Confetticount (resolved in JS)
import { Scene } from '@gfazioli/mantine-scene';

function Demo() {
  return (
    <Scene>
      {/* Fewer stars on small screens for performance */}
      <Scene.StarField count={{ base: 30, sm: 60, md: 100 }} />

      {/* Smaller glow on mobile */}
      <Scene.Glow
        color="violet"
        size={{ base: 200, md: 400 }}
        blur={{ base: 60, md: 120 }}
      />

      {/* Tighter dot grid on small screens */}
      <Scene.DotGrid color="gray" spacing={{ base: 16, md: 24 }} />

      {/* Lighter rain on mobile */}
      <Scene.Rain count={{ base: 40, md: 100 }} angle={15} />
    </Scene>
  );
}

Plain number values continue to work as before — responsive objects are entirely opt-in. The legacy ResponsiveValue<T> type re-export remains for backwards compatibility but is now an alias for StyleProp<T> and is marked as deprecated.

Responsive props

base

Resize the browser to see stars, glow, and dot spacing adapt

import { Badge, Box, Group, Text } from '@mantine/core';
import { useMatches } from '@mantine/core';
import { Scene } from '@gfazioli/mantine-scene';

function Demo() {
  const breakpoint = useMatches({
    base: 'base',
    xs: 'xs',
    sm: 'sm',
    md: 'md',
    lg: 'lg',
    xl: 'xl',
  });

  return (
    <Box pos="relative" h={300} style={{ borderRadius: 'var(--mantine-radius-md)', overflow: 'hidden', background: 'var(--mantine-color-body)' }}>
      <Scene>
        <Scene.StarField
          count={{ base: 20, sm: 50, md: 100 }}
          twinkle
          opacity={0.7}
        />
        <Scene.Glow
          color="violet"
          size={{ base: 150, sm: 250, md: 400 }}
          blur={{ base: 40, sm: 80, md: 120 }}
          opacity={0.4}
          top="30%"
          left="40%"
        />
        <Scene.DotGrid
          color="gray"
          spacing={{ base: 14, sm: 20, md: 28 }}
          opacity={0.2}
          fade="radial"
        />
        <Scene.Noise opacity={0.02} />
      </Scene>
      <Box pos="relative" style={{ zIndex: 1 }} p="xl">
        <Group>
          <Text size="lg" fw={700} c="white">Responsive props</Text>
          <Badge variant="filled" color="violet" size="lg">{breakpoint}</Badge>
        </Group>
        <Text c="dimmed" mt="xs" size="sm">
          Resize the browser to see stars, glow, and dot spacing adapt
        </Text>
      </Box>
    </Box>
  );
}

Gradient

Scene.Gradient supports three types: radial (default), linear, and conic. Use from and to with Mantine theme colors, or pass raw CSS color stops via colors.

Set animate to enable rotate (hue-rotate) or pulse (opacity oscillation) animations.

From
To
From opacity
To opacity
Angle
Opacity
Duration
Pulse min opacity
Interactive easing
import { Scene } from '@gfazioli/mantine-scene';
import { Box } from '@mantine/core';

function Demo() {
  return (
    <Box pos="relative" h={300} style={{ borderRadius: 'var(--mantine-radius-md)', overflow: 'hidden', background: 'var(--mantine-color-body)' }}>
      <Scene>
        <Scene.Gradient from="violet" to="pink" />
      </Scene>
    </Box>
  );
}

Glow

Scene.Glow renders animated floating blobs. Each blob can be positioned, sized, and animated independently. Disable animation with animate={false}.

Choose between animation types: float (default), pulse (scale oscillation), and breathe (opacity oscillation). Use delay to stagger multiple blobs.

Color
Size
Blur
Opacity
Top
Left
Duration
Delay
Drift x
Drift y
Interactive easing
import { Scene } from '@gfazioli/mantine-scene';
import { Box } from '@mantine/core';

function Demo() {
  return (
    <Box pos="relative" h={350} style={{ borderRadius: 'var(--mantine-radius-md)', overflow: 'hidden', background: 'var(--mantine-color-body)' }}>
      <Scene>
        <Scene.Glow top={50} />
      </Scene>
    </Box>
  );
}

DotGrid

Scene.DotGrid creates a repeating dot pattern. Enable stagger for a honeycomb-like layout. Use fade to mask the edges (radial, top, bottom, or edges).

Color
Dot size
Spacing
Duration
Opacity
import { Scene } from '@gfazioli/mantine-scene';
import { Box } from '@mantine/core';

function Demo() {
  return (
    <Box pos="relative" h={300} style={{ borderRadius: 'var(--mantine-radius-md)', overflow: 'hidden', background: 'var(--mantine-color-body)' }}>
      <Scene>
        <Scene.DotGrid />
      </Scene>
    </Box>
  );
}

Mesh

Scene.Mesh overlays multiple radial gradients to simulate a mesh gradient effect. Define color stops with position and spread.

Set animate to enable a hue-rotate shift. Use blend to change the CSS mix-blend-mode.

Opacity
Duration
import { Scene } from '@gfazioli/mantine-scene';
import { Box } from '@mantine/core';

function Demo() {
  return (
    <Box pos="relative" h={300} style={{ borderRadius: 'var(--mantine-radius-md)', overflow: 'hidden', background: 'var(--mantine-color-body)' }}>
      <Scene>
        <Scene.Mesh
          stops={[
            { color: 'violet', position: '20% 20%', spread: 50 },
            { color: 'pink', position: '80% 30%', spread: 45 },
            { color: 'blue', position: '50% 80%', spread: 55 },
          ]}
        />
      </Scene>
    </Box>
  );
}

Noise

Scene.Noise adds a film grain texture via an inline SVG filter. Control the noise with seed, type, and octaves. Use tint with a Mantine theme color to overlay a color tint.

Opacity
Grain
Seed
Octaves
Tint
Tint opacity
import { Scene } from '@gfazioli/mantine-scene';
import { Box } from '@mantine/core';

function Demo() {
  return (
    <Box pos="relative" h={300} style={{ borderRadius: 'var(--mantine-radius-md)', overflow: 'hidden', background: 'var(--mantine-color-body)' }}>
      <Scene>
        <Scene.Noise opacity={0.15} tint="" />
      </Scene>
    </Box>
  );
}

StarField

Scene.StarField renders a CSS-only star field using box-shadow on a single element per layer. Stars are positioned deterministically via a PRNG seeded with the seed prop.

Enable twinkle for a staggered opacity animation across three layers.

Count
Color
Min size
Max size
Duration
Seed
Opacity
import { Scene } from '@gfazioli/mantine-scene';
import { Box } from '@mantine/core';

function Demo() {
  return (
    <Box pos="relative" h={300} style={{ borderRadius: 'var(--mantine-radius-md)', overflow: 'hidden', background: 'var(--mantine-color-body)' }}>
      <Scene>
        <Scene.StarField color="blue" />
      </Scene>
    </Box>
  );
}

StarWarp

Scene.StarWarp creates a classic hyperspace / warp speed effect where stars radiate from a configurable focal point. Stars scale up as they move outward, simulating depth and perspective.

Use direction to choose between flying away (out) or approaching (in). Move the focal point with focalX / focalY to shift the vanishing point. Enable trail for elongated streaks.

Count
Speed
Focal x
Focal y
Color
Min size
Max size
Opacity
Seed
Interactive easing
import { Scene } from '@gfazioli/mantine-scene';
import { Box } from '@mantine/core';

function Demo() {
  return (
    <Box pos="relative" h={400} style={{ borderRadius: 'var(--mantine-radius-md)', overflow: 'hidden', background: 'var(--mantine-color-body)' }}>
      <Scene>
        <Scene.StarWarp color="blue" />
      </Scene>
    </Box>
  );
}

ShootingStar

Scene.ShootingStar creates streaks of light that traverse the scene at a configurable angle. Each trail has a gradient from transparent to the chosen color.

Control count (number of lanes), trailLength, speed, and minInterval/maxInterval between appearances.

Count
Color
Trail length
Angle
Speed
Min interval
Max interval
Seed
Opacity
import { Scene } from '@gfazioli/mantine-scene';
import { Box } from '@mantine/core';

function Demo() {
  return (
    <Box pos="relative" h={300} style={{ borderRadius: 'var(--mantine-radius-md)', overflow: 'hidden', background: 'var(--mantine-color-body)' }}>
      <Scene>
        <Scene.ShootingStar count={50} color="blue" angle={150} minInterval={1} maxInterval={3} />
      </Scene>
    </Box>
  );
}

Snow

Scene.Snow renders falling snowflakes with horizontal drift. Each flake has a randomized size, speed, and position (deterministic via seed).

Use wind (-1 to 1) to bias the drift direction and speed as a multiplier on fall duration.

Count
Color
Min size
Max size
Speed
Drift
Wind
Opacity
import { Scene } from '@gfazioli/mantine-scene';
import { Box } from '@mantine/core';

function Demo() {
  return (
    <Box pos="relative" h={300} style={{ borderRadius: 'var(--mantine-radius-md)', overflow: 'hidden', background: 'var(--mantine-color-body)' }}>
      <Scene>
        <Scene.Snow color="blue" />
      </Scene>
    </Box>
  );
}

Rain

Scene.Rain renders falling raindrops as angled streaks. Customize count, color, streak angle (degrees), thickness, and speed. Min/max streak length are randomized per drop.

Enable splash to render expanding splash rings at the bottom edge. Tune the effect with splashCount, splashSize (base diameter), splashOpacity, and splashThickness (ring width). splashColor overrides the default (which mirrors color).

For a thunderstorm effect, combine Scene.Rain with a bright Scene.Glow to simulate a lightning flash.

Count
Color
Angle
Speed
Min length
Max length
Thickness
Opacity
Splash count
Splash size
Splash opacity
Splash thickness
import { Scene } from '@gfazioli/mantine-scene';
import { Box } from '@mantine/core';

function Demo() {
  return (
    <Box pos="relative" h={300} style={{ borderRadius: 'var(--mantine-radius-md)', overflow: 'hidden', background: 'var(--mantine-color-body)' }}>
      <Scene>
        <Scene.Rain count={80} />
      </Scene>
    </Box>
  );
}

Confetti

Scene.Confetti renders multi-colored falling confetti pieces with rotation and horizontal flutter. Each piece picks one shape from shapes ('rectangle', 'circle', or 'triangle') and one color from the colors palette (Mantine theme colors).

Tune count, duration, flutter (sway range in px), and the size range with minSize/maxSize.

Origin: top vs bottom

Set origin="bottom" to flip the animation into a "confetti cannon" — pieces shoot upward in a parabolic arc, peak at rise pixels above the bottom edge, then fall back down. Pair it with burst for a celebratory one-shot blast triggered from a button.

<Scene>
  <Scene.Confetti
    origin="bottom"
    rise={400}
    burst
    count={120}
    shapes={['rectangle', 'triangle']}
  />
</Scene>

Continuous mode

By default Scene.Confetti loops continuously, emitting pieces in an infinite stream. Use the configurator below to tune count, sizes, flutter, and timing.

Count
Duration
Speed
Min size
Max size
Flutter
Opacity
Origin
Rise
import { Scene } from '@gfazioli/mantine-scene';
import { Box } from '@mantine/core';

function Demo() {
  return (
    <Box pos="relative" h={300} style={{ borderRadius: 'var(--mantine-radius-md)', overflow: 'hidden', background: 'var(--mantine-color-body)' }}>
      <Scene>
        <Scene.Confetti />
      </Scene>
    </Box>
  );
}

Burst mode with onComplete

Set burst to run a single one-shot animation instead of looping. Combine it with a React key to retrigger the burst on demand — the onComplete callback fires once the last piece has settled.

<Scene>
  <Scene.Confetti
    key={trigger}
    count={120}
    burst
    duration={3.5}
    onComplete={() => console.log('settled')}
  />
</Scene>

Triggered: 0

Completed: 0

import { useState } from 'react';
import { Scene } from '@gfazioli/mantine-scene';
import { Box, Button, Group, Stack, Text } from '@mantine/core';

function Demo() {
  const [bursts, setBursts] = useState(0);
  const [completed, setCompleted] = useState(0);

  return (
    <Stack>
      <Group justify="space-between">
        <Group gap="xs">
          <Text size="sm">Triggered: {bursts}</Text>
          <Text size="sm">Completed: {completed}</Text>
        </Group>
        <Button onClick={() => setBursts((n) => n + 1)}>Trigger burst</Button>
      </Group>

      <Box pos="relative" h={280} style={{ borderRadius: 'var(--mantine-radius-md)', overflow: 'hidden', background: 'var(--mantine-color-body)' }}>
        <Scene>
          {bursts > 0 && (
            <Scene.Confetti
              key={bursts}
              count={120}
              burst
              origin="bottom"
              rise={400}
              duration={3.5}
              shapes={['rectangle', 'triangle', 'circle']}
              onComplete={() => setCompleted((n) => n + 1)}
            />
          )}
        </Scene>
      </Box>
    </Stack>
  );
}

Aurora

Scene.Aurora creates shimmering aurora borealis bands. Each band is a blurred, colored rectangle with a wave animation.

Customize colors (Mantine theme colors), bands count, position (top/center/bottom), blur, and opacity.

Bands
Duration
Opacity
Blur
import { Scene } from '@gfazioli/mantine-scene';
import { Box } from '@mantine/core';

function Demo() {
  return (
    <Box pos="relative" h={300} style={{ borderRadius: 'var(--mantine-radius-md)', overflow: 'hidden', background: 'var(--mantine-color-body)' }}>
      <Scene>
        <Scene.Aurora />
      </Scene>
    </Box>
  );
}

Waves

Scene.Waves renders a stack of parallax SVG wave layers that pan horizontally for a soothing, ocean-like background. Each layer's path is generated deterministically from the seed and tiles seamlessly under a translateX(-50%) CSS animation — no requestAnimationFrame, no canvas, no React re-renders during the animation.

Pass a single Mantine theme color to derive shades across layers automatically (back layers use lighter shades, front layers darker), or an explicit array to map a specific color per layer. Use position ('top' / 'bottom') to anchor the waves to either edge.

Use direction ('left' / 'right') to flip the horizontal pan, and parallax to control the speed differential between foreground and background layers:

  • parallax={0} flattens parallax — all layers move at the same speed
  • parallax={1} (default) is the standard depth effect
  • parallax={2} exaggerates the differential
  • Negative values invert the parallax (background layers faster than foreground), creating a dreamy reverse-depth feel
<Scene>
  <Scene.Waves
    count={4}
    colors="violet"
    amplitude={50}
    wavelength={520}
    height={300}
    direction="right"
    parallax={1.5}
    position="bottom"
  />
</Scene>
Count
Colors
Amplitude
Wavelength
Height
Speed
Direction
Parallax
Position
Blur
Opacity
import { Scene } from '@gfazioli/mantine-scene';
import { Box } from '@mantine/core';

function Demo() {
  return (
    <Box pos="relative" h={300} style={{ borderRadius: 'var(--mantine-radius-md)', overflow: 'hidden', background: 'var(--mantine-color-body)' }}>
      <Scene>
        <Scene.Waves height={200} />
      </Scene>
    </Box>
  );
}

Combined

All sub-components can be composed freely. Layer order follows DOM order.

All effects combined

StarField + Gradient + DotGrid + Mesh + Glow + ShootingStar + Noise

import { Box, Text } from '@mantine/core';
import { Scene } from '@gfazioli/mantine-scene';

function Demo() {
  return (
    <Box pos="relative" h={400} style={{ borderRadius: 'var(--mantine-radius-md)', overflow: 'hidden', background: 'var(--mantine-color-body)' }}>
      <Scene>
        <Scene.StarField count={80} twinkle duration={4} opacity={0.6} />
        <Scene.Gradient from="violet" fromOpacity={0.12} />
        <Scene.DotGrid color="gray" opacity={0.15} spacing={28} fade="edges" />
        <Scene.Mesh
          stops={[
            { color: 'blue', position: '15% 20%', spread: 45 },
            { color: 'teal', position: '85% 40%', spread: 40 },
            { color: 'violet', position: '50% 80%', spread: 50 },
          ]}
          opacity={0.3}
        />
        <Scene.Glow color="blue" size={500} opacity={0.2} top="20%" left="30%" duration={10} />
        <Scene.Glow color="teal" size={400} opacity={0.15} top="60%" left="70%" duration={14} />
        <Scene.ShootingStar count={2} opacity={0.4} />
        <Scene.Noise opacity={0.03} />
      </Scene>
      <Box pos="relative" style={{ zIndex: 1 }} p="xl">
        <Text size="xl" fw={700} c="white">All effects combined</Text>
        <Text c="dimmed" mt="sm">StarField + Gradient + DotGrid + Mesh + Glow + ShootingStar + Noise</Text>
      </Box>
    </Box>
  );
}