Type something to search...
Using Sanity's Image URL Builder

Using Sanity's Image URL Builder

Sanity's image URL builder is the @sanity/image-url package, a small library that turns an image field from your Sanity content into a cdn.sanity.io URL with transformation parameters. You create a builder with createImageUrlBuilder(client), pass it an image with .image(source), chain methods such as .width(800), .height(450), .fit('crop') and .auto('format'), and finish with .url(). Its main advantage over writing URLs by hand is that it automatically applies the crop and hotspot editors set in Sanity Studio, so images are framed the way your content team intended.

You could build image URLs yourself by appending ?w=800&h=450 to an asset URL, but you'd need to dereference the asset first, convert crop fractions into pixel rectangles and calculate focal points. The builder does all of that from the raw image field. This guide covers installing and configuring it, the methods you'll use most, how fit and crop modes behave, responsive srcSet patterns, TypeScript types, using it outside a client, and the v2 changes that break older tutorials.

Install and Configure the Builder

Install the package:

npm install @sanity/image-url

The usual pattern is to create one builder from your configured client and export a urlFor helper:

// src/sanity/lib/client.ts
import { createClient } from "next-sanity";

export const client = createClient({
  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
  dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,
  apiVersion: "2026-03-01",
  useCdn: true,
});
// src/sanity/lib/image.ts
import {
  createImageUrlBuilder,
  type SanityImageSource,
} from "@sanity/image-url";
import { client } from "./client";

const builder = createImageUrlBuilder(client);

export function urlFor(source: SanityImageSource) {
  return builder.image(source);
}

The builder reads projectId and dataset from the client, which is all it needs. It doesn't make any network requests, so it's safe to use in server components, client components and build scripts alike.

Without a Client

If you don't have a client in scope, for example in a small utility package, pass the project details directly:

import { createImageUrlBuilder } from "@sanity/image-url";

const builder = createImageUrlBuilder({
  projectId: "abc123",
  dataset: "production",
});

Upgrading From v1

Version 2.0.0, released in November 2025, renamed the main export and moved types to the package root. The functionality didn't change, but imports did:

// Before (v1)
import imageUrlBuilder from "@sanity/image-url";
import type { SanityImageSource } from "@sanity/image-url/lib/types/types";

// After (v2)
import {
  createImageUrlBuilder,
  type SanityImageSource,
} from "@sanity/image-url";

The old default export still exists in v2 but is marked as deprecated, so switch to the named import now. If you see type import errors from @sanity/image-url/lib/types/types after updating, the moved types are the cause.

What You Can Pass to image()

.image() accepts three kinds of source:

  • An image field: The full object from your document, with asset, crop and hotspot. This is what you should pass in almost every case.
  • An asset document or reference: For example post.mainImage.asset. The URL works, but crop and hotspot are ignored because they live on the image field, not the asset.
  • An asset ID string: Such as image-7f3c...-1600x900-jpg. Useful when you only have an ID, for example from a webhook.

The most common mistake is passing image.asset instead of image. Your images render, but editors wonder why their crops are being ignored.

Make sure your GROQ query returns crop and hotspot too. If you project only asset->url, the builder has nothing to work with:

*[_type == "post"][0]{
  title,
  mainImage{ asset, crop, hotspot, alt }
}

Core Methods

Every chain starts with urlFor(image) and ends with .url(). In between, you add transformations:

const src = urlFor(post.mainImage)
  .width(1200)
  .height(630)
  .fit("crop")
  .auto("format")
  .quality(80)
  .url();

That produces a URL like:

https://cdn.sanity.io/images/abc123/production/7f3c9a1e...-1600x900.jpg?rect=0,45,1600,840&w=1200&h=630&fit=crop&q=80&auto=format

Notice the rect parameter. The builder calculated it from the editor's crop and hotspot to keep the important part of the image in frame.

Methods Reference

MethodURL parameterWhat it does
width(px), height(px), size(w, h)w, hSets output dimensions
fit(mode)fitHow the image fits the box: clip, crop, fill, fillmax, max, scale, min
crop(mode)cropWhich part to keep with fit=crop: top, bottom, left, right, center, focalpoint, entropy
auto('format')auto=formatServes WebP or AVIF to browsers that support them
format(name)fmForces jpg, pjpg, png or webp
quality(0-100)qCompression quality
dpr(1-3)dprMultiplies dimensions for high-density screens
blur(amount)blurBlurs the image
sharpen(amount)sharpSharpens the image
focalPoint(x, y)fp-x, fp-ySets a focal point from 0 to 1, overriding the hotspot
rect(left, top, w, h)rectCrops to an exact pixel rectangle, overriding crop and hotspot
bg(hex)bgBackground colour for transparent areas or padding
pad(px)padAdds padding around the image
flipHorizontal(), flipVertical()flipMirrors the image
orientation(deg)orRotates by 0, 90, 180 or 270 degrees
saturation(-100)satOnly -100 is supported, which produces greyscale
ignoreImageParams()noneIgnores the field's crop and hotspot
forceDownload(filename)dlMakes the browser download the image
vanityName(name)path suffixAppends a readable filename to the URL
frame(1)frameReturns the first frame of an animated image

minWidth(), maxWidth(), minHeight() and maxHeight() still exist but are deprecated. Use width() or height() with fit('max') or fit('min') instead.

Understanding fit Modes

fit decides what happens when your requested size doesn't match the image's aspect ratio. These are the modes you'll use most:

ModeBehaviourGood for
maxFits within the box, keeps aspect ratio, never upscalesContent images, galleries
clipFits within the box, keeps aspect ratio, may upscaleGeneral resizing
cropFills the box exactly, cropping the excess using the crop mode or hotspotCards, heroes, avatars
fillFits within the box and pads the rest with bg colourLogos on a fixed tile
minResizes and crops to the requested aspect ratio, never exceeding the original sizeFixed shapes from small uploads

Sanity's docs warn that small images are scaled up to the requested size unless you use fit=max. For images inside articles, fit('max') is usually the safest choice because a small upload won't be stretched and blurred.

When you set both width() and height() on an image with a hotspot, the builder computes a crop rectangle that keeps the hotspot in view. If you only set one dimension, there's nothing to crop, so the hotspot isn't used. See hotspot and crop in Sanity for how editors set these values.

Common Recipes

Square Avatar

urlFor(author.photo).width(96).height(96).fit("crop").auto("format").url();

Article Image That Never Upscales

urlFor(block).width(1200).fit("max").auto("format").url();

Open Graph Image

Social platforms don't send Accept headers that negotiate WebP or AVIF reliably, so force a widely supported format:

urlFor(post.mainImage)
  .width(1200)
  .height(630)
  .fit("crop")
  .format("jpg")
  .quality(85)
  .url();

Greyscale Logo on a White Tile

urlFor(logo)
  .width(240)
  .height(120)
  .fit("fill")
  .bg("ffffff")
  .saturation(-100)
  .url();

Download Link

<a href={urlFor(asset).forceDownload("press-kit-photo.jpg").url()}>
  Download high-resolution photo
</a>

Build Responsive srcSet Values

A single URL is rarely enough. Browsers pick the right size from a srcSet, and the builder makes generating one simple:

// src/components/sanity-image.tsx
import type { SanityImageSource } from "@sanity/image-url";
import { urlFor } from "@/sanity/lib/image";

const WIDTHS = [400, 800, 1200, 1600];

type SanityImageProps = {
  image: SanityImageSource;
  alt: string;
  aspectRatio?: number;
  sizes?: string;
  className?: string;
};

export function SanityImage({
  image,
  alt,
  aspectRatio,
  sizes = "100vw",
  className,
}: SanityImageProps) {
  const build = (w: number) => {
    let b = urlFor(image).width(w).auto("format");
    b = aspectRatio
      ? b.height(Math.round(w / aspectRatio)).fit("crop")
      : b.fit("max");
    return b.url();
  };

  return (
    <img
      src={build(800)}
      srcSet={WIDTHS.map((w) => `${build(w)} ${w}w`).join(", ")}
      sizes={sizes}
      alt={alt}
      loading="lazy"
      decoding="async"
      className={className}
    />
  );
}

Sanity's docs recommend reusing the same transformations across your front end, because identical URLs are served from the CDN cache. A fixed list of widths like WIDTHS does that naturally.

For high-density screens with a fixed-size image, use dpr instead of a width list:

<img
  src={urlFor(logo).width(160).url()}
  srcSet={`${urlFor(logo).width(160).dpr(2).url()} 2x, ${urlFor(logo).width(160).dpr(3).url()} 3x`}
  width={160}
  alt="Company logo"
/>

Use the Builder With next/image

