Skip to main content

Overview

Paper Shaders provide powerful sizing controls to manage how shader graphics scale and fit within their containers. This guide covers the sizing system, fit modes, and responsive strategies.

Container Sizing

Shaders automatically fill their container element:
// Use CSS to size the container
<MeshGradient
  colors={['#ff0000', '#00ff00']}
  style={{ width: 400, height: 400 }}
/>

// Or use className
<MeshGradient
  colors={['#ff0000', '#00ff00']}
  className="shader-container"
/>

// With inline width/height props
<MeshGradient
  colors={['#ff0000', '#00ff00']}
  width={400}
  height={400}
/>
Shader canvas automatically resizes when the container size changes using ResizeObserver. No manual intervention needed.

Fit Modes

The fit parameter controls how shader graphics scale within the container:

None

No automatic fitting - renders at natural scale:
<MeshGradient
  fit="none"
  scale={1}
  colors={['#ff0000', '#00ff00']}
  style={{ width: 400, height: 400 }}
/>
Use cases:
  • Tiling patterns
  • Precise pixel control
  • Coordinated multi-shader layouts

Contain

Fits entire graphic inside container while maintaining aspect ratio:
<MeshGradient
  fit="contain"
  colors={['#ff0000', '#00ff00']}
  style={{ width: 400, height: 300 }}
/>
Behavior:
  • Entire graphic visible
  • May have empty space on sides
  • Maintains aspect ratio
  • Default for most object-based shaders
Use cases:
  • Logos and graphics that must be fully visible
  • Aspect-ratio-sensitive designs
  • Card backgrounds

Cover

Fills entire container while maintaining aspect ratio:
<MeshGradient
  fit="cover"
  colors={['#ff0000', '#00ff00']}
  style={{ width: 400, height: 300 }}
/>
Behavior:
  • Fills entire container
  • May crop parts of graphic
  • Maintains aspect ratio
  • No empty space
Use cases:
  • Hero backgrounds
  • Full-bleed sections
  • When cropping is acceptable

Visual Comparison

<div style={{ width: 400, height: 200, border: '2px solid' }}>
  <MeshGradient
    fit="none"
    colors={['#ff0000', '#00ff00']}
  />
</div>
// Renders at natural scale, may overflow or have gaps

Sizing Parameters

Fine-tune shader positioning and scaling:

Scale

Zoom the shader graphic:
<MeshGradient
  fit="contain"
  scale={1.5}    // 150% zoom
  colors={['#ff0000', '#00ff00']}
/>

// Scale range: 0.01 to 4
// Default: 1
scale applies after fit calculations, so fit="contain" + scale={2} will contain the graphic then zoom it 2x (likely cropping).

Rotation

Rotate the shader graphic:
<MeshGradient
  rotation={45}   // 45 degrees clockwise
  colors={['#ff0000', '#00ff00']}
/>

// Range: 0 to 360 degrees
// Default: 0

Origin

Set rotation and scaling pivot point:
<MeshGradient
  originX={0.5}   // Center horizontally (0 = left, 1 = right)
  originY={0.5}   // Center vertically (0 = top, 1 = bottom)
  rotation={45}
  colors={['#ff0000', '#00ff00']}
/>

// Default: originX={0.5}, originY={0.5} (center)
Common origin points:
  • Top-left: originX={0}, originY={0}
  • Top-center: originX={0.5}, originY={0}
  • Center: originX={0.5}, originY={0.5} (default)
  • Bottom-right: originX={1}, originY={1}

Offset

Shift the graphic position:
<MeshGradient
  offsetX={0.2}   // Shift right by 20%
  offsetY={-0.1}  // Shift up by 10%
  colors={['#ff0000', '#00ff00']}
/>

// Range: -1 to 1 (relative to container)
// Default: offsetX={0}, offsetY={0}

World Dimensions

Define virtual canvas size for precise control:
<MeshGradient
  worldWidth={1000}   // Virtual width in arbitrary units
  worldHeight={1000}  // Virtual height in arbitrary units
  fit="contain"
  colors={['#ff0000', '#00ff00']}
/>

// Default: worldWidth={0}, worldHeight={0} (auto)
// When 0, uses container dimensions
Use cases:
  • Matching shader scale across different containers
  • Consistent sizing in responsive layouts
  • Precise alignment with other elements

