When fetching images from Sanity using GROQ, you need to include specific fields to take full advantage of SanityImage features. This guide shows you how to structure your queries to get hotspot, crop, and low-quality image preview (LQIP) data.
Basic Image Query
The minimal query to use SanityImage:
*[_type == "post"][0] {
title,
mainImage {
"id": asset._ref
}
}
This gives you the image ID, which is all that’s required:
< SanityImage
id = { post . mainImage . id }
baseUrl = "..."
width = { 800 }
alt = { post . title }
/>
Recommended Full Query
For the best experience, include hotspot, crop, and preview data:
*[_type == "post"][0] {
title,
mainImage {
"id": asset._ref,
"preview": asset->metadata.lqip,
hotspot { x, y },
crop {
bottom,
left,
right,
top,
}
}
}
Now you can use all of SanityImage’s features:
< SanityImage
id = { post . mainImage . id }
baseUrl = "..."
width = { 800 }
height = { 600 }
mode = "cover"
hotspot = { post . mainImage . hotspot }
crop = { post . mainImage . crop }
preview = { post . mainImage . preview }
alt = { post . title }
/>
Understanding Image Fields
The asset._ref Field
This is the Sanity Image ID, which looks like:
image-abc123def456-1200x800-jpg
The ID contains:
Image hash: abc123def456
Dimensions: 1200x800
Format: jpg
SanityImage parses this to determine the original image dimensions and format.
Hotspot Data
Hotspot defines the focal point of the image:
type Hotspot = {
x : number // 0 to 1, left to right
y : number // 0 to 1, top to bottom
}
Example:
{
"x" : 0.5 , // Center horizontally
"y" : 0.3 // 30% from the top
}
When using mode="cover", the hotspot ensures the focal point stays visible when cropping.
The hotspot width and height fields are not used by SanityImage and can be omitted from your query.
Crop Data
Crop defines the visible portion of the image:
type Crop = {
top : number // 0 to 1
bottom : number // 0 to 1
left : number // 0 to 1
right : number // 0 to 1
}
Example:
{
"top" : 0.1 , // Crop 10% from top
"bottom" : 0.05 , // Crop 5% from bottom
"left" : 0 , // No left crop
"right" : 0 // No right crop
}
Preview (LQIP) Data
The low-quality image preview is a base64-encoded tiny image:
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...
Sanity generates this automatically. It’s used for the blur-up effect while the full image loads.
Query Patterns
Multiple Images
Query all images in a document:
*[_type == "gallery"][0] {
title,
images[] {
"id": asset._ref,
"preview": asset->metadata.lqip,
hotspot { x, y },
crop { bottom, left, right, top },
alt
}
}
Usage:
{ gallery . images . map (( image ) => (
< SanityImage
key = { image . id }
id = { image . id }
baseUrl = "..."
width = { 400 }
hotspot = { image . hotspot }
crop = { image . crop }
preview = { image . preview }
alt = { image . alt }
/>
))}
Nested Images
Query images in nested objects:
*[_type == "page"][0] {
title,
sections[] {
_type,
_type == "hero" => {
heading,
backgroundImage {
"id": asset._ref,
"preview": asset->metadata.lqip,
hotspot { x, y },
crop { bottom, left, right, top }
}
}
}
}
Referenced Images
Query images from referenced documents:
*[_type == "post"][0] {
title,
author-> {
name,
avatar {
"id": asset._ref,
"preview": asset->metadata.lqip,
hotspot { x, y },
crop { bottom, left, right, top }
}
}
}
Creating a Reusable Fragment
Define a constant for the image fields:
// lib/sanity/fragments.ts
export const IMAGE_FRAGMENT = `
"id": asset._ref,
"preview": asset->metadata.lqip,
hotspot { x, y },
crop {
bottom,
left,
right,
top,
}
`
Use it in your queries:
import { IMAGE_FRAGMENT } from './fragments'
const query = `
*[_type == "post"][0] {
title,
mainImage {
${ IMAGE_FRAGMENT }
}
}
`
TypeScript Types for Image Data
Define TypeScript types for your image data:
export type SanityImageData = {
id : string
preview ?: string
hotspot ?: {
x : number
y : number
}
crop ?: {
top : number
bottom : number
left : number
right : number
}
alt ?: string
}
export type Post = {
title : string
mainImage : SanityImageData
}
Usage:
function PostImage ({ image } : { image : SanityImageData }) {
return (
< SanityImage
id = { image . id }
baseUrl = "..."
width = { 800 }
hotspot = { image . hotspot }
crop = { image . crop }
preview = { image . preview }
alt = { image . alt || '' }
/>
)
}
Conditional Fields
Only include preview data when needed:
*[_type == "post"] {
title,
mainImage {
"id": asset._ref,
hotspot { x, y },
crop { bottom, left, right, top },
// Only include preview for featured posts
featured == true => {
"preview": asset->metadata.lqip
}
}
}
Optimizing Queries
Avoid Over-fetching
Don’t fetch image data you won’t use:
// Bad: Fetching full image data for thumbnails that don't need it
*[_type == "post"] {
thumbnail {
"id": asset._ref,
"preview": asset->metadata.lqip,
hotspot { x, y },
crop { bottom, left, right, top }
}
}
// Good: Minimal data for simple thumbnails
*[_type == "post"] {
thumbnail {
"id": asset._ref
}
}
Use GROQ projection to limit fields:
*[_type == "post"][0...10] {
_id,
title,
"image": mainImage {
"id": asset._ref,
hotspot { x, y },
crop { bottom, left, right, top }
}
} | order(publishedAt desc)
You can query additional metadata if needed:
*[_type == "post"][0] {
title,
mainImage {
"id": asset._ref,
"preview": asset->metadata.lqip,
"dimensions": asset->metadata.dimensions,
"hasAlpha": asset->metadata.hasAlpha,
"isOpaque": asset->metadata.isOpaque,
hotspot { x, y },
crop { bottom, left, right, top },
alt
}
}
Metadata fields:
dimensions - { width: number, height: number, aspectRatio: number }
hasAlpha - Whether the image has transparency
isOpaque - Whether the image is fully opaque
palette - Dominant colors in the image
You don’t need to fetch dimensions—SanityImage parses them from the image ID automatically.
Real-World Example
Complete example with a blog post:
// lib/sanity/queries.ts
import { client } from './client'
const POST_QUERY = `
*[_type == "post" && slug.current == $slug][0] {
_id,
title,
publishedAt,
author-> {
name,
image {
"id": asset._ref,
hotspot { x, y },
crop { bottom, left, right, top }
}
},
mainImage {
"id": asset._ref,
"preview": asset->metadata.lqip,
hotspot { x, y },
crop { bottom, left, right, top },
alt
},
body
}
`
export async function getPost ( slug : string ) {
return client . fetch ( POST_QUERY , { slug })
}
// components/Post.tsx
import { Image } from '@/components/Image'
export function Post ({ post }) {
return (
< article >
< Image
id = { post . mainImage . id }
width = { 1200 }
height = { 630 }
mode = "cover"
hotspot = { post . mainImage . hotspot }
crop = { post . mainImage . crop }
preview = { post . mainImage . preview }
loading = "eager"
alt = { post . mainImage . alt || post . title }
/>
< h1 > { post . title } </ h1 >
< div className = "author" >
< Image
id = { post . author . image . id }
width = { 48 }
height = { 48 }
mode = "cover"
hotspot = { post . author . image . hotspot }
crop = { post . author . image . crop }
alt = { post . author . name }
/>
< span > { post . author . name } </ span >
</ div >
{ /* Post body */ }
</ article >
)
}
Next Steps
Wrapper Component Create a reusable Image wrapper
Cover vs Contain Learn about image modes