Multiple upload inputs in a WordPress theme options page?

WordPress settings API

Lots of info and tutorials on the net on the subject of WordPress “theme options” or  “admin” pages. Many of them are out dated and don’t subscribe to the current best practices as outlined by WordPress – in other words the settings API. Nettuts is always a great place to start. In my case, the tutorial the resinated the most with me was a fairly recent post by Aliso The Geek. Her tutorial is clear, easy to follow, results in a organized tabbed theme options page and best of all, your new WordPress theme options page will be 100% contained within a tidy class file – easily transferred from theme to theme is so desired.

So what’s left to expound upon?

Not much, unless you’d like to include and upload field. The easiest way to implement this, with some possible pitfalls (more on this later), is to use WordPress’ built in Media Uploader and Thinkbox script.

Step one: Back to your theme options class

Since we are adding a new “type” we’re going to need to add a new case to the switch statement.

case 'upload':
echo '<input id="' . $id . '" class="upload-url' . $field_class . '" type="text" name="mytheme_options[' . $id . ']" value="' . esc_attr( $options[$id] ) . '" />'
'<input id="st_upload_button" class="st_upload_button" type="button" name="upload_button" value="Upload" />';

if ( $desc != '' )
echo '
<span class="description">' . $desc . '</span>';


Essentially the same as the ‘text’ case. Change type to “file”, add an input button with a class of “st_upload_button” (or whatever you so desire) that will later trigger the media uploader.

Step Two: Add the upload field Array