Complete Sizing Example

Combine all sizing parameters:
<MeshGradient
  // Fit mode
  fit="cover"
  
  // Scaling and rotation
  scale={1.2}
  rotation={15}
  
  // Position
  originX={0.3}
  originY={0.7}
  offsetX={0.1}
  offsetY={-0.05}
  
  // Virtual dimensions
  worldWidth={1200}
  worldHeight={800}
  
  // Visual parameters
  colors={['#ff0000', '#00ff00', '#0000ff']}
  distortion={0.8}
  
  style={{ width: 600, height: 400 }}
/>

Sizing in Vanilla JS

Set sizing parameters as uniforms:
import { ShaderFitOptions } from '@paper-design/shaders';

const uniforms = {
  // Shader-specific uniforms
  u_colors: [/*...*/],
  u_colorsCount: 3,
  
  // Sizing uniforms
  u_fit: ShaderFitOptions.cover,  // 0=none, 1=contain, 2=cover
  u_scale: 1.2,
  u_rotation: 15,
  u_originX: 0.3,
  u_originY: 0.7,
  u_offsetX: 0.1,
  u_offsetY: -0.05,
  u_worldWidth: 1200,
  u_worldHeight: 800,
};

const shaderMount = new ShaderMount(
  container,
  fragmentShader,
  uniforms
);

// Update sizing dynamically
shaderMount.setUniforms({
  u_scale: 1.5,
  u_rotation: 30,
});

Responsive Strategies

Fluid Container

Let shader scale naturally with container:
<div style={{ width: '100%', maxWidth: 600 }}>
  <MeshGradient
    fit="cover"
    colors={['#ff0000', '#00ff00']}
    style={{ width: '100%', height: 400 }}
  />
</div>

Aspect Ratio Container

Maintain aspect ratio on resize:
<div style={{ width: '100%', aspectRatio: '16 / 9' }}>
  <MeshGradient
    fit="cover"
    colors={['#ff0000', '#00ff00']}
    style={{ width: '100%', height: '100%' }}
  />
</div>

Breakpoint-Based Sizing

Adjust sizing parameters by screen size:
import { useState, useEffect } from 'react';

function ResponsiveSizing() {
  const [scale, setScale] = useState(1);
  
  useEffect(() => {
    const updateScale = () => {
      if (window.innerWidth < 768) setScale(0.8);
      else if (window.innerWidth < 1024) setScale(1);
      else setScale(1.2);
    };
    
    updateScale();
    window.addEventListener('resize', updateScale);
    return () => window.removeEventListener('resize', updateScale);
  }, []);
  
  return (
    <MeshGradient
      fit="cover"
      scale={scale}
      colors={['#ff0000', '#00ff00']}
      style={{ width: '100%', height: 400 }}
    />
  );
}

Container Queries

Use container queries for component-level responsive sizing:
.shader-wrapper {
  container-type: inline-size;
}

@container (min-width: 600px) {
  .shader-container {
    height: 400px;
  }
}

@container (max-width: 599px) {
  .shader-container {
    height: 300px;
  }
}
<div className="shader-wrapper">
  <MeshGradient
    fit="cover"
    colors={['#ff0000', '#00ff00']}
    className="shader-container"
    style={{ width: '100%' }}
  />
</div>

Pattern vs Object Sizing

Different shader types have different default sizing:

Pattern Shaders

Default: fit="none" for tiling:
import { defaultPatternSizing } from '@paper-design/shaders';

console.log(defaultPatternSizing);
// {
//   fit: 'none',
//   scale: 1,
//   rotation: 0,
//   offsetX: 0,
//   offsetY: 0,
//   originX: 0.5,
//   originY: 0.5,
//   worldWidth: 0,
//   worldHeight: 0,
// }

// Examples: DotGrid, Waves, Voronoi
<DotGrid
  fit="none"  // Default
  scale={0.5} // Adjust pattern density
  colors={['#ff0000']}
/>

Object Shaders

Default: fit="contain" for full visibility:
import { defaultObjectSizing } from '@paper-design/shaders';

