A well-considered React component for displaying images from Sanity. At a glance:
- Outputs a single
<img>
tag, no nested DOM structure to mess with - Zero styling included so you can style it however you want…it's just an
img
tag! - Supports low-quality image previews out of the box, without build-time penalties (native lazy loading)
- Generates a
srcSet
automatically based on thewidth
you specify - Dynamic
srcSet
factor based on image output width - Knows exactly what size the image will be and sets
width
andheight
attributes accordingly - Supports
crop
andhotspot
values from the Sanity Studio - Automatically crops to the most “interesting” part of the image if the aspect
ratio changes and no
hotspot
is provided - Images are never scaled up
- Tiny 4kb bundle size (2kb gzipped)
- No dependencies
- TypeScript support
- Works with Gatsby, Next.js, and any other React-based framework
- Polymorphic component (supports
as
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={{ sharpen: 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:
- The image ID is parsed to determine the source image dimensions and format
- SVG images get special treatment from the Sanity Image API (they don't support
params), so they're handled a bit differently (check
SanityImage.ts
for details) - All other images have
src
andsrcSet
props generated based on thewidth
andheight
props you pass in (or the image dimensions if you don't pass in a width or height) - The
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 (seedynamicMultipliers
inurlBuilder.ts
) - Values in the
srcSet
are never duplicated and never upscale the image - Since we can compute the output dimensions of the image in all cases, the
width
andheight
attributes are set automatically to avoid layout shifts - A few image params are applied by default:
auto=format
- Sanity will use WebP images if they're supported by the browser (note: if you specifyfm
manually, this won't be set)fit
- if the image aspect ratio isn't changed, this will be set tomax
; if the aspect ratio will change it's set tocrop
; you don't really need to worry about this thoughq
- the quality is set to 75 by default, but you can override it with thequeryParams
prop
- The
loading
attribute will be set tolazy
if it isn't supplied; useloading="eager"
for images above the fold - The
alt
attribute will be set to an empty string if it isn't supplied; set it if it isn't a decorative image! - By default it renders an
img
tag (two if you pass in apreview
), but you can pass in a custom component to render as using theas
prop (see theSanityImage.test.tsx
file for an example of this) - If you wanna get weird you can also import the
buildSrc
andbuildSrcSet
exports to do your own thing with. You get a lot of the magic still this way with a skosh more control. - Similarly, the
parseImageId
function is available as a named export; it takes an image ID and returns an object with the image id, dimensions, and format. - Query params passed to Sanity are all sorted and minimized like heck for
improved caching and smaller URLs. Pass in a
height
only? Don't be alarmed, but it'll be converted to aw
param without altering what you're asking Sanity for. Ask formode="cover"
but the aspect ratio matches the source? It'll be ignored and fall back tofit=max
with just aw
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 - Usecover
to crop the image to match the requested aspect ratio (based onwidth
andheight
). Usecontain
to fit the image to the boundaries provided without altering the aspect ratio. Defaults to"contain"
.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: hotspotwidth
andheight
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, theprojectId
anddataset
props will be used to construct the URL.projectId
(string) — Optional - The Sanity project ID to use for the image. Only used ifbaseUrl
is not specified.dataset
(string) — Optional - The Sanity dataset to use for the image. Only used ifbaseUrl
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!
⚠️ Minor gotchas with deferred loading
SanityImage
is relying on browser-native deferred image loading. This
generally works fine in browsers that support it, but there are situations where
the unloaded image is hidden or covered, resulting in the full image never
loading.
If this happens, you can override the styles set on the full-size image using
the img[data-loading]
selector. This image sits immediately adjacent to the
spaceball 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;
I recommend creating a wrapper component internally to pass your baseUrl
prop
and pass through any props. This keeps the configuration in one place and gives
you an entry point to add any other logic you might need. Here's a TypeScript
example (for JavaScript, just remove the type annotation after props
):
import { SanityImage } from "sanity-image"
const projectId = process.env.SANITY_PROJECT_ID
const dataset = process.env.SANITY_DATASET
const baseUrl = `https://cdn.sanity.io/images/${projectId}/${dataset}/`
export const Image = (
props: Omit<
React.ComponentProps<typeof SanityImage>,
"baseUrl" | "dataset" | "projectId"
>
) => <SanityImage baseUrl={baseUrl} {...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:
<div
css={{
display: "grid",
gridTemplateColumns: "repeat(3, 1fr)",
gap: 15,
maxWidth: 1240,
paddingInline: 20,
marginInline: "auto",
}}
>
{["image-a", "image-b", "image-c"].map((imageId) => (
<div key={imageId}>
<SanityImage
id={imageId}
baseUrl="..."
width={390}
sizes="(min-width: 1240px) 390px, calc((100vw - 40px - 30px) / 3)"
/>
</div>
))}
</div>
If you need these images to all match in height, it's a good idea to switch to
cover
mode. With the height set to 260px and mode="cover"
, this will produce
images with a 3:2 aspect ratio that fill the column width even if the source
image is too small:
<SanityImage
id={imageId}
baseUrl="..."
width={390}
height={260}
mode="cover"
sizes="(min-width: 1240px) 390px, calc((100vw - 40px - 30px) / 3)"
/>
In this example we don't pass a hotspot
value, so the image will be cropped
based on what Sanity thinks is the most interesting part of the image since
SanityImage
automatically sets crop=entropy
in these cases. If you want to
override that, you can pass a hotspot
value.
Using SanityImage
for background images is easy, you just style the image to
match the expectations of your mockup. In most cases that means setting
position: relative
on the container you want to fill, then using absolute
positioning for the image. Here’s an example:
<section
css={{
position: "relative",
paddingBlock: 100,
}}
>
<SanityImage
id="..."
baseUrl="..."
width={1440}
css={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
objectFit: "cover",
userSelect: "none",
zIndex: 1,
}}
alt=""
/>
<div css={{ position: "relative", zIndex: 2 }}>
<h1>Your big hero copy</h1>
<LinkButton to="/signup/">Get started</LinkButton>
</div>
</section>
This will cause the section
to be sized based on the content inside of the
div
, and the image will be sized to fill the entire section. The aspect ratio
of the image will be maintained due to the use of object-fit: cover
. Note that
we are still using mode="contain"
for SanityImage
here. If you have a rough
idea of the height your section, you can set height
and mode="cover"
which
will prevent, for example, a portrait orientation image from being retrieved and
cropped by the browser.
Since the z-index is set higher on the div
containing the content, it will
show above the image. This example also sets user-select: none
on the image to
prevent the image from being selected when the user clicks and drags on the page
to make it behave more like a traditional background image.
If you're using Sanity's GROQ query language to fetch data, here is how I recommend fetching the fields you need from a typical image with the hotspot, crop, and low-quality image preview included:
"id": asset._ref,
"preview": asset->metadata.lqip,
hotspot { x, y },
crop {
bottom,
left,
right,
top,
}
Copyright ©2023 Corey Ward. Available under the MIT License.