Contents
Overview
This tutorial shows, in complete detail, how to create a Gutenberg block that lets editors upload files to the WordPress Media Library using JavaScript. It covers two common approaches: using the built-in MediaUpload component (media modal and uploader), and programmatically uploading files via the data store using wp.data.dispatch(core).uploadMedia. It also explains how to register the block, provide editor UI, save attributes, render on the front end, and handle permissions, multiple files, allowed MIME types, progress and error handling.
Prerequisites
- WordPress 5.8 (Gutenberg integrated or plugin-enabled)
- Node.js (v14 ) and npm (or yarn)
- @wordpress/scripts for bundling (recommended) or your preferred build setup
- Editor user with upload_files capability to test uploading
Recommended project structure
A simple block plugin can use this layout:
- my-file-uploader-block/
- package.json
- src/
- index.js
- edit.js
- save.js
- styles/editor.css
- styles/style.css
- build/ (generated)
- block.json
- my-file-uploader-block.php
Quick setup with @wordpress/create-block or @wordpress/scripts
Use create-block scaffolder or set up your own with @wordpress/scripts. Example package.json scripts:
{ name: my-file-uploader-block, version: 1.0.0, scripts: { build: wp-scripts build, start: wp-scripts start }, devDependencies: { @wordpress/scripts: ^25.0.0 } }
Define block metadata (block.json)
Use block.json so WordPress can register assets automatically when you use register_block_type or the block scaffolds. Keep attributes that store attachment IDs, URLs or metadata.
{ apiVersion: 2, name: my-plugin/file-uploader, title: File Uploader, category: media, icon: upload, description: Upload files to the media library and insert references., supports: { html: false }, editorScript: file-uploader-block-editor, editorStyle: file-uploader-block-editor-style, style: file-uploader-block-style, attributes: { files: { type: array, default: [] }, allowedTypes: { type: array, default: [] }, multiple: { type: boolean, default: true } } }
Register block PHP (plugin bootstrap)
Register your block and enqueue built assets (if not auto-handled). Also check capability for rendering or server-side rendering.
High-level design choices
- Use MediaUpload component: easiest integration with the media modal and the uploader (provides select/upload UI). Good for standard workflows.
- Programmatic upload: use wp.data.dispatch(core).uploadMedia to upload files you obtain from an input[type=file] or drag-and-drop handler. This gives full control over upload UI (progress, custom metadata, grouping) and is required if you want to bypass the modal.
- Store attachment IDs in block attributes: store lightweight references (IDs) and render full data either client-side (query store) or server-side (render_callback) for stable front-end output.
Editor: using MediaUpload (recommended for standard use)
The MediaUpload component opens the media modal, supports uploading, selecting multiple or single, and returns attachment objects via onSelect. Use MediaUploadCheck to ensure the user can upload.
Edit component (MediaUpload example)
import { registerBlockType } from @wordpress/blocks import { useSelect } from @wordpress/data import { MediaUpload, MediaUploadCheck, BlockControls, InspectorControls } from @wordpress/block-editor import { Button, PanelBody, ToggleControl } from @wordpress/components import { Fragment } from @wordpress/element registerBlockType( my-plugin/file-uploader, { edit: ( props ) => { const { attributes, setAttributes } = props const { files = [], multiple = true, allowedTypes = [] } = attributes // Fetch full attachment objects from the store if you only store IDs const attachments = useSelect( ( select ) => { const { getMedia } = select( core ) return files.map( ( f ) => { return typeof f === number ? getMedia( f ) : f } ) }, [ files ] ) const onSelect = ( selection ) => { // selection will be an object (single) or array (multiple) const next = Array.isArray( selection ) ? selection : [ selection ] // Store IDs to keep data compact setAttributes( { files: next.map( ( a ) => a.id ), } ) } const removeFile = ( id ) => { setAttributes( { files: files.filter( ( fid ) => fid !== id ) } ) } return () }, save: () => { // Well render on the front end using saved attributes (IDs) or allow the Editors save to store JSON. return null }, } ) {/ optional controls /} setAttributes( { multiple: val } ) } /> ( ) } /> { attachments attachments.map( ( a ) => a ? ( ) : null ) }Notes about this approach
- MediaUpload handles both selecting existing files and uploading new ones via the modal UI.
- Store attachment IDs (integers) in attributes for compact storage and stability across environments.
- Use useSelect to map IDs to full attachment objects for preview inside the editor.
- allowedTypes accepts an array of MIME types (e.g., [application/pdf, image/]) or leave undefined to allow all types the site permits.
- MediaUploadCheck ensures the UI only shows if the current user can upload files.
Editor: programmatic upload using wp.data.dispatch(core).uploadMedia
This technique is useful if you want a custom upload UI or drag-and-drop without the media modal. The core data store provides an uploadMedia action that will POST the file to the REST API, create an attachment and return the media object.
Key points
- uploadMedia takes an object: { file, title, alt, post } — file is a File/Blob.
- It returns a Promise that resolves to the created attachment object.
- The REST nonce must be present scripts enqueued in WP admin have it set automatically if dependencies include wp-api-fetch and are registered properly.
Example edit component using an input[type=file] uploader
import { useState } from @wordpress/element import { Button } from @wordpress/components import { useDispatch, useSelect } from @wordpress/data export default function Edit( { attributes, setAttributes } ) { const { files = [], multiple = true } = attributes const { uploadMedia } = useDispatch( core ) const [ uploading, setUploading ] = useState( false ) const attachments = useSelect( ( select ) => { const { getMedia } = select( core ) return files.map( ( id ) => getMedia( id ) ) }, [ files ] ) const onFileInput = async ( event ) => { const inputFiles = Array.from( event.target.files ) if ( inputFiles.length === 0 ) return setUploading( true ) try { const uploaded = [] for ( const file of inputFiles ) { // You can pass metadata like title or alt here const media = await uploadMedia( { file, title: file.name, } ) uploaded.push( media.id ) } const nextFiles = multiple ? [ ...files, ...uploaded ] : uploaded.slice( 0, 1 ) setAttributes( { files: nextFiles } ) } catch ( err ) { // handle error (display to user) console.error( Upload failed, err ) } finally { setUploading( false ) } } return ({ uploading) }Uploading...
}{ attachments attachments.map( ( a ) => a ? : null ) }
Progress reporting
The data store uploadMedia doesnt provide progress events directly. For advanced progress reporting, you can:
- Use wp.apiFetch with custom fetch options and FormData, adding an onprogress handler by using XMLHttpRequest instead of fetch.
- Use the REST route /wp/v2/media and a custom XHR upload to expose progress.
Saving and front-end rendering
Decide whether to save full markup in the post content (save component) or save minimal data (IDs) and render server-side with a render_callback. Storing IDs is more stable server-side rendering can fetch the latest attachment URLs and metadata.
Client-side save (serialized HTML)
// save.js - example when storing links directly in content export default function save( { attributes } ) { const { files = [] } = attributes // If you stored objects, you can render anchors. But keep content portable. return ({ files.map( ( f, index ) => { // If f is an object with url/title OR if you stored a minimal object, handle accordingly. const url = typeof f === object ? f.url : const title = typeof f === object ? ( f.title f.name ) : return { title url } } ) }) }
Server-side render example (recommended if saving IDs)
Use register_block_type with a render_callback that outputs attachment URLs based on stored IDs. This ensures the front-end always shows the correct URL and metadata regardless of editor JS state.
foreach ( attributes[files] as id ) { id = absint( id ) if ( ! id ) { continue } url = wp_get_attachment_url( id ) title = get_the_title( id ) mime = get_post_mime_type( id ) html .= sprintf(%2s (%3s), esc_url( url ), esc_html( title ?: basename( url ) ), esc_html( mime ) ) } html .=