How to Create Media Library Folders Without a Plugin

In this step-by-step tutorial, I’d like to guide you on how to create media library folders for your WordPress website, and we will not use any plugins for that.

The whole idea of publishing this guide came to me because of the questions of clients who are using my shared media library plugin for WordPress Multisite. Specifically, they were asking whether or not it supports third-party plugins for media library folders.

The thing is that there are no official WordPress plugins for creating media library folders. In another case, of course, I had added their support in the multisite shared media library.

First of all, I will show you how you can add a folder selection when uploading media files to the WordPress media library:

WordPress media library folders without plugins

Second, we will create a filter by a folder in the media library itself:

Filter media by folder in the WordPress media library.

If you’re not into coding, please take a look at my Simple Media Library Folders plugin.

The folders will look like this in that case:

Simple media library folders plugin for WordPress

I just wanted to show you that this option exists for you. From now on, we will only be talking about WordPress media library folders without plugins at all.

How to Add Media Library Folders to Your Site

That’s the first question we’re going to answer in this tutorial. In other words, what are the media library folders going to be?

There are two options so far:

  • We can create a custom table in our WordPress database and use it for a folder structure.
  • Or register a taxonomy for the attachment post type and use it for folders as well.

In my opinion, creating a new database table for media folders isn’t a reasonable thing to do here, because taxonomy terms already have all the data we need: name, slug, parent, count, and we can use term_group for displaying the folders in a custom order.

Another important moment is that if you choose custom database tables for that, you will need to develop an admin interface for folder management, which could be a pretty big deal.

Register a taxonomy for attachments

Great, we decided to use a custom taxonomy as a folder structure on our website. It means that it is time to use the register_taxonomy() function here. Don’t forget to put it inside the init hook, by the way.

register_taxonomy(
	'folders',
	array( 'attachment' ),
	array(
		'hierarchical' => true,
		'labels' => array(
			'name'           => 'Folders',
			'singular_name'  => 'Folder',
			'search_items'   => 'Search folders',
			'all_items'      => 'All folders',
			'parent_item'    => 'Parent folder',
			'edit_item'      => 'Edit folder',
			'update_item'    => 'Update folder',
			'add_new_item'   => 'Add new folder',
			'new_item_name'  => 'New folder name',
			'menu_name'      => 'Folders',
		),
		'show_ui' => true,
		'show_admin_column' => true,
		'query_var' => true,
		'rewrite' => false,
		'update_count_callback' => 'rudr_update_folder_attachment_count',
	)
);

Most of the parameters of the register_taxonomy() function depend on whether you’re going to use the standard taxonomy admin interface to manage media library folders or not.

For example, if you decide to display the folders in a custom way, you may skip the whole labels parameter, set show_ui to false (or even public to false), etc.

One more thing, we also need to do one of the following things:

  • Either modify the standard _update_post_term_count function using the update_count_callback parameter,
  • Or just to use the update_post_term_count_statuses hook to allow the inherit status to be counted when calculating the total number of attachments in a folder.

By default in WordPress, the attachment count in taxonomy terms can only be calculated correctly for attachments that are attached to posts.

Here is the update_count_callback-approach:

function rudr_update_folder_attachment_count( $terms, $taxonomy ) {
	global $wpdb;
	foreach ( (array) $terms as $term_id ) {
		$count = 0;

		$count += (int) $wpdb->get_var(
			$wpdb->prepare(
				"
				SELECT COUNT(*)
				FROM $wpdb->term_relationships, $wpdb->posts p1
				WHERE p1.ID = $wpdb->term_relationships.object_id
				AND post_status = 'inherit'
				AND post_type = 'attachment'
				AND term_taxonomy_id = %d
				",
				$term_id
			)
		);

		do_action( 'edit_term_taxonomy', $term_id, $taxonomy->name );

		$wpdb->update(
			$wpdb->term_taxonomy,
			compact( 'count' ),
			array( 'term_taxonomy_id' => $term_id )
		);

		do_action( 'edited_term_taxonomy', $term, $taxonomy->name );
	}
}

Funny thing, right after creating this snippet, I found out that we can use another standard WordPress callback function, which is _update_generic_term_count, and it seems to be working great for attachment taxonomies.

'update_count_callback' => '_update_generic_term_count',

Anyway, here is the update_post_term_count_statuses-approach as well:

add_filter( 'update_post_term_count_statuses', function( $statuses, $taxonomy ) {
	
	if( 'folders' === $taxonomy->name ) {
		$statuses[] = 'inherit';
		$statuses = array_unique( $statuses );
	}
	return $statuses;

}, 25, 2 );

Add, rename, or delete media library folders as regular WordPress terms

To be honest, it seems like there is nothing specific I can write about here. You can just go to Media > Folders and edit your WordPress media library folders just like any regular taxonomy terms:

Add or edit WordPress media library folders without plugins

Selecting a Folder When Uploading Media

Ok, the easy part is now over. It is time for more interesting stuff. There is going to be a lot of customization with JavaScript of the WordPress Plupload uploader and the media modal wp.media.

