The <Image> component extends the HTML <img> element with automatic image optimization, responsive sizing, modern format conversion (WebP, AVIF), and lazy loading. Image processing (encoding, resizing, format conversion) runs in Rust on the server for fast throughput.

Import

import { Image } from 'rari/image'

Configuration

Before using the Image component with remote images, configure allowed sources in your vite.config.ts:

vite.config.ts
import { rari } from 'rari/vite'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [
    rari({
      images: {
        // Allow remote images from specific domains
        remotePatterns: [
          {
            hostname: 'images.unsplash.com',
          },
          {
            protocol: 'https',
            hostname: 'cdn.example.com',
            pathname: '/images/**',
          },
        ],
        // Define local image paths
        localPatterns: [
          {
            pathname: '/images/**',
          },
        ],
        // Optional: customize optimization settings
        deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
        imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
        formats: ['avif', 'webp'],
        qualityAllowlist: [25, 50, 75, 100],
      },
    }),
  ],
})

Remote Patterns

The remotePatterns array defines which external domains are allowed to serve images. This is a security feature that prevents your application from being used as an open proxy for arbitrary images.

Each pattern can include:

  • protocol - Optional: 'http' or 'https'
  • hostname - Required: The domain name (e.g., 'images.unsplash.com')
  • port - Optional: Specific port number
  • pathname - Optional: Path pattern (e.g., '/images/**')
  • search - Optional: Query string pattern

Local Patterns

The localPatterns array defines which paths in your public directory should be optimized. Images outside these patterns will not be processed.

Optimization Settings

  • deviceSizes - Widths generated for fixed-size images (default: [640, 750, 828, 1080, 1200, 1920, 2048, 3840])
  • imageSizes - Widths generated for fill images (default: [16, 32, 48, 64, 96, 128, 256, 384])
  • formats - Output formats to generate (default: ['avif', 'webp'])
  • qualityAllowlist - Allowed quality values that can be requested (default: [25, 50, 75, 100]). Build-time preprocessing uses quality 75 when present in the allowlist, otherwise the first value. Runtime requests default to quality 75 unless specified. For example, with qualityAllowlist: [25, 50, 75, 100], both build-time preprocessing and runtime default to quality 75.

Basic Usage

src/app/page.tsx
import { Image } from 'rari/image'

export default function HomePage() {
  return (
    <div>
      <Image
        src="/hero.jpg"
        alt="Hero image"
        width={1200}
        height={600}
      />
    </div>
  )
}

Props

Required Props

src

  • Type: string | StaticImageData
  • Required: Yes

The image source. Can be a URL string or a static image import.

// URL string
<Image src="/images/photo.jpg" alt="Photo" width={800} height={600} />

// Static import
import heroImage from './hero.jpg'
<Image src={heroImage} alt="Hero" />

alt

  • Type: string
  • Required: Yes

Alternative text for the image. Important for accessibility and SEO.

<Image
  src="/profile.jpg"
  alt="User profile photo"
  width={200}
  height={200}
/>

Size Props

width

  • Type: number
  • Default: undefined

The intrinsic width of the image in pixels. Required unless using fill prop.

height

  • Type: number
  • Default: undefined

The intrinsic height of the image in pixels. Required unless using fill prop.

fill

  • Type: boolean
  • Default: false

Makes the image fill its parent container. The parent must have position: relative, position: fixed, or position: absolute.

<div style={{ position: 'relative', width: '100%', height: '400px' }}>
  <Image
    src="/banner.jpg"
    alt="Banner"
    fill
    style={{ objectFit: 'cover' }}
  />
</div>

sizes

  • Type: string
  • Default: undefined

Defines which image size to load at different viewport widths. Uses standard responsive image syntax.

<Image
  src="/responsive.jpg"
  alt="Responsive image"
  fill
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>

Optimization Props

quality

  • Type: number
  • Default: 75

The quality of the optimized image, between 1 and 100. Higher values mean better quality but larger file sizes.

Important: The quality value must be in the qualityAllowlist configured in your vite.config.ts. By default, only [25, 50, 75, 100] are allowed. Requests with other quality values will be rejected.

// Valid - 100 is in the default allowlist
<Image
  src="/high-quality.jpg"
  alt="High quality image"
  width={1920}
  height={1080}
  quality={100}
/>

