effects#background#texture
Noise Gradient
Animated film grain texture layered over smooth gradients for a premium aesthetic
Implementation Guide
# Noise Gradient Effect
> Animated film grain/noise texture layered over smooth gradients. Adds texture and depth to flat backgrounds - very trendy in modern design.
## Quick Start
```tsx
import NoiseGradient from '@/components/examples/effects/noise-gradient';
export default function Hero() {
return (
<div className="relative h-screen">
<NoiseGradient colors={['#0a0a0a', '#1a1a3e']} />
<div className="relative z-10">{/* Your content goes here */}</div>
</div>
);
}
```
## Props
| Prop | Type | Default | Description |
| ------------- | ---------------- | ---------------------- | --------------------------------------------- |
| colors | [string, string] | ['#0a0a0a', '#1a1a3e'] | Gradient start and end colors |
| noiseOpacity | number | 0.15 | Noise layer opacity (0 - 1) |
| noiseScale | number | 1 | Grain size multiplier (1 = fine, 2+ = coarse) |
| animated | boolean | true | Whether noise animates/shifts |
| gradientAngle | number | 135 | Gradient direction in degrees (0 - 360) |
## Full Implementation
```tsx
'use client';
import { useEffect, useRef, useCallback } from 'react';
interface NoiseGradientProps {
colors?: [string, string];
noiseOpacity?: number;
noiseScale?: number;
animated?: boolean;
gradientAngle?: number;
}
export default function NoiseGradient({
colors = ['#0a0a0a', '#1a1a3e'],
noiseOpacity = 0.15,
noiseScale = 1,
animated = true,
gradientAngle = 135,
}: NoiseGradientProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const animationRef = useRef<number>(0);
const noiseDataRef = useRef<ImageData | null>(null);
const offsetRef = useRef({ x: 0, y: 0 });
const generateNoisePattern = useCallback(
(ctx: CanvasRenderingContext2D, width: number, height: number): ImageData => {
const scaledWidth = Math.ceil(width / noiseScale);
const scaledHeight = Math.ceil(height / noiseScale);
const imageData = ctx.createImageData(scaledWidth, scaledHeight);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const value = Math.random() * 255;
data[i] = value;
data[i + 1] = value;
data[i + 2] = value;
data[i + 3] = 255;
}
return imageData;
},
[noiseScale]
);
const drawNoise = useCallback(
(
ctx: CanvasRenderingContext2D,
noiseCanvas: HTMLCanvasElement,
width: number,
height: number,
offsetX: number,
offsetY: number
) => {
ctx.save();
ctx.globalAlpha = noiseOpacity;
ctx.globalCompositeOperation = 'overlay';
// Draw noise with offset for animation effect
const patternOffsetX = Math.floor(offsetX) % noiseCanvas.width;
const patternOffsetY = Math.floor(offsetY) % noiseCanvas.height;
// Tile the noise pattern across the canvas
for (let x = -patternOffsetX; x < width; x += noiseCanvas.width) {
for (let y = -patternOffsetY; y < height; y += noiseCanvas.height) {
ctx.drawImage(
noiseCanvas,
x,
y,
noiseCanvas.width * noiseScale,
noiseCanvas.height * noiseScale
);
}
}
ctx.restore();
},
[noiseOpacity, noiseScale]
);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d', { alpha: false });
if (!ctx) return;
// Create offscreen canvas for noise pattern
const noiseCanvas = document.createElement('canvas');
const noiseCtx = noiseCanvas.getContext('2d');
if (!noiseCtx) return;
let prefersReducedMotion = false;
const resizeCanvas = () => {
const rect = canvas.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
// Generate noise pattern sized for the canvas
const noiseSize = Math.max(256, Math.ceil(Math.max(rect.width, rect.height) / noiseScale));
noiseCanvas.width = noiseSize;
noiseCanvas.height = noiseSize;
noiseDataRef.current = generateNoisePattern(noiseCtx, noiseSize, noiseSize);
noiseCtx.putImageData(noiseDataRef.current, 0, 0);
};
const drawGradient = (width: number, height: number) => {
// Convert angle to radians and calculate gradient coordinates
const angleRad = (gradientAngle * Math.PI) / 180;
const diagonal = Math.sqrt(width * width + height * height);
const centerX = width / 2;
const centerY = height / 2;
const gradientStartX = centerX - (Math.cos(angleRad) * diagonal) / 2;
const gradientStartY = centerY - (Math.sin(angleRad) * diagonal) / 2;
const gradientEndX = centerX + (Math.cos(angleRad) * diagonal) / 2;
const gradientEndY = centerY + (Math.sin(angleRad) * diagonal) / 2;
const gradient = ctx.createLinearGradient(
gradientStartX,
gradientStartY,
gradientEndX,
gradientEndY
);
gradient.addColorStop(0, colors[0]);
gradient.addColorStop(1, colors[1]);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height);
};
const render = () => {
const rect = canvas.getBoundingClientRect();
const width = rect.width;
const height = rect.height;
// Draw gradient background
drawGradient(width, height);
// Draw noise overlay
drawNoise(ctx, noiseCanvas, width, height, offsetRef.current.x, offsetRef.current.y);
};
const animate = () => {
if (prefersReducedMotion) {
render();
return;
}
// Slowly drift the noise pattern
offsetRef.current.x += 0.3;
offsetRef.current.y += 0.2;
render();
animationRef.current = requestAnimationFrame(animate);
};
const regenerateNoise = () => {
if (!noiseCtx || prefersReducedMotion) return;
noiseDataRef.current = generateNoisePattern(noiseCtx, noiseCanvas.width, noiseCanvas.height);
noiseCtx.putImageData(noiseDataRef.current, 0, 0);
};
// Check for reduced motion preference
const motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
prefersReducedMotion = motionQuery.matches;
const handleMotionChange = (event: MediaQueryListEvent) => {
prefersReducedMotion = event.matches;
if (prefersReducedMotion) {
cancelAnimationFrame(animationRef.current);
render();
} else if (animated) {
animate();
}
};
motionQuery.addEventListener('change', handleMotionChange);
resizeCanvas();
if (animated && !prefersReducedMotion) {
animate();
// Periodically regenerate noise for more organic feel
const noiseInterval = setInterval(regenerateNoise, 100);
const handleResize = () => {
resizeCanvas();
};
window.addEventListener('resize', handleResize);
return () => {
cancelAnimationFrame(animationRef.current);
clearInterval(noiseInterval);
window.removeEventListener('resize', handleResize);
motionQuery.removeEventListener('change', handleMotionChange);
};
} else {
render();
const handleResize = () => {
resizeCanvas();
render();
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
motionQuery.removeEventListener('change', handleMotionChange);
};
}
}, [colors, noiseOpacity, noiseScale, animated, gradientAngle, generateNoisePattern, drawNoise]);
return (
<canvas
ref={canvasRef}
className="h-full w-full"
style={{
display: 'block',
}}
/>
);
}
```
## Customization
### Dark Moody Gradient
Classic dark gradient with subtle film grain:
```tsx
<NoiseGradient colors={['#0a0a0a', '#1a1a3e']} noiseOpacity={0.12} gradientAngle={135} />
```
### Warm Sunset
Warm tones with visible grain texture:
```tsx
<NoiseGradient colors={['#1a0a0a', '#3e2a1a']} noiseOpacity={0.2} gradientAngle={180} />
```
### Cool Ocean
Blue-green gradient with fine noise:
```tsx
<NoiseGradient colors={['#0a1a1a', '#1a3e3e']} noiseOpacity={0.1} noiseScale={0.5} />
```
### High Contrast Grain
Bold gradient with heavy film grain effect:
```tsx
<NoiseGradient colors={['#000000', '#2a2a4a']} noiseOpacity={0.25} noiseScale={2} />
```
### Static Background
Non-animated version for performance-sensitive contexts:
```tsx
<NoiseGradient colors={['#0f0f1a', '#1a1a2e']} animated={false} noiseOpacity={0.15} />
```
### Horizontal Gradient
Change gradient direction:
```tsx
<NoiseGradient colors={['#1a0a2e', '#0a1a1a']} gradientAngle={90} />
```
### Vertical Gradient
Top to bottom gradient:
```tsx
<NoiseGradient colors={['#0a0a1a', '#1a1a3e']} gradientAngle={180} />
```
### Brand Colors Example
Using brand colors with noise overlay:
```tsx
<NoiseGradient colors={['#007AFF', '#5856D6']} noiseOpacity={0.08} gradientAngle={135} />
```
## Performance Considerations
- **Canvas-based rendering** - Uses hardware-accelerated 2D canvas for smooth performance
- **Offscreen noise generation** - Noise pattern is generated on a separate canvas and reused
- **Noise regeneration interval** - Noise updates every 100ms for organic feel without overwhelming the GPU
- **Resize handling** - Canvas properly resizes with device pixel ratio support for crisp rendering on retina displays
- **Animation frame management** - Uses `requestAnimationFrame` for smooth 60fps animation
- **Static mode available** - Set `animated={false}` for static backgrounds to eliminate animation overhead
- **Memory efficient** - Reuses ImageData buffers rather than creating new ones each frame
### Mobile Optimization
For mobile devices, consider these adjustments:
```tsx
// Reduced animation intensity for mobile
<NoiseGradient
noiseOpacity={0.1}
noiseScale={2} // Coarser grain = fewer pixels to process
animated={true}
/>
```
## Accessibility
- **Respects `prefers-reduced-motion`** - Animation stops completely when user prefers reduced motion, displaying static noise instead
- **Listens for preference changes** - Dynamically responds if user toggles motion preferences during session
- **Purely decorative** - Effect is visual enhancement only; ensure content above has proper contrast
- **Content layering** - Use `position: relative; z-index: 10` on content to ensure it appears above the canvas
- **Contrast considerations** - Dark gradients work best; ensure text overlays have sufficient contrast ratios
### Reduced Motion Example
The component automatically detects and respects user preferences:
```tsx
// This will show static noise if user has prefers-reduced-motion enabled
<NoiseGradient animated={true} />
```
## Browser Support
- **Canvas 2D API** - All modern browsers (Chrome, Firefox, Safari, Edge)
- **requestAnimationFrame** - IE10+ and all modern browsers
- **ImageData manipulation** - All browsers with Canvas support
- **matchMedia** - IE10+ for `prefers-reduced-motion` detection
- **devicePixelRatio** - Full support for retina/HiDPI displays
### Fallback Considerations
For browsers without Canvas support (extremely rare), consider adding a CSS gradient fallback:
```tsx
<div className="relative h-full w-full">
{/* CSS fallback */}
<div
className="absolute inset-0"
style={{
background: 'linear-gradient(135deg, #0a0a0a 0%, #1a1a3e 100%)',
}}
/>
{/* Canvas noise overlay */}
<NoiseGradient colors={['#0a0a0a', '#1a1a3e']} />
</div>
```