Skip to main content
Supabase Storage provides built-in image transformation capabilities, allowing you to resize, crop, and optimize images dynamically without storing multiple versions.

Overview

Image transformations are applied via URL parameters, making it easy to serve images at different sizes and formats:
  • Resize: Scale images to specific dimensions
  • Crop: Extract portions of images
  • Format Conversion: Convert between image formats
  • Quality Control: Optimize file size
  • Auto-optimization: Automatic format and quality selection
Image transformations work only on public buckets and require the image transformation addon.

Basic Transformations

Resize Image

Resize images by width and height:
const { data } = supabase
  .storage
  .from('avatars')
  .getPublicUrl('avatar.png', {
    transform: {
      width: 500,
      height: 500,
    },
  })

console.log(data.publicUrl)
// Returns: https://<project>.supabase.co/storage/v1/render/image/public/avatars/avatar.png?width=500&height=500

Resize by Width Only

Maintain aspect ratio by specifying only width:
const { data } = supabase
  .storage
  .from('images')
  .getPublicUrl('photo.jpg', {
    transform: {
      width: 800,
    },
  })

// Image will be 800px wide, height calculated automatically

Resize by Height Only

const { data } = supabase
  .storage
  .from('images')
  .getPublicUrl('photo.jpg', {
    transform: {
      height: 600,
    },
  })

Responsive Images

Generate multiple image sizes for responsive design:
React Responsive Image
import { supabase } from './supabaseClient'

export default function ResponsiveImage({ path, alt }) {
  const bucketName = 'images'

  const getSrcSet = () => {
    const sizes = [400, 800, 1200, 1600]
    
    return sizes.map(width => {
      const { data } = supabase
        .storage
        .from(bucketName)
        .getPublicUrl(path, {
          transform: { width }
        })
      
      return `${data.publicUrl} ${width}w`
    }).join(', ')
  }

  const { data } = supabase
    .storage
    .from(bucketName)
    .getPublicUrl(path, {
      transform: { width: 800 }
    })

  return (
    <img
      src={data.publicUrl}
      srcSet={getSrcSet()}
      sizes="(max-width: 768px) 100vw, 50vw"
      alt={alt}
    />
  )
}

Resize Modes

Cover Mode

Crop image to fill dimensions exactly:
const { data } = supabase
  .storage
  .from('images')
  .getPublicUrl('photo.jpg', {
    transform: {
      width: 500,
      height: 500,
      resize: 'cover', // Default
    },
  })

Contain Mode

Resize to fit within dimensions:
const { data } = supabase
  .storage
  .from('images')
  .getPublicUrl('photo.jpg', {
    transform: {
      width: 500,
      height: 500,
      resize: 'contain',
    },
  })

Fill Mode

Resize and add padding to exact dimensions:
const { data } = supabase
  .storage
  .from('images')
  .getPublicUrl('photo.jpg', {
    transform: {
      width: 500,
      height: 500,
      resize: 'fill',
    },
  })

Quality Control

Control image quality (1-100):
const { data } = supabase
  .storage
  .from('images')
  .getPublicUrl('photo.jpg', {
    transform: {
      width: 800,
      quality: 80, // Balance between quality and file size
    },
  })
// quality: 100 - Maximum quality, larger file size
transform: { width: 800, quality: 100 }

Format Conversion

Convert images to different formats:
// Convert to WebP
const { data } = supabase
  .storage
  .from('images')
  .getPublicUrl('photo.jpg', {
    transform: {
      width: 800,
      format: 'webp',
    },
  })

// Supported formats: webp, jpg, jpeg, png, avif

Auto Format

Automatically select the best format:
const { data } = supabase
  .storage
  .from('images')
  .getPublicUrl('photo.jpg', {
    transform: {
      width: 800,
      format: 'origin', // Use original format
    },
  })

Advanced Transformations

Thumbnail Generator

Thumbnail Component
import { supabase } from './supabaseClient'

export default function Thumbnail({ path, size = 150 }) {
  const { data } = supabase
    .storage
    .from('images')
    .getPublicUrl(path, {
      transform: {
        width: size,
        height: size,
        resize: 'cover',
        quality: 80,
      },
    })

  return (
    <img
      src={data.publicUrl}
      alt="Thumbnail"
      width={size}
      height={size}
      style={{ objectFit: 'cover', borderRadius: '8px' }}
    />
  )
}

Avatar with Fallback

Avatar Component
import { useState } from 'react'
import { supabase } from './supabaseClient'

export default function Avatar({ userId, size = 100 }) {
  const [imageError, setImageError] = useState(false)
  const avatarPath = `avatars/${userId}.png`

  const { data } = supabase
    .storage
    .from('avatars')
    .getPublicUrl(avatarPath, {
      transform: {
        width: size,
        height: size,
        resize: 'cover',
      },
    })

  if (imageError) {
    // Fallback to default avatar
    return (
      <div
        style={{
          width: size,
          height: size,
          borderRadius: '50%',
          backgroundColor: '#ccc',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          fontSize: size / 2,
        }}
      >
        {userId[0].toUpperCase()}
      </div>
    )
  }

  return (
    <img
      src={data.publicUrl}
      alt="Avatar"
      width={size}
      height={size}
      style={{ borderRadius: '50%', objectFit: 'cover' }}
      onError={() => setImageError(true)}
    />
  )
}
Image Gallery
import { useState, useEffect } from 'react'
import { supabase } from './supabaseClient'

