
Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag storeA custom input component is definitely the right approach for your use case! Since you need to work with two related image fields (one original and one transformed), you'll want to create a custom input component that manages both.
Here's how to set it up based on the Custom Input Components documentation:
First, create an object type with both image fields:
import {defineField, defineType} from 'sanity'
export default defineType({
name: 'transformedImage',
type: 'object',
fields: [
defineField({
name: 'original',
type: 'image',
title: 'Original Image'
}),
defineField({
name: 'transformed',
type: 'image',
title: 'Transformed Image'
})
],
components: {
input: MyCustomImageTransform // Your custom component
}
})Then build your custom input component:
import {useCallback} from 'react'
import {Stack, Card} from '@sanity/ui'
import {set, unset} from 'sanity'
function MyCustomImageTransform(props) {
const {value, onChange, renderDefault} = props
const handleOriginalUpload = useCallback(async (originalAsset) => {
if (!originalAsset) return
// 1. Get the asset URL
const imageUrl = originalAsset.asset.url
// 2. Create your canvas and do your transformation
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
// ... your canvas transformation logic here ...
// 3. Convert canvas to blob
const blob = await new Promise(resolve =>
canvas.toBlob(resolve, 'image/png')
)
// 4. Upload the blob as a new asset using the Sanity client
const transformedAsset = await props.client.assets.upload('image', blob, {
filename: 'transformed.png'
})
// 5. Update both fields using onChange
onChange([
set({
original: originalAsset,
transformed: {
_type: 'image',
asset: {
_type: 'reference',
_ref: transformedAsset._id
}
}
})
])
}, [onChange, props.client])
// Render the default input but intercept changes
return (
<Stack space={3}>
{renderDefault({
...props,
// Override the onChange to intercept original image uploads
onChange: (patchEvent) => {
const patches = patchEvent.patches
const originalPatch = patches.find(p => p.path?.[0] === 'original')
if (originalPatch?.value) {
handleOriginalUpload(originalPatch.value)
} else {
onChange(patchEvent)
}
}
})}
</Stack>
)
}Access the Sanity client: Custom input components receive the client instance via props, so you can call props.client.assets.upload() directly
Upload blobs: The client.assets.upload() method accepts File, Blob, or Buffer objects. According to the GitHub client documentation, the signature is:
client.assets.upload(type: 'file' | 'image', body: File | Blob | Buffer | NodeJS.ReadableStream, options?)Patch with proper references: After uploading, you get back an asset document with an _id. Use this to create a proper image field value with the asset reference structure shown above
Use renderDefault when possible: This lets you leverage Sanity's built-in image input UI while intercepting the changes you need
This approach keeps everything within Sanity's asset management system and gives you full control over the transformation workflow!
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.
Content operations
Content backend


The only platform powering content operations
By Industry


Tecovas strengthens their customer connections
Build and Share

Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag store