If you're reading this, you probably couldn't find a solid guide on how to set up image cropping with Sanity either.
I think this is partly because Sanity provides a tonne of flexibility to manipulate your image using their CDN and image builder (more on this later). Which is both fantastic, and a touch overwhelming.
In this guide, you'll learn the easiest and clearest way that I discovered on my path to set up Sanity cropping in my frontend application. This guide is not a comprehensive overview into everything you can do with Sanity's image manipulation tools. For that, I'd recommend the Sanity documentation.
Prerequisites
- Sanity project (v3) with your document queries prepared
- Frontend project with the following dependencies:
Let's jump in.
Updating your Sanity schema
{
name: 'myImage',
title: 'My Image',
type: 'image',
options: {
hotspot: true
},
...
}
After nesting this options object into your image schema, you should see in your Studio instance the 'crop' aciton in the top right of the image form.
Updating your frontend code
We know that when you query your Sanity document(s), images are returned not with their source, but with a reference. And, when you add the hotspot in your schema as per section 1, it will also return the cropping/hotspot properties specified in Sanity studio. These are properties you can leverage in your frontend code however you like. Sanity does not have a direct pattern for how these should be used (albeit with good reason). That being said, we'll dive into one pattern you can use to easily apply cropping to images. Let's get started.
The boilerplate
I'll assume you've already set up your query(s) to retrieve the image(s) returned from Sanity. For more info on this I would recommend the Sanity documentation.
To generate image urls from an image reference, we will use Sanity's helper methods to build the src url:
import imageUrlBuilder from '@sanity/image-url'
const builder = imageUrlBuilder({
dataset: process.env.MY_DATASET,
projectId: process.env.MY_PROJECT_ID,
})
export const urlFor = (imageRef: string) => builder.image(imageRef)
Here, the urlFor function accepts the imageRef returned from the Sanity groq query (more on this later) and returns an "ImageBuilder" object - an inbuilt Sanity type that lets you build the CDN src something like this:
urlFor(imageRef).width(1600).height(900).url()
Which generates a url like:
https://cdn.sanity.io/images/project_id/development/28e908-1680x1204.png?width=1600&height=900
So how the crop do we use the crop/hotspot properties returned from Sanity in our image builder? Maybe I missed something, but this is where the Sanity documentation (as ace as they are), left me high and dry.
Cropping
It's probably what you're here for, so here ya go:
import { getImageDimensions } from '@sanity/asset-utils'
import { urlFor } from './imageUrlFor'
export const getCroppedImageSrc = (
image: SanityImageQueryResult, // Details on this type in the appendix
) => {
const imageRef = image?.src?.asset?._ref
const crop = image.src.crop
// get the image's og dimensions
const { width, height } = getImageDimensions(imageRef)
if (Boolean(crop)) {
// compute the cropped image's area
const croppedWidth = Math.floor(width * (1 - (crop.right + crop.left)))
const croppedHeight = Math.floor(height * (1 - (crop.top + crop.bottom)))
// compute the cropped image's position
const left = Math.floor(width * crop.left)
const top = Math.floor(height * crop.top)
// gather into a url
return urlFor(imageRef)
.rect(left, top, croppedWidth, croppedHeight)
.url()
}
return urlFor(imageRef).url()
}
For those interested, let's break it down.
The width, height properties returned from getImageDimensions do not return the cropped width and height, but the original image's width and height.
This is why we use the crop settings to compute the cropped width and height as set in Sanity Studio. Crop settings return the cropped area's (percentage) distance from each edge.
For example, {top: 0.2, bottom: 0.3, ...} means that the cropped area is 20% away from the top of the original area, and 30% away from the bottom of the original area; similar to absolute positioning in css - probably why Sanity chose this convention 🤔.
This is why:
1 - (crop.right + crop.left)
can be thought of as
1 - (percentage distance from horizontal edges)
can be thought as
percentage of image to retain/not crop
Multiplying by this value the original width gives us the cropped width. The corollary is true for the cropping in the y/height axis.
This gives us the size of the crop, but not the position. For that, we simply retrieve the cropped area's actual distance (not percentage distance) from the top and left of the original image's area.
const left = Math.floor(width * crop.left)
const top = Math.floor(height * crop.top)
Rembember that width and height here are the image's original properties.
Now we use the 'rect' method that accepts our computed values for the size and position:
return urlFor(image)
.rect(left, top, croppedWidth, croppedHeight)
.url()
Which generates a url like:
https://cdn.sanity.io/images/project_id/development/28e903-1680x704.png?rect=10,40,1074,704
For those curious, Math.floor is required because the Sanity CDN expects integer values.
Voila
To caveat, you still have a responsibility to correctly place and position your image within your HTML or JSX.
I hope you found this helpful! Please let me know if you are interested in image manipulation using the hotspot and the imageBuilder's focalpoint method.
Appendix
This is the shape of an image type returned from a Sanity query.
export type SanityImageQueryResult = {
src: {
asset: { _ref: string }
crop: {
_type: 'sanity.imageCrop'
bottom: number
left: number
right: number
top: number
}
hotspot: {
_type: 'sanity.imageHotspot'
height: number
width: number
x: number
y: number
}
}
alt: string
}
Top comments (4)
Hello. Thank you for the article. This is really helped me solve my issue.
I wanted to ask you, have you crop a image's preview?
I mean lqip or blurHash?
If yes, could you show an example?
Glad this could help!
Sorry I haven't actually. I would super curious if you have managed to get the blur/lqip crop working since.
Isn't is easier to use
.fit('crop')
on the image builder function? That's what I've always done and it seems to work great.I struggle hardcore with the hotspot though. I can never seem to get that to work. If you have any info on that, that'd be super helpful!
Super-helpful. Sanity is so powerful (even at the free level) but I find the documentation lacking. Thanks for writing this up!