Images
<MarkdownEditor
value={value}
onChange={setValue}
onUploadImage={async (file, context) => {
const asset = await uploadToMyStorage(file, { signal: context.signal })
return { src: asset.url, alt: asset.alt ?? '' }
}}
/>Your app decides where the file goes. The editor handles the UI state, cancellation and the Markdown that ends up in the document.
Inserting an image by URL#
If the image already has a URL you don't need to configure anything. The toolbar's image button asks for the URL, and  works in source mode.
Reference images work too. ![alt][id] keeps its definition, and the definition is kept exactly as written. See Round-trip preservation.
Uploading a file#
onUploadImage is where you turn a file into a URL.
type MarkdownUploadImage = (
file: File,
context: MarkdownUploadContext,
) => Promise<MarkdownImageValue>
interface MarkdownUploadContext {
readonly signal: AbortSignal
readonly documentKey?: string
}
interface MarkdownImageValue {
readonly src: string
readonly alt?: string
readonly title?: string
}Resolve with the URL your storage gave you back, and the editor inserts the image at the selection.
<MarkdownEditor
documentKey={note.id}
value={value}
onChange={setValue}
onUploadImage={async (file, { signal, documentKey }) => {
const body = new FormData()
body.append('file', file)
const response = await fetch(`/api/notes/${documentKey}/images`, {
method: 'POST',
body,
signal,
})
if (!response.ok) throw new Error('Upload failed')
const asset = await response.json()
return { src: asset.url, alt: file.name }
}}
/>documentKey is passed through so your endpoint knows which document the asset belongs to, which makes it possible to clean up orphaned uploads later.
Cancellation#
context.signal is aborted when the editor unmounts. Pass it to fetch or whatever client your storage uses.
If an upload outlives its editor there's nowhere to put the result. Respecting the signal keeps you from writing into a component that's already gone.
Errors#
Reject the promise and let it go through your own error reporting and toast.
The editor doesn't make up an error message and doesn't insert anything on failure. Nothing goes into the document unless the promise resolves.
Calling commands.uploadImage(file) without onUploadImage configured rejects with an error that says so (it doesn't fail silently).
Never persist a blob URL#
URL.createObjectURL(file) gives you a URL that only works in that one browser tab, and only while the page is open.
If it gets written into Markdown, it's a broken image as soon as the document is saved and reopened. It's also a memory leak, since the object stays alive until it's revoked.
The editor never writes a blob: URL into a document. Only the src your handler resolves with makes it into the Markdown.
If you want a local preview while the upload runs, keep the blob URL in your own component state and insert the image once the upload resolves.
Your own drop zone or file picker#
Build whatever UI you want and call the command from it.
'use client'
import { useMarkdownEditorContext } from '@react-markdown-kit/editor'
function ImageDropZone({ children }) {
const editor = useMarkdownEditorContext()
return (
<div
onDragOver={(event) => event.preventDefault()}
onDrop={async (event) => {
event.preventDefault()
const files = [...event.dataTransfer.files].filter((file) =>
file.type.startsWith('image/'),
)
for (const file of files) {
await editor.commands.uploadImage(file)
}
}}
>
{children}
</div>
)
}commands.uploadImage(file) runs onUploadImage and inserts the result. commands.insertImage({ src, alt, title }) inserts an image you already have a URL for.
Both are on the instance you get from useMarkdownEditor or useMarkdownEditorContext. See Headless editing.
Alt text#
Alt text is part of the content. Return real alt text from your handler when you can, and let authors edit it.
An empty alt is correct for a purely decorative image. A missing alt means something different, so don't treat them as the same.
The renderer passes alt straight through to your img component, so you can swap in an accessible image component with one override. See Components.
Rendering the result#
Images the editor writes are normal Markdown images. The renderer applies the same URL policy it uses for links, so an unsafe scheme gets emptied before it reaches the DOM.
If your assets sit behind a CDN or need sizing, override img in the preset that the editor preview and the published page share. See Presets.
Read next#
The three modes and a round-trip check you can run on your own Markdown: Editor demo.
Last updated on