// Invalid - 90 is NOT in the default allowlist
<Image
  src="/photo.jpg"
  alt="Photo"
  width={800}
  height={600}
  quality={90}  // ❌ This will fail!
/>

// To use quality={90}, add it to your vite.config.ts:
// qualityAllowlist: [25, 50, 75, 90, 100]

unoptimized

  • Type: boolean
  • Default: false

When true, the image will be served as-is without optimization. Useful for images that are already optimized or for testing.

<Image
  src="/already-optimized.jpg"
  alt="Pre-optimized image"
  width={800}
  height={600}
  unoptimized
/>

loader

  • Type: (props: { src: string, width: number, quality: number }) => string
  • Default: undefined

Custom function to generate image URLs. Useful for custom CDN configurations.

const customLoader = ({ src, width, quality }) => {
  return `https://cdn.example.com/?w=&q=`
}

<Image
  src="photo.jpg"
  alt="Photo"
  width={800}
  height={600}
  loader={customLoader}
/>

Loading Props

loading

  • Type: 'lazy' | 'eager'
  • Default: 'lazy'

The loading behavior of the image. Use 'eager' for above-the-fold images and 'lazy' for below-the-fold images.

// Above the fold - load immediately
<Image
  src="/hero.jpg"
  alt="Hero"
  width={1920}
  height={1080}
  loading="eager"
/>

// Below the fold - lazy load
<Image
  src="/gallery-1.jpg"
  alt="Gallery image"
  width={800}
  height={600}
  loading="lazy"
/>

preload

  • Type: boolean
  • Default: false

When true, adds a <link rel="preload"> tag to the document head for high-priority images.

<Image
  src="/hero.jpg"
  alt="Hero image"
  width={1920}
  height={1080}
  preload
/>

decoding

  • Type: 'async' | 'sync' | 'auto'
  • Default: 'async' (or 'sync' when preload is true)

Hint to the browser on how to decode the image. When preload is true, decoding defaults to 'sync' for immediate rendering.

// Async decoding (default) - non-blocking
<Image
  src="/photo.jpg"
  alt="Photo"
  width={800}
  height={600}
  decoding="async"
/>

// Sync decoding - blocks rendering until decoded
<Image
  src="/hero.jpg"
  alt="Hero"
  width={1920}
  height={1080}
  preload
  decoding="sync"
/>

Placeholder Props

placeholder

  • Type: 'blur' | 'empty'
  • Default: 'empty'

The placeholder to show while the image is loading. Use 'blur' with blurDataURL for a blurred preview.

<Image
  src="/photo.jpg"
  alt="Photo"
  width={800}
  height={600}
  placeholder="blur"
  blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..."
/>

blurDataURL

  • Type: string
  • Default: undefined

A Data URL to use as a blur placeholder. Only used when placeholder="blur".

Style Props

style

  • Type: React.CSSProperties
  • Default: undefined

Inline styles to apply to the image element.

<Image
  src="/photo.jpg"
  alt="Photo"
  width={800}
  height={600}
  style={{ borderRadius: '8px', boxShadow: '0 4px 6px rgba(0,0,0,0.1)' }}
/>

className

  • Type: string
  • Default: undefined

CSS class name(s) to apply to the image element.

<Image
  src="/photo.jpg"
  alt="Photo"
  width={800}
  height={600}
  className="rounded-lg shadow-md"
/>

onLoad

  • Type: (event: React.SyntheticEvent<HTMLImageElement>) => void
  • Default: undefined

Callback function invoked when the image successfully loads.

<Image
  src="/photo.jpg"
  alt="Photo"
  width={800}
  height={600}
  onLoad={(e) => console.log('Image loaded', e.currentTarget.src)}
/>

onError

  • Type: (event: React.SyntheticEvent<HTMLImageElement>) => void
  • Default: undefined

Callback function invoked when the image fails to load.

<Image
  src="/photo.jpg"
  alt="Photo"
  width={800}
  height={600}
  onError={(e) => console.error('Image failed to load')}
/>

Advanced Props

overrideSrc

  • Type: string
  • Default: undefined

Override the source URL used for optimization. Useful when you need to display one image but optimize from a different source URL.

// Display a thumbnail but optimize from full-size source
<Image
  src="/thumbnails/photo-thumb.jpg"
  overrideSrc="/originals/photo.jpg"
  alt="Photo"
  width={400}
  height={300}
/>

StaticImageData

When importing images statically, they return a StaticImageData object:

