Using a custom input component to upload and transform images in Sanity.io.

10 replies
Last updated: Jun 8, 2021
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
what are you trying to do with the two fields? like do an image transformation in-browser and re-upload it to another field? or like provide a default value based on another field?
I’ll pass the asset url from the first field to three.js to wrap a 3D object as a texture, then push the resulting canvas.dataUrl into the other image field. Or that’s the plan anyway.
So the first one!
ah so you’ll be using three.js to create a 3D image? do you want to keep the original upload too?
Yeah I’ll need both. Hence the two fields.
As far as I can tell, a custom input component is just for a single field. But a plugin might allow me to have multiple fields and basically do all the work in React. The docs don’t make the distinction clear at all. I’m not sure about populating the 2nd field at the end though.
for your case, i think i would create an object type with two sub-fields
originalImage
transformedImage
(or similar) then I would create a custom input component for the whole object type (that way it’s like one custom input for two fields)
in this component, you’d have your own image input component. on a file drop, you can apply your transform and then on save button click, you can use the sanity client to upload the assets to sanity and write the asset IDs back to the document
brb. let me find some links
uploading assets with the sanity client (i believe it accepts a blob too)
creating a custom input component i think the way you want to do it is to upload the assets to sanity via the client, then use the resulting asset IDs to create a patch in the custom input
Good plan. Thanks User, I’ll give it a go.

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?