That’s what we should have as a result:

WordPress media library folders without plugins

Let’s start with the PHP code snippet and the pre-upload-ui action hook:

add_action( 'pre-upload-ui', 'rudr_select_folder' );
function rudr_select_folder() {

	wp_dropdown_categories( array(
		'hide_empty'       => 0,
		'hide_if_empty'    => false,
		'taxonomy'         => 'folders',
		'name'             => 'folder_id',
		'id'               => 'folders',
		'orderby'          => 'name',
		'hierarchical'     => true,
		'show_option_none' => 'Choose folder',
	) );

}

The JavaScript code is going to be different depending on which page we’re currently implementing the folder selection.

Media > Add New page:

if( $( 'body.wp-admin' ).hasClass( 'media-new-php' ) && 'object' == typeof window.uploader ) {
	window.uploader.bind( 'BeforeUpload', function( up ) {
		const settings = up.settings.multipart_params;
		settings.folder_id = $( '#folder_id' ).val()
	})
}

Inside the WordPress media modal window, when editing a post, and not only:

if( 'function' == typeof wp.Uploader ) {
	$.extend( wp.Uploader.prototype, {
		init: function() {
			
			let selectedFolder = -1;
			
			// this part is a little bit tricky, but we need it without a doubt
			$( 'body' ).on( 'change', '#folder_id', function() {
				selectedFolder = $(this).val()
			} )
			
			this.uploader && this.uploader.bind( 'BeforeUpload', function( up, file ) {
				up.settings.multipart_params = up.settings.multipart_params || {}
				up.settings.multipart_params.folder_id = selectedFolder
			})
		}
	})
}

One more thing here: The code mentioned above most likely won’t be triggered unless you run it inside the setTimeout() function (with any time interval).

Ok, now it is time to process the folder_id parameter when uploading an attachment. Luckily, this part is easy and can be achieved with a WordPress hook add_attachment.

add_action( 'add_attachment', 'rudr_add_attachment_to_folder' );

function rudr_add_attachment_to_folder( $attachment_id ) {
	
	if( 
		! isset( $_POST[ '_wpnonce' ] ) 
		|| ! wp_verify_nonce( $_POST[ '_wpnonce' ], 'media-form' ) 
	) {
		return;
	}
	
	$folder_id = ! empty( $_POST[ 'folder_id' ] ) ? (int) $_POST[ 'folder_id' ] : 0;
	if( ! $folder_id ) ) {
		return;
	}
	
	wp_set_object_terms( $attachment_id, $folder_id, 'folders' );

}

Don’t forget about validating the nonce. It is easy, because it has the same $action value in both places.

Change a Folder When Editing Media

There are 3 different scenarios here:

  1. When editing a media file from the “List view” of your media library, your media library folders will be displayed just like a custom taxonomy in the Classic Editor. There is nothing we need to do here, it will work out of the box.
  2. When editing a media file from the “Grid view”, the folder structure won’t be displayed correctly by default. There is only a text field that is going to be displayed, which is not good. And that’s where we need to do most of the work.
  3. You can always create a bunch of bulk actions for adding or removing a media file to a specific media library folder.

All right, now our goal is to display a media library folder structure like on the screenshot below:

Edit attachment media library folders

Good news – no JavaScript in this chapter, guys, the PHP code snippet is below:

add_filter( 'attachment_fields_to_edit', 'rudr_edit_attachment_fields', 25, 2 );
function rudr_edit_attachment_fields( $form_fields, $post ) {

	// prepare an array for new folder fields here
	$folder_fields = array(
		'label' => 'Folders',
		'show_in_edit' => false,
		'input' => 'html',
		'value' => '',
	);
	
	$taxonomy_name = 'folders';
	
	// get the assigned media library folders from the cache
	$terms = get_the_terms( $post->ID, $taxonomy_name );
	if( $terms ) {
		$folder_fields[ 'value' ]  = join( ', ', wp_list_pluck( $terms, 'slug' ) );
	}

	ob_start();

	wp_terms_checklist(
		$post->ID,
		array(
			'taxonomy' => $taxonomy_name,
			'checked_ontop' => false,
			'walker' => new Rudr_Folders_Walker()
		)
	);

	$html = '<ul class="term-list">' . ob_get_contents() . '</ul>';

	ob_end_clean();

	$folder_fields[ 'html' ] = $html;

	$form_fields[ $taxonomy_name ] = $folder_fields;

	return $form_fields;
}

class Rudr_Folders_Walker extends Walker {

	var $db_fields = array(
		'parent' => 'parent',
		'id'     => 'term_id',
	);

	function start_lvl( &$output, $depth = 0, $args = array() ) {
		$indent  = str_repeat( "\t", $depth );
		$output .= "$indent<ul class='children'>\n";
	}

	function end_lvl( &$output, $depth = 0, $args = array() ) {
		$indent  = str_repeat( "\t", $depth );
		$output .= "$indent</ul>\n";
	}

	function start_el( &$output, $term, $depth = 0, $args = array(), $id = 0 ) {
	
		$output .= sprintf(
			"\n<li id='%s-%s'><label class='selectit'><input value='%s' type='checkbox' name='%s' id='%s' %s %s /> %s</label>",
			$term->taxonomy,
			$term->term_id,
			$term->slug,
			"tax_input[{$term->taxonomy}][{$term->slug}]",
			"in-{$term->taxonomy}-{$term->term_id}",
			checked( in_array( $term->term_id, $args[ 'selected_cats' ] ), true, false ),
			disabled( empty( $args[ 'disabled' ] ), false, false ),
			esc_html( $term->name )
		);

	}

	function end_el( &$output, $term, $depth = 0, $args = array() ) {
		$output .= "</li>\n";
	}

}

Are your folder checkboxes getting saved correctly? That’s what I thought.

The thing is that WordPress expects the value of folder fields to be a comma-separated string, but we just provide an array instead. There are different ways of fixing it, by the way. For example, we can alter the wp_ajax_save_attachment_compat() function with our custom one using the hook below:

add_filter( 'wp_ajax_save-attachment-compat', 'custom_save_attachment_compat', 0 );

Or we can also do some tricks with JavaScript to make sure WordPress receives a comma-separated string of custom taxonomy (folders) slugs.

Almost forgot, just a little bit of CSS:

.compat-field-folders .term-list {
	margin-top: 5px;
}
.compat-field-folders .children {
	margin: 5px 0 0 20px;
}
.compat-field-folders .field input[type=checkbox] {
	margin-top: 0;
}

Create a Filter to Display Media from a Specific Folder

If you think that we can easily achieve that using the restrict_manage_posts action, well, you’re 50% right, because everything depends on whether the “Grid” or “List” view is in use.

In the “List” view, it will be sufficient to use the restrict_manage_posts action hook, and our code snippet is going to be like the one below:

add_action( 'restrict_manage_posts', 'rudr_list_view_filter' );

function rudr_list_view_filter() {
	global $typenow;

	if( 'attachment' !== $typenow ) {
		return;
	}

	$selected = isset( $_GET[ 'folders' ] ) ? $_GET[ 'folders' ] : false;
	wp_dropdown_categories(
		array(
			'show_option_all' =>  'All folders',
			'taxonomy'        =>  'folders',
			'name'            =>  'folders',
			'orderby'         =>  'name',
			'selected'        =>  $selected,
			'hierarchical'    =>  true,
			'value_field'     => 'slug',
			'depth'           =>  3,
			'hide_empty'      =>  true,
		)
	);

}

In the “Grid” view, the solution is significantly more complicated:

(function(){

	const MediaLibraryTaxonomyFilter = wp.media.view.AttachmentFilters.extend({
		// literally anything here
		id: 'rudr-grid-taxonomy-filter',

		createFilters: function() {
			const filters = {}

			_.each( FolderTaxonomyTerms.terms || {}, function( value, index ) {
				filters[ value.term_id ] = {
					text: value.name,
					props: {
						folders: value.slug,
					}
				}
			})
			filters.all = {
				text:  'All folders',
				props: {
					folders: ''
				},
				priority: 10
			}
			this.filters = filters
		}
	})
	
	// add the current AttachmentsBrowser into a constant
	const AttachmentsBrowser = wp.media.view.AttachmentsBrowser;
	wp.media.view.AttachmentsBrowser = AttachmentsBrowser.extend({
		createToolbar: function() {
			AttachmentsBrowser.prototype.createToolbar.call( this )
			this.toolbar.set( 
				'MediaLibraryTaxonomyFilter', 
				new MediaLibraryTaxonomyFilter({
					controller: this.controller,
					model:      this.collection.props,
					priority: -75
				}).render() 
			)
		}
	})

})()

If you’re wondering what FolderTaxonomyTerms is – it is just an object of folders, which I printed using the wp_localize_script() and get_terms() functions.

Here is the result, by the way:

Filter media by folder in the WordPress media library.

With the help of this code, the filters will appear not only in the Media > Library page but also in the media modal, where you select images when you want to insert them in posts.

Media Library Folders in WordPress Multisite

Finally, we’re ready to talk about WordPress Multisite compatibility.

Under multisite compatibility, I assume using the shared media library folders together with my Multisite Shared Media Library plugin, which allows you to use a single media library and its files across the whole multisite network.

By the way, this feature is available in my Simple Media Library Folders plugin.

Speaking of code integration, it is not that difficult; we just need to keep in mind the following:

  • The custom taxonomy from the first step should be registered on all sites across the multisite network, or at least on the main site.
  • When displaying the folder selection when uploading and when filtering media, don’t forget to use the switch_to_blog() function right before the wp_dropdown_categories() and restore_current_blog() after it.
  • Of course, all the JavaScript code should be included in all sites within a multisite network.

I think that’s pretty much it. Let me know if I forgot to mention anything.

Misha Rudrastyh

Misha Rudrastyh

Hey guys and welcome to my website. For more than 15 years I've been doing my best to share with you some superb WordPress guides and tips for free.

Need some developer help? Contact me

Follow me on X