Sanity image focal point not working with @sanity/image-url in NextJS

13 replies
Last updated: Jan 3, 2022
Hi, I am struggling with an image’s focal point. I am using NextJS +
@sanity/image-url
with the following code:
/* JSX */
// CSS Classes are from <https://tailwindcss.com/docs>
<section className={`w-full h-full absolute flex`}>
 <div id="image" className="w-full flex-1 relative">
  <img
   alt={args.data.metadata.alt}
   src={urlFor(args.data.cover).url()}
   className="w-full h-full"
  />
 </div>
<section/>

/* JS */
function urlFor(image) {
  const builder = imageUrlBuilder(sanity);
  return builder.image(image);
}
But the focal point is not respected, even though other options (like crop) are. Is there something I am doing wrong?

I am not using the
next/image
component because it leads to the same result; I thought a normal
<img />
tag would be more appropriate for the sake of this message.
AI Update

The issue you're experiencing is that the hotspot won't be respected unless you specify both width and height dimensions. The @sanity/image-url library needs explicit dimensions to know how to apply the hotspot/crop data.

Here's what's happening: when you call urlFor(args.data.cover).url() without dimensions, the library returns the original image URL without any transformations. The hotspot data exists in your image object, but it requires width and height parameters to be activated.

Solution: Add width and height

Update your urlFor function or the specific call to include dimensions:

function urlFor(image) {
  const builder = imageUrlBuilder(sanity);
  return builder.image(image);
}

// In your JSX - specify dimensions where you use it
<img
  alt={args.data.metadata.alt}
  src={urlFor(args.data.cover).width(800).height(600).url()}
  className="w-full h-full object-cover"
/>

When you provide both width and height, @sanity/image-url automatically converts the hotspot data from your image document into the proper URL parameters (crop=focalpoint, fp-x, and fp-y) as documented in Sanity's image URL transformation guide.

For responsive layouts with Tailwind

Since you're using w-full h-full with Tailwind, you likely want the image to fill its container while respecting the hotspot. Here's a better approach:

<div id="image" className="w-full flex-1 relative">
  <img
    alt={args.data.metadata.alt}
    src={urlFor(args.data.cover).width(1200).height(800).url()}
    className="w-full h-full object-cover"
  />
</div>

The object-cover class ensures the image fills the container while maintaining aspect ratio, and the hotspot will be respected in how the image is cropped by Sanity's CDN.

With next/image

If you do want to use Next.js's Image component (which is generally recommended for performance), the same principle applies:

import Image from 'next/image'

<div id="image" className="w-full flex-1 relative">
  <Image
    src={urlFor(args.data.cover).width(1200).height(800).url()}
    alt={args.data.metadata.alt}
    fill
    className="object-cover"
  />
</div>

Why crop works but hotspot doesn't

The crop parameter you mentioned works because it's a simpler transformation that just defines rectangular boundaries. The hotspot, however, specifically requires dimensions to calculate where to focus when the image is resized to different aspect ratios. According to the @sanity/image-url documentation, the library states: "In order for hotspot/crop processing to be applied, the image record must be supplied, as well as both width and height."

The key takeaway: hotspot data is only applied when you specify both width and height, which tells Sanity's image pipeline how to intelligently crop around the focal point you've defined.

Show original thread
13 replies
Hi! Do you mean using the focal point that is set in the studio (hotspot), or passing in as an option with the image builder (i.e.
urlFor(...).focalpoint(0.1, 0.1)
)?
The focal point that is set in the studio, yes 😄
It seems to ignore the focal point set as an option too
From what I understand the focal point would only apply if you let sanity crops the image on its own by setting a height or a width on the url, i.e
urlFor(...).width(200)
... but I think it's complicated if an image also have a crop defined at the same time 🤔
In my case, no crop has been applied explicitly on the editor; sanity's query does however send crop data, so I think it's just activated by default.
I did consider the width too, as I suspected the dynamic sizing of the image was to blame, but it doesn't seem to make any difference (just tested again to be sure).
Changing the side of the image only seems to affect the display quality, the focal point is always the same.
I just wrote a helper function to use with the
objectPosition
property.

export default function getPositionFromHotspot(hotspot) {
  if (!hotspot || !hotspot.x || !hotspot.y) return "center";

  return `${hotspot.x * 100}% ${hotspot.y * 100}%`;
}
and in the Next Image component:

<Image
  src={urlForImage(image).url()}
  alt={ ... }
  sizes={ ... }
  layout="fill"
  objectFit="cover"
  objectPosition={getPositionFromHotspot(image?.hotspot)}
/>
I worked! Thank you
My saviors.I don't understand why this is happening in the first place, though. Should it be considered unexpected behavior?
I shouldn't need to set the
objectPosition
property myself
I did it that way so I wouldn't have to set a specific width. I wanted the image to fill its container.
Yup, but in my case it doesn't work even if i set a predefined height and width.
It should according to
this link.

In order for hotspot/crop processing to be applied, the image record must be supplied, as well as both width and height.
I think I'll investigate more and eventually open an issue if I can replicate the problem.
Thank you guys again for your time

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?