$this->settings['st_upload'] = array(
'title' => __( 'Example upload Input' ),
'desc' => __( 'This is a description for the upload input.' ),
'std' => 'My logo',
'type' => 'upload',
'section' => 'general'

Nothing new here…

  1. Name it what you want
  2. Your title
  3. Your description
  4. Default Value (for the text field in this case)
  5. Type (our new type, yay)
  6. Whatever section you’d like to appear

Refresh your WordPress theme options page and your upload field should be present and look something like this:

Step Three: Gather the tools

First we need to load a few scripts and styles.

  1. Medial Uploader script
  2. Thickbox script
  3. Thickbox CSS
  4. And finally, “my-upload” script

We already have a scripts() and styles() function in our class, so this is really quite easy.

//Media Uploader Scripts
wp_register_script('my-upload', get_bloginfo( 'stylesheet_directory' ) . '/js/uploader.js', array('jquery','media-upload','thickbox'));

//Media Uploader Style

Step Four: Make it do something cool

jQuery(document).ready(function() {

jQuery('.st_upload_button').click(function() {
targetfield = jQuery(this).prev('.upload-url');
tb_show('', 'media-upload.php?type=image&amp;TB_iframe=true');
return false;

window.send_to_editor = function(html) {
imgurl = jQuery('img',html).attr('src');


When the upload button is clicked, we launch the WordPress media uploader and set the ‘targetfield’ variable to the previous text input. We’ll swing back around for this later. Next we overwrite send_to_editor,  get the image URL from the SCR attribute and then finally put that value in the target field.


My main complaint with the media uploader in this situation is it’s not particularly intuitive for clients or people not familiar with the finner details of WordPress. One has to either upload an image or choose from the media library and then click “insert into post”. On the other hand, it is consistent with the rest of the WordPress GUI and allows you to choose from already uploaded images.

30 thoughts on “Multiple upload inputs in a WordPress theme options page?

  1. You are my hero. I have been searching for EONS for a simple way to include uploads in an options page. Found lots of frameworks, but nothing that was simple and straight-forward. Aliso’s options page tutorial was perfect, except that it was missing this. And now my world is complete. THANK YOU!!! 🙂

  2. Hey Genevieve. Aliso did the heavy lifting. It wasn’t until I read her tutorial did I really wrap my head around the WordPress Settings API.

    Adding the upload field(s), just makes it that much sweeter…

  3. HI! Thank you for this script.
    I’m using the theme option from Alison:

    And your script works when i click over UPLOAD button, the wordpress media upload pop up, i choose a image, i click on ‘insert in post’ and it comes back to the previous upload field. When I click SAVE, it works fine. The image is stored in Media attachs.


    When i come back to MENU OPTION, the field is blank. No more url shows…

    I wonder if you plan to to do something like that:

    When clicking again to show this option, that image stored could be printed in the field upload, and when clicking again over UPLOAD button, maybe load wordpress media again, but appointing to the same stored image…

  4. Hey Mira. I’m not sure I quite understand you…

    Are you saying that your link is not saved when when you return to your theme options page? If that’s the case, it’s more then likely that that issue is in your theme options class and not in this script. The image URL paths DO are saved in the coinciding text field if implemented correctly.

  5. Other thing… the image is uploaded fine! But is not stored in option inside database. so i can’t do ‘echo’ option(‘image_id’);

  6. Justin, i fixed! YEAHHH! 🙂

    I forgot to switch ‘mytheme_option’. Now it is ok! Thank you dude!!!

    Do you plan to do a color pick too?

  7. Using tagetfield has a varible isnt working.


    That wont work but


    will… Why is this?

  8. @Eric-
    Tough to say without looking at your code… Are you sure that you cached “jQuery(this).prev(‘.upload-url’);” correctly? I see a misspelling in “targetfield” in you post here.

    Remember, the “target field” is initiated when you click on the upload button. jQuery then looks for the previous element in the DOM with the class ‘upload-url’. hard coding a taget field with specific ID or Class will work just fine until you have more then one upload.

  9. I think here is my issue.

    For some odd reason the variable isnt working.

    Say like


    Will not work at all. However…


    will work just fine, however this stops me from using multi fields.

  10. @Eric, What does your target field look like? Does it look like this:

     targetfield = jQuery(this).prev('.upload-url');

    Have you rearranged your markup in the case statement?

  11. Excellent!, i recently implemented on my theme options ;).

    Now i’m trying to implement the resize/crop function like in WordPress Custom Header, after upload they have Cut & publish, if in your upload field Array put 2 new options (width & height) and call this cool function for crop or resize image to the array values, any idea :P?

    Thank you and regards!

  12. @joan – what you’re looking for is a built in WordPress function not really related to your them options page – Add_image_size.

    if ( function_exists( 'add_theme_support' ) ) {
    	add_theme_support( 'post-thumbnails' );
            set_post_thumbnail_size( 150, 150 ); // default Post Thumbnail dimensions   
    if ( function_exists( 'add_image_size' ) ) { 
    	add_image_size( 'category-thumb', 300, 9999 ); //300 pixels wide (and unlimited height)
    	add_image_size( 'homepage-thumb', 220, 180, true ); //(cropped)
    //add more sizes here...


  13. @Justin:

    What i meant is to use the functions from Custom Header (wp-admin/custom-header.php) for the upload field in our new upload type in theme options, extending it with 2 new fields for width & height.

    It can be a very cool feature for average users, with this implemented they can upload any image size and the functions will resize/crop automatically to the field values, exactly the same what the “Custom Header” theme feature does.

    The functions:

    function js_2() { ?>
    <script type="text/javascript">
    /* <![CDATA[ */
    	function onEndCrop( coords ) {
    		jQuery( '#x1' ).val(coords.x);
    		jQuery( '#y1' ).val(coords.y);
    		jQuery( '#width' ).val(coords.w);
    		jQuery( '#height' ).val(coords.h);
    	jQuery(document).ready(function() {
    		var xinit = <?php echo HEADER_IMAGE_WIDTH; ?>;
    		var yinit = <?php echo HEADER_IMAGE_HEIGHT; ?>;
    		var ratio = xinit / yinit;
    		var ximg = jQuery('img#upload').width();
    		var yimg = jQuery('img#upload').height();
    		if ( yimg < yinit || ximg < xinit ) {
    			if ( ximg / yimg > ratio ) {
    				yinit = yimg;
    				xinit = yinit * ratio;
    			} else {
    				xinit = ximg;
    				yinit = xinit / ratio;
    			handles: true,
    			keys: true,
    			aspectRatio: xinit + ':' + yinit,
    			show: true,
    			x1: 0,
    			y1: 0,
    			x2: xinit,
    			y2: yinit,
    			maxHeight: <?php echo HEADER_IMAGE_HEIGHT; ?>,
    			maxWidth: <?php echo HEADER_IMAGE_WIDTH; ?>,
    			onInit: function () {
    			onSelectChange: function(img, c) {
    /* ]]> */
    	 * Display second step of custom header image page.
    	 * @since 2.1.0
    	function step_2() {
    		check_admin_referer('custom-header-upload', '_wpnonce-custom-header-upload');
    		if ( ! current_theme_supports( 'custom-header-uploads' ) )
    			wp_die( __( 'Cheatin’ uh?' ) );
    		$overrides = array('test_form' => false);
    		$file = wp_handle_upload($_FILES['import'], $overrides);
    		if ( isset($file['error']) )
    			wp_die( $file['error'],  __( 'Image Upload Error' ) );
    		$url = $file['url'];
    		$type = $file['type'];
    		$file = $file['file'];
    		$filename = basename($file);
    		// Construct the object array
    		$object = array(
    		'post_title' => $filename,
    		'post_content' => $url,
    		'post_mime_type' => $type,
    		'guid' => $url,
    		'context' => 'custom-header'
    		// Save the data
    		$id = wp_insert_attachment($object, $file);
    		list($width, $height, $type, $attr) = getimagesize( $file );
    		if ( $width == HEADER_IMAGE_WIDTH && $height == HEADER_IMAGE_HEIGHT ) {
    			// Add the meta-data
    			wp_update_attachment_metadata( $id, wp_generate_attachment_metadata( $id, $file ) );
    			update_post_meta( $id, '_wp_attachment_is_custom_header', get_option('stylesheet' ) );
    			set_theme_mod('header_image', esc_url($url));
    			do_action('wp_create_file_in_uploads', $file, $id); // For replication
    			return $this->finished();
    		} elseif ( $width > HEADER_IMAGE_WIDTH ) {
    			$oitar = $width / HEADER_IMAGE_WIDTH;
    			$image = wp_crop_image($file, 0, 0, $width, $height, HEADER_IMAGE_WIDTH, $height / $oitar, false, str_replace(basename($file), 'midsize-'.basename($file), $file));
    			if ( is_wp_error( $image ) )
    				wp_die( __( 'Image could not be processed.  Please go back and try again.' ), __( 'Image Processing Error' ) );
    			$image = apply_filters('wp_create_file_in_uploads', $image, $id); // For replication
    			$url = str_replace(basename($url), basename($image), $url);
    			$width = $width / $oitar;
    			$height = $height / $oitar;
    		} else {
    			$oitar = 1;
    <div class="wrap">
    <?php screen_icon(); ?>
    <h2><?php _e( 'Crop Header Image' ); ?></h2>
    <form method="post" action="<?php echo esc_attr(add_query_arg('step', 3)); ?>">
    	<p class="hide-if-no-js"><?php _e('Choose the part of the image you want to use as your header.'); ?></p>
    	<p class="hide-if-js"><strong><?php _e( 'You need Javascript to choose a part of the image.'); ?></strong></p>
    	<div id="crop_image" style="position: relative">
    		<img src="<?php echo esc_url( $url ); ?>" id="upload" width="<?php echo $width; ?>" height="<?php echo $height; ?>" />
    	<input type="hidden" name="x1" id="x1" value="0"/>
    	<input type="hidden" name="y1" id="y1" value="0"/>
    	<input type="hidden" name="width" id="width" value="<?php echo esc_attr( $width ); ?>"/>
    	<input type="hidden" name="height" id="height" value="<?php echo esc_attr( $height ); ?>"/>
    	<input type="hidden" name="attachment_id" id="attachment_id" value="<?php echo esc_attr( $id ); ?>" />
    	<input type="hidden" name="oitar" id="oitar" value="<?php echo esc_attr( $oitar ); ?>" />
    	<?php wp_nonce_field( 'custom-header-crop-image' ) ?>
    	<?php submit_button( __( 'Crop and Publish' ) ); ?>
    function step_3() {
    		if ( ! current_theme_supports( 'custom-header-uploads' ) )
    			wp_die( __( 'Cheatin’ uh?' ) );
    		if ( $_POST['oitar'] > 1 ) {
    			$_POST['x1'] = $_POST['x1'] * $_POST['oitar'];
    			$_POST['y1'] = $_POST['y1'] * $_POST['oitar'];
    			$_POST['width'] = $_POST['width'] * $_POST['oitar'];
    			$_POST['height'] = $_POST['height'] * $_POST['oitar'];
    		$attachment_id = absint( $_POST['attachment_id'] );
    		$original = get_attached_file($attachment_id);
    		$cropped = wp_crop_image( $attachment_id, (int) $_POST['x1'], (int) $_POST['y1'], (int) $_POST['width'], (int) $_POST['height'], HEADER_IMAGE_WIDTH, HEADER_IMAGE_HEIGHT );
    		if ( is_wp_error( $cropped ) )
    			wp_die( __( 'Image could not be processed.  Please go back and try again.' ), __( 'Image Processing Error' ) );
    		$cropped = apply_filters('wp_create_file_in_uploads', $cropped, $attachment_id); // For replication
    		$parent = get_post($attachment_id);
    		$parent_url = $parent->guid;
    		$url = str_replace(basename($parent_url), basename($cropped), $parent_url);
    		// Construct the object array
    		$object = array(
    			'ID' => $attachment_id,
    			'post_title' => basename($cropped),
    			'post_content' => $url,
    			'post_mime_type' => 'image/jpeg',
    			'guid' => $url,
    			'context' => 'custom-header'
    		// Update the attachment
    		wp_insert_attachment($object, $cropped);
    		wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $cropped ) );
    		update_post_meta( $attachment_id, '_wp_attachment_is_custom_header', get_option('stylesheet' ) );
    		set_theme_mod('header_image', $url);
    		// cleanup
    		$medium = str_replace(basename($original), 'midsize-'.basename($original), $original);
    		@unlink( apply_filters( 'wp_delete_file', $medium ) );
    		@unlink( apply_filters( 'wp_delete_file', $original ) );
    		return $this->finished();

    Forgive the misunderstanding, my english is very basic…

  14. Hey Justin W, i tried this but images are not getting saved, i m sorry for my poor english, only one image return the true value, rest just show blank, i think something wrong with the js or mytheme_options, can u please help me out from this mess???

  15. hey Guys i just sorted it out, check this out….
    add this code into your JS file.

    jQuery(document).ready(function($) {

    $( ‘input.uploading’ ).click(function() {
    formfield = jQuery(this).attr(‘name’);
    tb_show(”, ‘media-upload.php?type=image&TB_iframe=true’);
    return false;

    window.send_to_editor = function(html) {
    imgurl = jQuery(‘img’,html).attr(‘src’);

    and add this to your theme option.php………..

    <input class="" name="” id=”” type=”text” value=”” />
    <input class="uploading" id="” type=”button” name=”” value=”Upload Image” />

  16. @Adeel-

    That’s the same thing I have posted above but instead of traveling up the DOM until a ‘upload-url’ class is found, your looking for the name atribute.

    The reason it wasn’t working for you is because you removed that the upload-url class.

  17. @Joan-

    I see. Yes, the custom header. I generally don’t use that often. As you’ve become aware, it’s a whole lot more more work for very little gain. Typically I just specify crop dimensions or sometimes use the timbthumb script. Though, with all the security issues surrounding that, i’ve sort of backed off that too.

  18. I just have to say this worked PERFECTLY!!! you dah man!!! nice work. I see you did a tut on how to add a color picker and I am gonna go through that one next.

    I also noticed that you said you added a slider?…lol can’t wait for that one.

    If i can make a suggestion for another tutorial for you. I think a tutorial on adding sidebars would be really cool. I have tried to follow this one:

    but can’t get my head around it as I am using what Aliso provided. So if you get a chance, this might be something cool to figure, wish i could, but I’m sure it would be a piece of cake for you.

    Keep up the good work and keep them coming.

  19. Adding a color picker is super easy. The slider isn’t terribly difficult either but requires a little experience with the jQuery UI.

    I haven’t really done anything with adding custom sidebars. Not that it’s not useful, I just haven’t had the time or a situation where i’ve needed it.

  20. Understandable, but none the less your tuts are wonderful and have helped a lot. Thank you very much.

    And if you get the time, maybe you can “take a peek” into a tut on adding those custom (hint, hint) 🙂

  21. Thank you.. The class by ‘Aliso the Geek’ is far better than other scripts I’ve come across so far.. It’s clean and understandable.. and thanks for expanding it with the upload feature..

  22. I’m using the WordPress Settings API to create an options page for a plugin that handles images and links. I can register 3 add_settings_field calls and add 3 images and links to the site. Does anyone know of a way to dynamically let the user add a new field (add_settings_field) on the fly. For instance if it started with 1 field, but they needed a second for another picture.

  23. Sorry, complete newbie here with this. I got everything except Step 3 & 4. Where do those lines of code go?

    Also, on step one, I am getting a syntax error. Do I need to put a ; at the end of line 3?

Leave a Reply

To post code -> [code]// Your code here[/code]