interface StaticImageData {
  src: string
  height: number
  width: number
  blurDataURL?: string
}

Common Patterns

Remote Images

Use remote images from configured domains:

src/app/page.tsx
import { Image } from 'rari/image'

export default function Page() {
  return (
    <Image
      src="https://images.unsplash.com/photo-1576191919769-40424bb34367"
      alt="Joshua Tree landscape"
      width={1200}
      height={600}
      quality={75}
      className="w-full h-auto"
    />
  )
}

Priority Loading for Hero Images

Use preload for above-the-fold images to ensure they load immediately:

src/app/page.tsx
import { Image } from 'rari/image'

export default function Page() {
  return (
    <Image
      src="https://images.unsplash.com/photo-1654652602865-53efa00c3e05"
      alt="Beach with mountain background"
      width={1200}
      height={600}
      preload
      quality={75}
      className="w-full h-auto"
    />
  )
}

Responsive Images

Use the sizes prop to serve different image sizes based on viewport width:

src/components/ResponsiveImage.tsx
import { Image } from 'rari/image'

export default function ResponsiveImage() {
  return (
    <div style={{ position: 'relative', width: '100%', height: '400px' }}>
      <Image
        src="/hero.jpg"
        alt="Hero image"
        fill
        sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
        style={{ objectFit: 'cover' }}
      />
    </div>
  )
}
src/components/Gallery.tsx
import { Image } from 'rari/image'

const images = [
  { src: '/gallery-1.jpg', alt: 'Gallery image 1' },
  { src: '/gallery-2.jpg', alt: 'Gallery image 2' },
  { src: '/gallery-3.jpg', alt: 'Gallery image 3' },
]

export default function Gallery() {
  return (
    <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '1rem' }}>
      {images.map((img, i) => (
        <Image
          key={i}
          src={img.src}
          alt={img.alt}
          width={400}
          height={300}
          style={{ width: '100%', height: 'auto' }}
        />
      ))}
    </div>
  )
}

Background Image with Fill

src/components/Hero.tsx
import { Image } from 'rari/image'

export default function Hero() {
  return (
    <section style={{ position: 'relative', width: '100%', height: '600px' }}>
      <Image
        src="/hero-background.jpg"
        alt="Hero background"
        fill
        style={{ objectFit: 'cover' }}
        quality={75}
        preload
      />
      <div style={{ position: 'relative', zIndex: 1, padding: '2rem' }}>
        <h1>Welcome to Our Site</h1>
        <p>Beautiful hero section with optimized background image</p>
      </div>
    </section>
  )
}

Avatar with Blur Placeholder

src/components/Avatar.tsx
import { Image } from 'rari/image'

export default function Avatar() {
  return (
    <Image
      src="/avatar.jpg"
      alt="User avatar"
      width={100}
      height={100}
      placeholder="blur"
      blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBD..."
      style={{ borderRadius: '50%' }}
    />
  )
}

Static Image Import

src/app/page.tsx
import { Image } from 'rari/image'
import heroImage from './hero.jpg'

export default function HomePage() {
  return (
    <div>
      <Image
        src={heroImage}
        alt="Hero"
        placeholder="blur"
      />
    </div>
  )
}

Custom Loader for CDN

src/components/CDNImage.tsx
import { Image } from 'rari/image'

const cloudinaryLoader = ({ src, width, quality }) => {
  const params = ['f_auto', 'c_limit', `w_`, `q_`]
  return `https://res.cloudinary.com/demo/image/upload//`
}

export default function CDNImage() {
  return (
    <Image
      src="sample.jpg"
      alt="Sample"
      width={800}
      height={600}
      loader={cloudinaryLoader}
    />
  )
}

Image Optimization

The Image component automatically optimizes your images using high-performance Rust transformations:

  • Converts to modern formats: Serves WebP and AVIF when supported by the browser, reducing file sizes by up to 50%
  • Generates responsive sizes: Creates multiple image sizes for different devices automatically
  • Lazy loads by default: Only loads images when they enter the viewport, improving initial page load
  • Optimizes quality: Balances file size and visual quality with intelligent compression
  • Prevents layout shift: Reserves space for images before they load, improving Core Web Vitals
  • Fast processing: Rust-powered backend handles encoding, resizing, and format conversion at native speeds

Supported Formats

The optimization endpoint supports:

  • Input formats: JPEG, PNG, WebP, AVIF, GIF, SVG
  • Output formats: JPEG, PNG, WebP, AVIF (with intelligent format selection based on browser support)

