A well-considered React component for displaying images from Sanity. At a glance:
<img> tag, no nested DOM structure to mess withimg
tag!srcSet automatically based on the width you specifysrcSet factor based on image output widthwidth and height
attributes accordinglycrop and hotspot values from the Sanity Studiohotspot is providedas prop to render as a custom component)yarn add sanity-image
# or
npm install sanity-image
You can find the full writeup on getting going below, but in the interest of making it easy to see if this is the thing you are looking for, here’s a quick example of most of what you’ll need to know:
Simplest Case:
This will render the image out assuming it will be displayed at half its original width with a srcSet included (multiplies vary based on original image size):
import { SanityImage } from "sanity-image"
const YourSweetComponent = ({ image }: ComponentProps) => (
<SanityImage
// Pass the Sanity Image ID (`_id`) (e.g., `image-abcde12345-1200x800-jpg`)
id={image._id}
baseUrl="https://cdn.sanity.io/images/abcd1234/production/"
alt="Demo image"
/>
)
More full-featured example:
import { SanityImage } from "sanity-image"
const YourSweetComponent = ({ image }: ComponentProps) => (
<SanityImage
// Pass the Sanity Image ID (`_id`) (e.g., `image-abcde12345-1200x800-jpg`)
id={image._id}
//
// You can set the base URL manually, or let it be constructed by passing
// `projectId` and `dataset` props.
baseUrl="https://cdn.sanity.io/images/abcd1234/production/"
//
// Specify how big it is expected to render so a reasonable srcSet can be
// generated using `width`, `height`, or both
width={500}
height={250}
//
// Choose whether you want it to act like `object-fit: cover` or
// `object-fit: contain`, or leave it out to use the default (contain)
mode="cover"
//
// Have hotspot or crop data from Sanity? Pass it in!
hotspot={image.hotspot}
crop={image.crop}
//
// Want low-quality image previews? Fetch them from Sanity and pass them in too.
preview={image.asset.metadata.lqip}
//
// Have a burning desire to have Sanity change the format or something?
// Most of the visual effects from the Sanity Image API are available:
queryParams={{ sharp: 30, q: 80 }}
//
// Anything else you want to pass through to the img tag? Go for it!
alt="Sweet Christmas!"
className="big-ol-image"
sizes="(min-width: 500px) 500px, 100vw"
/>
)
export default YourSweetComponent
That’s the gist. Read on for more. 👇
How it works at a glance:
SanityImage.ts for
details)src and srcSet props generated based on the width
and height props you pass in (or the image dimensions if you don't pass in a
width or height)srcSet widths depend on the size of the output image and the original
image; there's some logic to avoid wasteful tiny images or giant jumps in size
between large entries (see dynamicMultipliers in urlBuilder.ts)srcSet are never duplicated and never upscale the imagewidth and height attributes are set automatically to avoid layout shiftsauto=format - Sanity will use AVIF images if they're supported by the
browser (https://www.sanity.io/help/avif) (note: if you specify fm
manually, this won't be set)fit - if the image aspect ratio isn't changed, this will be set to max;
if the aspect ratio will change it's set to crop; you don't really need to
worry about this thoughq - the quality is set to 75 by default, but you can override it with the
queryParams proploading attribute will be set to lazy if it isn't supplied; use
loading="eager" for images above the foldalt attribute will be set to an empty string if it isn't supplied; set
it if it isn't a decorative image!img tag (two if you pass in a preview), but you
can pass in a custom component to render as using the as prop (see the
SanityImage.test.tsx file for an example of this)buildSrc and buildSrcSet
exports to do your own thing with. You get a lot of the magic still this way
with a skosh more control.parseImageId function is available as a named export; it
takes an image ID and returns an object with the image id, dimensions, and
format.height only? Don't be alarmed,
but it'll be converted to a w param without altering what you're asking
Sanity for. Ask for mode="cover" but the aspect ratio matches the source?
It'll be ignored and fall back to fit=max with just a w param. You get the
idea (I hope, or at least, I'm pretending, but no judgement if you don't, it's
definitely 11:09pm and I'm on fumes)This is mostly copied and reformatted from the types.ts file; if you're
comfortable with TypeScript, that might give you more detail.
id (string) — Required - The Sanity Image ID (_id or _ref field value)mode ("cover" | "contain") — Optional - Use cover to crop the image to
match the requested aspect ratio (based on width and height). Use
contain to fit the image to the boundaries provided without altering the
aspect ratio. Defaults to "contain". See the image below for a comparison.width (number) — Optional - The target width of the image in pixels. Only
used for determining the dimensions of the generated assets, not for layout.
Use CSS to specify how the browser should render the image instead.height (number) — Optional - The target height of the image in pixels. Only
used for determining the dimensions of the generated assets, not for layout.
Use CSS to specify how the browser should render the image instead.hotspot ({ x: number, y: number }) — Optional - The hotspot coordinates to
use for the image. Note: hotspot width and height are not used.crop ({ top: number, bottom: number, left: number, right: number }) —
Optional - The crop coordinates to use for the image.preview (string) — Optional - A low-quality image preview to use while the
full-size image is loading. This should be a base64-encoded image string.as (React.ElementType) — Optional - The component to render as. Defaults to
"img".baseUrl (string) — Optional - The base URL to use for the image. If not
specified, the projectId and dataset props will be used to construct the
URL.projectId (string) — Optional - The Sanity project ID to use for the image.
Only used if baseUrl is not specified.dataset (string) — Optional - The Sanity dataset to use for the image. Only
used if baseUrl is not specified.queryParams (object) — Optional - An object of query parameters to pass to
the Sanity Image API. See the
Sanity Image API documentation for a
list of available options.That's the gist. There's a ton more in the inline comments and types and such, and I'll add more details as I think of them. Feel free to open an issue or start a discussion if you have questions or suggestions, or find me on the Sanity Slack!
⚠️ “Help! My full-size image isn’t loading!"
SanityImage is relying on browser-native deferred image loading. This relies
on the <img> element being visible on the page to trigger loading
automatically. If the image is hidden or covered by another element, it may
never be loaded.
For example, if you are using overflow: hidden on a parent element, the image
might be positioned in a way that it is not treated as visible by the browser.
This can also happen if you have display: none on the image or a parent
element, or if the image is positioned off-screen (e.g., in a carousel, or
waiting for an animation to reveal it).
Inspecting the DOM in your browser’s dev tools and checking the position of the
<img> element with the data-loading attribute can help you identify the
issue usually. Once you can identify the cause of the image being hidden, you
can adjust the CSS styles on the img[data-loading] element to make it visible.
For reference, the full-size image sits immediately adjacent to the preview image and has the following default styles while loading:
position: absolute;
width: 10px !important; /* must be > 4px to be lazy loaded */
height: 10px !important; /* must be > 4px to be lazy loaded */
opacity: 0;
zindex: -10;
pointerevents: none;
userselect: none;
modeIf you are providing only one dimension (width or height, but not both), it
doesn't matter since the behavior will be the same.
Here's a visual of this in action:
To improve the DX of using sanity-image, create a wrapper component in your
app that sets the baseUrl prop (or projectId and dataset). This keeps the
configuration in one place and gives you an entry point to add any other logic
you might need. There's a
full ImageWrapper example
in the examples folder including comments. Here's a simplified version of that
example for quick reference:
import * as React from "react"
import { SanityImage, type WrapperProps } from "sanity-image"
export const Image = <T extends React.ElementType = "img">(
props: WrapperProps<T>
) => <SanityImage baseUrl="<your-baseurl-here>" {...props} />
👩🎤 Using Emotion’s jsxImportSource? Read this.
When you set the jsxImportSource to @emotion/react it replaces some core
React types with those of its own. This allows Emotion’s polymorphic components
to work, but it also makes typing a polymorphic component like <SanityImage> a
bit harder. This is okay when it's used directly, but the wrapper approach winds
up breaking it—the use of Omit to remove configuration props breaks the
polymorphism and prompts TS to complain about unexpected props.
I am not a TS wizard, alas, and despite lots of reading and attempting to make
something that works out of the box for Emotion, I have not managed to do so.
Instead I recommend using @ts-expect-error on the <SanityImage> line in your
wrapper. This will tell TS to ignore the props we're passing in, but it will
still ensure your in-app <Image> component works as expected with full
polymorphic type support.
export const Image = <T extends React.ElementType = "img">(
props: WrapperProps<T>
) => (
/* @ts-expect-error Emotion types are incompatible with polymorphic component */
<SanityImage baseUrl="<your-baseurl-here>" {...props} />
)
I recommend setting something like the following CSS for images in your project,
then overriding styles as needed. This will ensure images act like block-level
elements with infinitely scalable contents even with the width and height
attributes set. It also makes it easier to handle responsiveness—if your
container gets smaller, the image gets smaller.
img {
display: block;
max-width: 100%;
width: 100%;
height: auto;
}
Here's an example of how that works when using, for example, a 3-column grid that fills the viewport until it is a maximum of 1,200px wide (plus padding). This produces columns that are 390px at most on desktop:
```jsx
{["image-a", "image-b", "image-c"].map((imageId) => (
$ claude mcp add sanity-image \
-- python -m otcore.mcp_server <graph>