console.log(defaultObjectSizing);
// {
//   fit: 'contain',
//   scale: 1,
//   rotation: 0,
//   offsetX: 0,
//   offsetY: 0,
//   originX: 0.5,
//   originY: 0.5,
//   worldWidth: 0,
//   worldHeight: 0,
// }

// Examples: MeshGradient, Metaballs, SmokeRing
<MeshGradient
  fit="contain"  // Default
  colors={['#ff0000', '#00ff00']}
/>

Advanced: Coordinated Sizing

Sync sizing across multiple shaders:
function CoordinatedShaders() {
  const [rotation, setRotation] = useState(0);
  
  useEffect(() => {
    const interval = setInterval(() => {
      setRotation(r => (r + 1) % 360);
    }, 50);
    return () => clearInterval(interval);
  }, []);
  
  const sharedSizing = {
    fit: 'cover',
    scale: 1.5,
    rotation,
    worldWidth: 1000,
    worldHeight: 1000,
  };
  
  return (
    <div style={{ position: 'relative', width: 600, height: 400 }}>
      {/* Background layer */}
      <MeshGradient
        {...sharedSizing}
        colors={['#ff0000', '#00ff00']}
        style={{ position: 'absolute', inset: 0 }}
      />
      
      {/* Foreground layer */}
      <DotOrbit
        {...sharedSizing}
        colors={['#0000ff', '#ffff00']}
        style={{
          position: 'absolute',
          inset: 0,
          mixBlendMode: 'screen',
        }}
      />
    </div>
  );
}

Sizing Best Practices

  • Use cover for backgrounds that should fill completely
  • Use contain for graphics that must be fully visible
  • Use none for tiling patterns and precise layouts
  • Use percentage-based container widths
  • Consider aspect-ratio CSS property
  • Adjust scale parameter for mobile vs desktop
  • Test on actual devices, not just browser resize
  • Larger containers = more pixels = slower rendering
  • Use maxPixelCount to limit rendering resolution
  • Consider static shaders (speed={0}) for large areas
  • See Performance Guide for details
  • Use matching worldWidth and worldHeight
  • Share rotation and scale for synchronized effects
  • Consider offsetX/offsetY for layered compositions

Common Sizing Patterns

Full-Screen Background

<div style={{ width: '100vw', height: '100vh', overflow: 'hidden' }}>
  <MeshGradient
    fit="cover"
    colors={['#ff0000', '#00ff00']}
    style={{ width: '100%', height: '100%' }}
  />
</div>

Hero Section

<section style={{ position: 'relative', height: '70vh' }}>
  <MeshGradient
    fit="cover"
    colors={['#ff0000', '#00ff00']}
    style={{
      position: 'absolute',
      inset: 0,
      zIndex: -1,
    }}
  />
  <div style={{ position: 'relative', zIndex: 1 }}>
    <h1>Hero Content</h1>
  </div>
</section>

Card Background

<div style={{ position: 'relative', borderRadius: 16, overflow: 'hidden' }}>
  <MeshGradient
    fit="cover"
    colors={['#ff0000', '#00ff00']}
    style={{
      position: 'absolute',
      inset: 0,
      zIndex: -1,
    }}
  />
  <div style={{ padding: 24, position: 'relative' }}>
    <h2>Card Title</h2>
    <p>Card content</p>
  </div>
</div>

Tiling Pattern

<div style={{ width: '100%', height: 400 }}>
  <DotGrid
    fit="none"
    scale={0.5}      // Controls dot density
    colors={['#ff0000']}
    colorBack="#000000"
    style={{ width: '100%', height: '100%' }}
  />
</div>

Troubleshooting

Shader not visible

1

Check container has dimensions

Ensure container has explicit width and height (CSS or inline)
2

Verify fit mode

Try fit="cover" to ensure shader fills container
3

Check scale and offset

Ensure scale isn’t too large or offset isn’t pushing content out

Shader looks cropped

  • Try fit="contain" instead of cover
  • Reduce scale parameter
  • Adjust offset to recenter

Shader doesn’t resize

  • Verify container size actually changes (check DevTools)
  • ResizeObserver works automatically - no manual action needed
  • Check for fixed dimensions preventing resize

Next Steps

Performance

Optimize shader resolution and rendering

Customization

Customize shader visual parameters

React Usage

React-specific sizing patterns

Vanilla Usage

Vanilla JS sizing control