Images are automatically transcoded to the most efficient format supported by the user's browser, with AVIF preferred for maximum compression efficiency.

Default Sizes

The component generates images at these widths:

  • Device sizes (for fixed-width images): 640, 750, 828, 1080, 1200, 1920, 2048, 3840
  • Image sizes (for fill images): 16, 32, 48, 64, 96, 128, 256, 384

When you use width and height props, the component uses device sizes. When you use fill, it uses image sizes. The browser automatically selects the appropriate size based on the viewport and the sizes attribute.

You can customize these in your vite.config.ts:

export default defineConfig({
  plugins: [
    rari({
      images: {
        deviceSizes: [640, 750, 1080, 1920], // Custom device sizes
        imageSizes: [16, 32, 64, 128],       // Custom image sizes
      },
    }),
  ],
})

Best Practices

Always Provide Width and Height

This prevents layout shift and improves Core Web Vitals (CLS score):

// Good
<Image src="/photo.jpg" alt="Photo" width={800} height={600} />

// Bad - causes layout shift
<Image src="/photo.jpg" alt="Photo" />

Use Appropriate Loading Strategy

  • Use loading="eager" or preload for above-the-fold images
  • Use loading="lazy" (default) for below-the-fold images
// Hero image - load immediately
<Image src="/hero.jpg" alt="Hero" width={1920} height={1080} loading="eager" />

// Gallery images - lazy load
<Image src="/gallery-1.jpg" alt="Gallery" width={400} height={300} loading="lazy" />

Optimize Quality Based on Use Case

Choose quality values from your configured qualityAllowlist (default: [25, 50, 75, 100]):

  • Use quality={100} for hero images and important visuals where maximum quality is critical
  • Use quality={75} (default) for most images - provides excellent quality with good compression
  • Use quality={50} for thumbnails and less critical images
  • Use quality={25} for backgrounds and decorative images where file size is more important than quality

Note: Only values in your qualityAllowlist are accepted. Requests with other quality values will be rejected with an error.

Use Sizes for Responsive Images

Define how much viewport width the image occupies at different breakpoints:

<Image
  src="/responsive.jpg"
  alt="Responsive"
  fill
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 800px"
/>

Provide Meaningful Alt Text

Alt text is crucial for accessibility and SEO:

// Good
<Image src="/product.jpg" alt="Blue cotton t-shirt with round neck" width={400} height={400} />

// Bad
<Image src="/product.jpg" alt="image" width={400} height={400} />

Troubleshooting

Remote Images Not Loading

If remote images fail to load, check your vite.config.ts:

// Make sure the hostname is in remotePatterns
export default defineConfig({
  plugins: [
    rari({
      images: {
        remotePatterns: [
          {
            hostname: 'images.unsplash.com', // Add your image domain here
          },
        ],
      },
    }),
  ],
})

Remote images are blocked by default for security. You must explicitly allow each domain to prevent your application from being used as an open image proxy.

Image Not Loading

Check that:

  • The image path is correct and accessible
  • The image file exists in the public directory or is a valid URL
  • For remote images, the domain is listed in remotePatterns in your vite.config.ts
  • For local images, the path matches a pattern in localPatterns
  • The rari development server is running

Layout Shift Issues

Always provide width and height props, or use fill with a sized parent container:

// Option 1: Fixed dimensions
<Image src="/photo.jpg" alt="Photo" width={800} height={600} />

// Option 2: Fill parent
<div style={{ position: 'relative', width: '100%', height: '400px' }}>
  <Image src="/photo.jpg" alt="Photo" fill />
</div>

Slow Image Loading

  • Use preload for critical above-the-fold images
  • Use loading="eager" for hero images
  • Reduce quality for non-critical images
  • Ensure images are appropriately sized (don't serve 4K images for thumbnails)
  • Use placeholder="blur" with blurDataURL for better perceived performance
  • Check that remotePatterns is configured correctly to enable caching

CORS Errors with Remote Images

Remote images are proxied through your rari server to avoid CORS issues. If you see CORS errors:

  • Verify the domain is in remotePatterns
  • Check that the remote server allows your domain
  • Ensure you're not using unoptimized with remote images (which bypasses the proxy)
  • ImageResponse - Generate dynamic Open Graph images
  • Metadata - Configure page metadata including Open Graph images