Using a custom input component to upload and transform images in Sanity.io.
A 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:
Schema Setup
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
}
})Custom Input 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>
)
}Key Points
Access the Sanity client: Custom input components receive the client instance via props, so you can call
props.client.assets.upload()directlyUpload blobs: The
client.assets.upload()method acceptsFile,Blob, orBufferobjects. 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 aboveUse
renderDefaultwhen 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!
Show original thread10 replies
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.