In Next.js, you have two options. With next/image, add cdn.sanity.io to images.remotePatterns and pass a builder URL as src. Next.js will then optimise the image again through its own pipeline, which works but duplicates effort.

The more efficient option is the Image component from next-sanity/image, which takes a builder URL and generates the srcSet by adjusting Sanity's CDN parameters directly:

import type { SanityImageObject } from "@sanity/image-url";
import { Image } from "next-sanity/image";
import { urlFor } from "@/sanity/lib/image";

export function Hero({
  image,
}: {
  image: SanityImageObject & { alt?: string };
}) {
  return (
    <Image
      src={urlFor(image).width(1600).height(900).url()}
      alt={image.alt ?? ""}
      width={1600}
      height={900}
      sizes="100vw"
      preload
    />
  );
}

The loader recalculates h for each width so the aspect ratio stays consistent, and it adds auto=format for you. Sanity's documentation currently labels this component as alpha. The trade-offs between the two approaches are covered in optimising Sanity images for performance.

TypeScript Types

v2 exports its types from the package root:

import type {
  SanityImageSource,
  SanityImageObject,
  ImageUrlBuilder,
} from "@sanity/image-url";

Use SanityImageSource for props that accept any valid source. If you generate types with Sanity TypeGen, your query result types will already describe the image field shape, and they're compatible with SanityImageSource.

Handle Missing Images

urlFor() throws if the source has no asset, which happens when an editor adds an image field with a caption but no upload, or deletes an image. Guard before you build:

if (!post.mainImage?.asset) return null;

You can also prevent the problem at the schema level with rule.required().assetRequired(), which is covered in how to handle images in Sanity. Required alt text is a separate concern, explained in alt text requirements for Sanity images.

Multiple Projects or Datasets

If you render images from more than one dataset, such as a staging and production preview, override per call:

urlFor(image).dataset("staging").width(800).url();

Or switch the whole builder to another client while keeping options you've already chained:

const base = urlFor(image).width(800).auto("format");
const fromOtherProject = base.withClient(otherClient).url();

FAQ: Sanity Image URL Builder

Either you passed the asset instead of the full image field, your query didn't return crop and hotspot, or you only set one dimension. Pass the image object, include crop and hotspot in the projection, and set both width and height.

The default export imageUrlBuilder was replaced by the named export createImageUrlBuilder, and types moved to the package root. Behaviour is otherwise the same, so most projects only need to update one import.

Use auto('format') for browsers, because it serves AVIF or WebP based on what each browser supports and falls back to the original format. Use format() when the consumer isn't a browser, such as social media crawlers or email clients.

No. It builds a string from data you already have, using the project ID and dataset from your client. The image is only fetched and transformed when a browser requests the URL from Sanity's CDN.

Both keep the aspect ratio and fit the image inside the requested box. fit('max') never upscales a smaller image, while clip will enlarge it to the requested size, which can make small uploads look blurry.

Yes. It's a plain JavaScript library that returns strings, so it works in Astro, Vue, Svelte, Node scripts, email templates and anywhere else you need an image URL.


Conclusion

The image URL builder is the bridge between Sanity's structured image data and its image pipeline. Configure it once with createImageUrlBuilder(client), pass full image fields rather than assets, and chain width, height, fit and auto('format') to get correctly framed, efficiently encoded images. The builder handles the tricky parts, such as converting crop and hotspot into a rect, so you don't have to.

Wrap it in a small component that generates a consistent srcSet, guard against missing assets, and choose fit('max') or fit('crop') based on whether the layout needs a fixed shape. With that in place, every image on your site respects the editor's choices and loads at a sensible size.

Tags :
Share :

Related Posts

Building a Static Site With Astro and Sanity

Building a Static Site With Astro and Sanity

To build a static site with Astro and Sanity, keep Astro's default output: 'static', configure @sanity/astro with useCdn: false, and fetch

Dive Deeper
Common Sanity Mistakes and How to Fix Them

Common Sanity Mistakes and How to Fix Them

The most common Sanity mistakes are using the CDN where you need fresh data, fetching whole documents with ..., leaving slugs unvalidated, ship

Dive Deeper
How to Create a Custom Sanity Plugin

How to Create a Custom Sanity Plugin

To create a custom Sanity plugin, wrap a piece of Studio configuration in definePlugin from the sanity package and add it to the plugins ar

Dive Deeper