How to programmatically set image field from another image's URL in Sanity?

10 replies
Last updated: Jan 29, 2026
Hi. I have two image fields. When the first is populated, I want to take its asset.url, create something in a canvas and add the dataUrl into the other image field. Is this a job for a plugin, or a custom input component, or is there some simpler way? Thanks.
AI Update

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

  1. Access the Sanity client: Custom input components receive the client instance via props, so you can call props.client.assets.upload() directly

  2. 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?)
  3. 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

  4. 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!

Show original thread
10 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.

Was this answer helpful?