export default function ImageGallery({ folder = '' }) {
  const [images, setImages] = useState([])
  const [selectedImage, setSelectedImage] = useState(null)

  useEffect(() => {
    loadImages()
  }, [folder])

  async function loadImages() {
    const { data, error } = await supabase
      .storage
      .from('gallery')
      .list(folder)

    if (error) {
      console.error('Error loading images:', error)
      return
    }

    // Filter image files
    const imageFiles = data.filter(file => 
      /\.(jpg|jpeg|png|gif|webp)$/i.test(file.name)
    )
    
    setImages(imageFiles)
  }

  function getThumbnailUrl(fileName) {
    const path = folder ? `${folder}/${fileName}` : fileName
    const { data } = supabase
      .storage
      .from('gallery')
      .getPublicUrl(path, {
        transform: {
          width: 300,
          height: 300,
          resize: 'cover',
          quality: 80,
        },
      })
    return data.publicUrl
  }

  function getFullUrl(fileName) {
    const path = folder ? `${folder}/${fileName}` : fileName
    const { data } = supabase
      .storage
      .from('gallery')
      .getPublicUrl(path, {
        transform: {
          width: 1200,
          quality: 90,
        },
      })
    return data.publicUrl
  }

  return (
    <div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: '16px' }}>
        {images.map((image) => (
          <img
            key={image.name}
            src={getThumbnailUrl(image.name)}
            alt={image.name}
            style={{ width: '100%', cursor: 'pointer', borderRadius: '8px' }}
            onClick={() => setSelectedImage(image.name)}
          />
        ))}
      </div>

      {selectedImage && (
        <div
          style={{
            position: 'fixed',
            top: 0,
            left: 0,
            right: 0,
            bottom: 0,
            backgroundColor: 'rgba(0,0,0,0.9)',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            zIndex: 1000,
          }}
          onClick={() => setSelectedImage(null)}
        >
          <img
            src={getFullUrl(selectedImage)}
            alt={selectedImage}
            style={{ maxWidth: '90%', maxHeight: '90%' }}
          />
        </div>
      )}
    </div>
  )
}

Performance Optimization

Lazy Loading

Lazy Loaded Image
export default function LazyImage({ path, alt, width, height }) {
  const { data } = supabase
    .storage
    .from('images')
    .getPublicUrl(path, {
      transform: { width, height, quality: 80 },
    })

  return (
    <img
      src={data.publicUrl}
      alt={alt}
      loading="lazy"
      width={width}
      height={height}
    />
  )
}

Progressive JPEG

// Use lower quality for initial load, higher for final
const { data: lowQuality } = supabase
  .storage
  .from('images')
  .getPublicUrl('photo.jpg', {
    transform: {
      width: 800,
      quality: 20, // Low quality placeholder
    },
  })

const { data: highQuality } = supabase
  .storage
  .from('images')
  .getPublicUrl('photo.jpg', {
    transform: {
      width: 800,
      quality: 90, // High quality final image
    },
  })

CDN and Caching

Transformed images are automatically cached:
// First request: Image is transformed and cached
const url1 = getTransformedUrl('photo.jpg', { width: 800 })

// Subsequent requests: Served from cache (very fast)
const url2 = getTransformedUrl('photo.jpg', { width: 800 })

Transformation Limits

  • Maximum dimensions: 2500 x 2500 pixels
  • Maximum file size: 25MB for transformation
  • Supported formats: JPEG, PNG, WebP, GIF, AVIF
Images larger than 25MB cannot be transformed. Upload optimized images or use image processing before upload.

URL Parameters

Alternatively, use URL parameters directly:
// Base URL
const baseUrl = 'https://<project>.supabase.co/storage/v1/object/public/images/photo.jpg'

// Add transformation parameters
const transformedUrl = `${baseUrl}?width=800&height=600&resize=cover&quality=80&format=webp`
Available parameters:
  • width: Image width in pixels
  • height: Image height in pixels
  • resize: cover, contain, or fill
  • quality: 1-100
  • format: webp, jpg, png, avif

Best Practices

Use WebP Format

WebP provides better compression than JPEG/PNG

Set Appropriate Quality

Quality 80 is usually a good balance

Generate Responsive Images

Use srcset for different screen sizes

Lazy Load Images

Improve page load time with lazy loading

Common Use Cases

// Thumbnail for listing
transform: { width: 300, height: 300, resize: 'cover', quality: 80 }

// Detail view
transform: { width: 1200, quality: 90 }
// Small avatar
transform: { width: 40, height: 40, resize: 'cover' }

// Profile page avatar
transform: { width: 200, height: 200, resize: 'cover' }
// Hero image
transform: { width: 1200, quality: 85, format: 'webp' }

// Thumbnail
transform: { width: 400, height: 300, resize: 'cover', quality: 80 }

Next Steps

Access Control

Secure your images with RLS policies

Upload Files

Learn how to upload images