
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,cropandhotspot. 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
| Method | URL parameter | What it does |
|---|---|---|
width(px), height(px), size(w, h) | w, h | Sets output dimensions |
fit(mode) | fit | How the image fits the box: clip, crop, fill, fillmax, max, scale, min |
crop(mode) | crop | Which part to keep with fit=crop: top, bottom, left, right, center, focalpoint, entropy |
auto('format') | auto=format | Serves WebP or AVIF to browsers that support them |
format(name) | fm | Forces jpg, pjpg, png or webp |
quality(0-100) | q | Compression quality |
dpr(1-3) | dpr | Multiplies dimensions for high-density screens |
blur(amount) | blur | Blurs the image |
sharpen(amount) | sharp | Sharpens the image |
focalPoint(x, y) | fp-x, fp-y | Sets a focal point from 0 to 1, overriding the hotspot |
rect(left, top, w, h) | rect | Crops to an exact pixel rectangle, overriding crop and hotspot |
bg(hex) | bg | Background colour for transparent areas or padding |
pad(px) | pad | Adds padding around the image |
flipHorizontal(), flipVertical() | flip | Mirrors the image |
orientation(deg) | or | Rotates by 0, 90, 180 or 270 degrees |
saturation(-100) | sat | Only -100 is supported, which produces greyscale |
ignoreImageParams() | none | Ignores the field's crop and hotspot |
forceDownload(filename) | dl | Makes the browser download the image |
vanityName(name) | path suffix | Appends a readable filename to the URL |
frame(1) | frame | Returns 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:
| Mode | Behaviour | Good for |
|---|---|---|
max | Fits within the box, keeps aspect ratio, never upscales | Content images, galleries |
clip | Fits within the box, keeps aspect ratio, may upscale | General resizing |
crop | Fills the box exactly, cropping the excess using the crop mode or hotspot | Cards, heroes, avatars |
fill | Fits within the box and pads the rest with bg colour | Logos on a fixed tile |
min | Resizes and crops to the requested aspect ratio, never exceeding the original size | Fixed 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.


