diff --git src/wp-admin/customize.php src/wp-admin/customize.php
index ed525581..eac27de 100644
--- src/wp-admin/customize.php
+++ src/wp-admin/customize.php
@@ -53,8 +53,6 @@ do_action( 'customize_controls_init' );
 wp_enqueue_script( 'customize-controls' );
 wp_enqueue_style( 'customize-controls' );
 
-wp_enqueue_script( 'accordion' );
-
 /**
  * Enqueue Customizer control scripts.
  *
@@ -130,7 +128,7 @@ do_action( 'customize_controls_print_scripts' );
 		?>
 
 		<div id="widgets-right"><!-- For Widget Customizer, many widgets try to look for instances under div#widgets-right, so we have to add that ID to a container div in the Customizer for compat -->
-		<div class="wp-full-overlay-sidebar-content accordion-container" tabindex="-1">
+		<div class="wp-full-overlay-sidebar-content" tabindex="-1">
 			<div id="customize-info" class="accordion-section <?php if ( $cannot_expand ) echo ' cannot-expand'; ?>">
 				<div class="accordion-section-title" aria-label="<?php esc_attr_e( 'Customizer Options' ); ?>" tabindex="0">
 					<span class="preview-notice"><?php
@@ -160,13 +158,9 @@ do_action( 'customize_controls_print_scripts' );
 				<?php endif; ?>
 			</div>
 
-			<div id="customize-theme-controls"><ul>
-				<?php
-				foreach ( $wp_customize->containers() as $container ) {
-					$container->maybe_render();
-				}
-				?>
-			</ul></div>
+			<div id="customize-theme-controls">
+				<ul><?php // Panels and sections are managed here via JavaScript ?></ul>
+			</div>
 		</div>
 		</div>
 
@@ -252,10 +246,13 @@ do_action( 'customize_controls_print_scripts' );
 		),
 		'settings' => array(),
 		'controls' => array(),
+		'panels'   => array(),
+		'sections' => array(),
 		'nonce'    => array(
 			'save'    => wp_create_nonce( 'save-customize_' . $wp_customize->get_stylesheet() ),
 			'preview' => wp_create_nonce( 'preview-customize_' . $wp_customize->get_stylesheet() )
 		),
+		'autofocus' => array(),
 	);
 
 	// Prepare Customize Setting objects to pass to Javascript.
@@ -266,10 +263,32 @@ do_action( 'customize_controls_print_scripts' );
 		);
 	}
 
-	// Prepare Customize Control objects to pass to Javascript.
+	// Prepare Customize Control objects to pass to JavaScript.
 	foreach ( $wp_customize->controls() as $id => $control ) {
-		$control->to_json();
-		$settings['controls'][ $id ] = $control->json;
+		$settings['controls'][ $id ] = $control->json();
+	}
+
+	// Prepare Customize Section objects to pass to JavaScript.
+	foreach ( $wp_customize->sections() as $id => $section ) {
+		$settings['sections'][ $id ] = $section->json();
+	}
+
+	// Prepare Customize Panel objects to pass to JavaScript.
+	foreach ( $wp_customize->panels() as $id => $panel ) {
+		$settings['panels'][ $id ] = $panel->json();
+		foreach ( $panel->sections as $section_id => $section ) {
+			$settings['sections'][ $section_id ] = $section->json();
+		}
+	}
+
+	// Pass to frontend the Customizer construct being deeplinked
+	if ( isset( $_GET['autofocus'] ) && is_array( $_GET['autofocus'] ) ) {
+		$autofocus = wp_unslash( $_GET['autofocus'] );
+		foreach ( $autofocus as $type => $id ) {
+			if ( isset( $settings[ $type . 's' ][ $id ] ) ) {
+				$settings['autofocus'][ $type ] = $id;
+			}
+		}
 	}
 
 	?>
diff --git src/wp-admin/js/accordion.js src/wp-admin/js/accordion.js
index 6cb1c1c..1769d27 100644
--- src/wp-admin/js/accordion.js
+++ src/wp-admin/js/accordion.js
@@ -25,9 +25,6 @@
  *
  * Note that any appropriate tags may be used, as long as the above classes are present.
  *
- * In addition to the standard accordion behavior, this file includes JS for the
- * Customizer's "Panel" functionality.
- *
  * @since 3.6.0.
  */
 
@@ -46,20 +43,8 @@
 			accordionSwitch( $( this ) );
 		});
 
-		// Go back to the top-level Customizer accordion.
-		$( '#customize-header-actions' ).on( 'click keydown', '.control-panel-back', function( e ) {
-			if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key
-				return;
-			}
-
-			e.preventDefault(); // Keep this AFTER the key filter above
-
-			panelSwitch( $( '.current-panel' ) );
-		});
 	});
 
-	var sectionContent = $( '.accordion-section-content' );
-
 	/**
 	 * Close the current accordion section and open a new one.
 	 *
@@ -69,75 +54,22 @@
 	function accordionSwitch ( el ) {
 		var section = el.closest( '.accordion-section' ),
 			siblings = section.closest( '.accordion-container' ).find( '.open' ),
-			content = section.find( sectionContent );
+			content = section.find( '.accordion-section-content' );
 
 		// This section has no content and cannot be expanded.
 		if ( section.hasClass( 'cannot-expand' ) ) {
 			return;
 		}
 
-		// Slide into a sub-panel instead of accordioning (Customizer-specific).
-		if ( section.hasClass( 'control-panel' ) ) {
-			panelSwitch( section );
-			return;
-		}
-
 		if ( section.hasClass( 'open' ) ) {
 			section.toggleClass( 'open' );
 			content.toggle( true ).slideToggle( 150 );
 		} else {
 			siblings.removeClass( 'open' );
-			siblings.find( sectionContent ).show().slideUp( 150 );
+			siblings.find( '.accordion-section-content' ).show().slideUp( 150 );
 			content.toggle( false ).slideToggle( 150 );
 			section.toggleClass( 'open' );
 		}
 	}
 
-	/**
-	 * Slide into an accordion sub-panel.
-	 *
-	 * For the Customizer-specific panel functionality
-	 *
-	 * @param {Object} panel Title element or back button of the accordion panel to toggle.
-	 * @since 4.0.0
-	 */
-	function panelSwitch( panel ) {
-		var position, scroll,
-			section = panel.closest( '.accordion-section' ),
-			overlay = section.closest( '.wp-full-overlay' ),
-			container = section.closest( '.accordion-container' ),
-			siblings = container.find( '.open' ),
-			topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ).add( '#customize-info > .accordion-section-title' ),
-			backBtn = overlay.find( '.control-panel-back' ),
-			panelTitle = section.find( '.accordion-section-title' ).first(),
-			content = section.find( '.control-panel-content' );
-
-		if ( section.hasClass( 'current-panel' ) ) {
-			section.toggleClass( 'current-panel' );
-			overlay.toggleClass( 'in-sub-panel' );
-			content.delay( 180 ).hide( 0, function() {
-				content.css( 'margin-top', 'inherit' ); // Reset
-			} );
-			topPanel.attr( 'tabindex', '0' );
-			backBtn.attr( 'tabindex', '-1' );
-			panelTitle.focus();
-			container.scrollTop( 0 );
-		} else {
-			// Close all open sections in any accordion level.
-			siblings.removeClass( 'open' );
-			siblings.find( sectionContent ).show().slideUp( 0 );
-			content.show( 0, function() {
-				position = content.offset().top;
-				scroll = container.scrollTop();
-				content.css( 'margin-top', ( 45 - position - scroll ) );
-				section.toggleClass( 'current-panel' );
-				overlay.toggleClass( 'in-sub-panel' );
-				container.scrollTop( 0 );
-			} );
-			topPanel.attr( 'tabindex', '-1' );
-			backBtn.attr( 'tabindex', '0' );
-			backBtn.focus();
-		}
-	}
-
 })(jQuery);
diff --git src/wp-admin/js/customize-controls.js src/wp-admin/js/customize-controls.js
index 812f4fa..536e05a 100644
--- src/wp-admin/js/customize-controls.js
+++ src/wp-admin/js/customize-controls.js
@@ -1,6 +1,6 @@
 /* globals _wpCustomizeHeader, _wpMediaViewsL10n */
 (function( exports, $ ){
-	var api = wp.customize;
+	var bubbleChildValueChanges, Container, focus, api = wp.customize;
 
 	/**
 	 * @constructor
@@ -31,60 +31,581 @@
 	});
 
 	/**
+	 * Watch all changes to Value properties, and bubble changes to parent Values instance
+	 *
+	 * @param {wp.customize.Class} instance
+	 * @param {Array} properties  The names of the Value instances to watch.
+	 */
+	bubbleChildValueChanges = function ( instance, properties ) {
+		$.each( properties, function ( i, key ) {
+			instance[ key ].bind( function ( to, from ) {
+				if ( instance.parent && to !== from ) {
+					instance.parent.trigger( 'change', instance );
+				}
+			} );
+		} );
+	};
+
+	/**
+	 * Expand a panel, section, or control and focus on the first focusable element.
+	 *
+	 * @param {Object} [params]
+	 */
+	focus = function ( params ) {
+		var container, completeCallback, focus;
+		container = this;
+		params = params || {};
+		focus = function () {
+			container.container.find( ':focusable:first' ).focus();
+			container.container[0].scrollIntoView( true );
+		};
+		if ( params.completeCallback ) {
+			completeCallback = params.completeCallback;
+			params.completeCallback = function () {
+				focus();
+				completeCallback();
+			};
+		} else {
+			params.completeCallback = focus;
+		}
+		if ( container.expand ) {
+			container.expand( params );
+		} else {
+			params.completeCallback();
+		}
+	};
+
+	/**
+	 * Base class for Panel and Section
+	 *
 	 * @constructor
 	 * @augments wp.customize.Class
 	 */
-	api.Control = api.Class.extend({
-		initialize: function( id, options ) {
-			var control = this,
-				nodes, radios, settings;
+	Container = api.Class.extend({
+		defaultActiveArguments: { duration: 'fast' },
+		defaultExpandedArguments: { duration: 'fast' },
+
+		initialize: function ( id, options ) {
+			var container = this;
+			container.id = id;
+			container.params = {};
+			$.extend( container, options || {} );
+			container.container = $( container.params.content );
+
+			container.deferred = {
+				ready: new $.Deferred()
+			};
+			container.priority = new api.Value();
+			container.active = new api.Value();
+			container.activeArgumentsQueue = [];
+			container.expanded = new api.Value();
+			container.expandedArgumentsQueue = [];
+
+			container.active.bind( function ( active ) {
+				var args = container.activeArgumentsQueue.shift();
+				args = $.extend( {}, container.defaultActiveArguments, args );
+				active = ( active && container.isContextuallyActive() );
+				container.onChangeActive( active, args );
+				// @todo trigger 'activated' and 'deactivated' events based on the expanded param?
+			});
+			container.expanded.bind( function ( expanded ) {
+				var args = container.expandedArgumentsQueue.shift();
+				args = $.extend( {}, container.defaultExpandedArguments, args );
+				container.onChangeExpanded( expanded, args );
+				// @todo trigger 'expanded' and 'collapsed' events based on the expanded param?
+			});
 
-			this.params = {};
-			$.extend( this, options || {} );
+			container.attachEvents();
 
-			this.id = id;
-			this.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' );
-			this.container = $( this.selector );
-			this.active = new api.Value( this.params.active );
+			bubbleChildValueChanges( container, [ 'priority', 'active' ] );
 
-			settings = $.map( this.params.settings, function( value ) {
-				return value;
+			container.priority.set( isNaN( container.params.priority ) ? 100 : container.params.priority );
+			container.active.set( container.params.active );
+			container.expanded.set( false ); // @todo True if deeplinking?
+		},
+
+		/**
+		 * Get the child models associated with this parent, sorting them by their priority Value.
+		 *
+		 * @param {String} parentType
+		 * @param {String} childType
+		 * @returns {Array}
+		 */
+		_children: function ( parentType, childType ) {
+			var parent = this,
+				children = [];
+			api[ childType ].each( function ( child ) {
+				if ( child[ parentType ].get() === parent.id ) {
+					children.push( child );
+				}
+			} );
+			children.sort( function ( a, b ) {
+				return a.priority() - b.priority();
+			} );
+			return children;
+		},
+
+		/**
+		 * To override by subclass, to return whether the container has active children.
+		 */
+		isContextuallyActive: function () {
+			throw new Error( 'Must override with subclass.' );
+		},
+
+		/**
+		 * Handle changes to the active state.
+		 * This does not change the active state, it merely handles the behavior
+		 * for when it does change.
+		 *
+		 * To override by subclass, update the container's UI to reflect the provided active state.
+		 *
+		 * @param {Boolean} active
+		 * @param {Object} args  merged on top of this.defaultActiveArguments
+		 */
+		onChangeActive: function ( active, args ) {
+			var duration = ( 'resolved' === api.previewer.deferred.active.state() ? args.duration : 0 );
+			if ( active ) {
+				this.container.stop( true, true ).slideDown( duration, args.completeCallback );
+			} else {
+				this.container.stop( true, true ).slideUp( duration, args.completeCallback );
+			}
+		},
+
+		/**
+		 * @params {Boolean} active
+		 * @param {Object} [params]
+		 * @returns {Boolean} false if state already applied
+		 */
+		_toggleActive: function ( active, params ) {
+			var self = this;
+			if ( ( active && this.active.get() ) || ( ! active && ! this.active.get() ) ) {
+				setTimeout( function () {
+					self.onChangeActive( self.active.get(), params || {} );
+				});
+				return false;
+			}
+			this.activeArgumentsQueue.push( params || {} );
+			this.active.set( active );
+			return true;
+		},
+
+		/**
+		 * @param {Object} [params]
+		 * @returns {Boolean} false if already active
+		 */
+		activate: function ( params ) {
+			return this._toggleActive( true, params );
+		},
+
+		/**
+		 * @param {Object} [params]
+		 * @returns {Boolean} false if already inactive
+		 */
+		deactivate: function ( params ) {
+			return this._toggleActive( false, params );
+		},
+
+		/**
+		 * To override by subclass, update the container's UI to reflect the provided active state.
+		 */
+		onChangeExpanded: function () {
+			throw new Error( 'Must override with subclass.' );
+		},
+
+		/**
+		 * @param {Boolean} expanded
+		 * @param {Object} [params]
+		 * @returns {Boolean} false if state already applied
+		 */
+		_toggleExpanded: function ( expanded, params ) {
+			var self = this;
+			if ( ( expanded && this.expanded.get() ) || ( ! expanded && ! this.expanded.get() ) ) {
+				setTimeout( function () {
+					self.onChangeExpanded( self.expanded.get(), params || {} );
+				});
+				return false;
+			}
+			this.expandedArgumentsQueue.push( params || {} );
+			this.expanded.set( expanded );
+			return true;
+		},
+
+		/**
+		 * @param {Object} [params]
+		 * @returns {Boolean} false if already expanded
+		 */
+		expand: function ( params ) {
+			return this._toggleExpanded( true, params );
+		},
+
+		/**
+		 * @param {Object} [params]
+		 * @returns {Boolean} false if already collapsed
+		 */
+		collapse: function ( params ) {
+			return this._toggleExpanded( false, params );
+		},
+
+		/**
+		 * Bring the container into view and then expand this and bring it into view
+		 * @param {Object} [params]
+		 */
+		focus: focus
+	});
+
+	/**
+	 * @constructor
+	 * @augments wp.customize.Class
+	 */
+	api.Section = Container.extend({
+
+		/**
+		 * @param {String} id
+		 * @param {Array} options
+		 */
+		initialize: function ( id, options ) {
+			var section = this;
+			Container.prototype.initialize.call( section, id, options );
+
+			section.panel = new api.Value();
+			section.panel.bind( function ( id ) {
+				$( section.container ).toggleClass( 'control-subsection', !! id );
 			});
+			section.panel.set( section.params.panel || '' );
+			bubbleChildValueChanges( section, [ 'panel' ] );
 
-			api.apply( api, settings.concat( function() {
-				var key;
+			section.embed( function () {
+				section.deferred.ready.resolve();
+			});
+		},
 
-				control.settings = {};
-				for ( key in control.params.settings ) {
-					control.settings[ key ] = api( control.params.settings[ key ] );
+		/**
+		 * Embed the container in the DOM when any parent panel is ready.
+		 *
+		 * @param {Function} readyCallback
+		 */
+		embed: function ( readyCallback ) {
+			var panel_id = this.panel.get(),
+				section = this;
+			readyCallback = readyCallback || function () {};
+
+			// Short-circuit if already embedded
+			if ( 'resolved' === section.deferred.ready.state() ) {
+				readyCallback();
+			} else if ( ! panel_id ) {
+				$( '#customize-theme-controls > ul' ).append( section.container );
+				readyCallback();
+			} else {
+				api.panel( panel_id, function ( panel ) {
+					panel.deferred.ready.done( function () {
+						panel.container.find( 'ul:first' ).append( section.container );
+						readyCallback();
+					});
+				});
+			}
+		},
+
+		/**
+		 * Add behaviors for the accordion section
+		 */
+		attachEvents: function () {
+			var section = this;
+
+			// Expand/Collapse accordion sections on click.
+			section.container.find( '.accordion-section-title' ).on( 'click keydown', function( e ) {
+				if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key
+					return;
 				}
+				e.preventDefault(); // Keep this AFTER the key filter above
 
-				control.setting = control.settings['default'] || null;
-				control.renderContent( function() {
-					// Don't call ready() until the content has rendered.
-					control.ready();
+				if ( section.expanded() ) {
+					section.collapse();
+				} else {
+					section.expand();
+				}
+			});
+		},
+
+		/**
+		 * Return whether this section has any active controls.
+		 *
+		 * @returns {boolean}
+		 */
+		isContextuallyActive: function () {
+			var section = this,
+				controls = section.controls(),
+				activeCount = 0;
+			_( controls ).each( function ( control ) {
+				if ( control.active() ) {
+					activeCount += 1;
+				}
+			} );
+			return ( activeCount !== 0 );
+		},
+
+		/**
+		 * Get the controls that are associated with this section, sorted by their priority Value.
+		 *
+		 * @returns {Array}
+		 */
+		controls: function () {
+			return this._children( 'section', 'control' );
+		},
+
+		/**
+		 * Update UI to reflect expanded state
+		 *
+		 * @param {Boolean} expanded
+		 * @param {Object} args
+		 */
+		onChangeExpanded: function ( expanded, args ) {
+			var section = this,
+				content = section.container.find( '.accordion-section-content' ),
+				expand;
+
+			if ( expanded ) {
+
+				expand = function () {
+					content.stop().slideDown( args.duration, args.completeCallback );
+					section.container.addClass( 'open' );
+				};
+
+				if ( ! args.allowMultiple ) {
+					api.section.each( function ( otherSection ) {
+						if ( otherSection !== section ) {
+							otherSection.collapse( {duration: 0} );
+						}
+					});
+				}
+
+				if ( section.panel() ) {
+					api.panel( section.panel() ).expand({
+						duration: args.duration,
+						completeCallback: expand
+					});
+				} else {
+					expand();
+				}
+
+			} else {
+				section.container.removeClass( 'open' );
+				content.slideUp( args.duration, args.completeCallback );
+			}
+		}
+	});
+
+	/**
+	 * @constructor
+	 * @augments wp.customize.Class
+	 */
+	api.Panel = Container.extend({
+		initialize: function ( id, options ) {
+			var panel = this;
+			Container.prototype.initialize.call( panel, id, options );
+
+			panel.embed( function () {
+				panel.deferred.ready.resolve();
+			});
+		},
+
+		/**
+		 * Embed the container in the DOM when any parent panel is ready.
+		 *
+		 * @param {Function} readyCallback
+		 */
+		embed: function ( readyCallback ) {
+			var panel = this;
+			readyCallback = readyCallback || function () {};
+
+			// Short-circuit if already embedded
+			if ( 'resolved' === panel.deferred.ready.state() ) {
+				readyCallback();
+			} else {
+				$( '#customize-theme-controls > ul' ).append( panel.container );
+				readyCallback();
+			}
+		},
+
+		/**
+		 *
+		 */
+		attachEvents: function () {
+			var meta, panel = this;
+
+			// Expand/Collapse accordion sections on click.
+			panel.container.find( '.accordion-section-title' ).on( 'click keydown', function( e ) {
+				if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key
+					return;
+				}
+				e.preventDefault(); // Keep this AFTER the key filter above
+
+				if ( ! panel.expanded() ) {
+					panel.expand();
+				}
+			});
+
+			meta = panel.container.find( '.panel-meta:first' );
+
+			meta.find( '> .accordion-section-title' ).on( 'click keydown', function( e ) {
+				if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key
+					return;
+				}
+				e.preventDefault(); // Keep this AFTER the key filter above
+
+				if ( meta.hasClass( 'cannot-expand' ) ) {
+					return;
+				}
+
+				var content = meta.find( '.accordion-section-content:first' );
+				if ( meta.hasClass( 'open' ) ) {
+					meta.toggleClass( 'open' );
+					content.slideUp( 150 );
+				} else {
+					content.slideDown( 150 );
+					meta.toggleClass( 'open' );
+				}
+			});
+
+		},
+
+		/**
+		 * Get the sections that are associated with this panel, sorted by their priority Value.
+		 *
+		 * @returns {Array}
+		 */
+		sections: function () {
+			return this._children( 'panel', 'section' );
+		},
+
+		/**
+		 * Return whether this panel has any active sections.
+		 *
+		 * @returns {boolean}
+		 */
+		isContextuallyActive: function () {
+			var panel = this,
+				sections = panel.sections(),
+				activeCount = 0;
+			_( sections ).each( function ( section ) {
+				if ( section.active() && section.isContextuallyActive() ) {
+					activeCount += 1;
+				}
+			} );
+			return ( activeCount !== 0 );
+		},
+
+		/**
+		 * Update UI to reflect expanded state
+		 *
+		 * @param {Boolean} expanded
+		 * @param {Object} args  merged with this.defaultExpandedArguments
+		 */
+		onChangeExpanded: function ( expanded, args ) {
+
+			// Note: there is a second argument 'args' passed
+			var position, scroll,
+				panel = this,
+				section = panel.container.closest( '.accordion-section' ),
+				overlay = section.closest( '.wp-full-overlay' ),
+				container = section.closest( '.accordion-container' ),
+				siblings = container.find( '.open' ),
+				topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ).add( '#customize-info > .accordion-section-title' ),
+				backBtn = overlay.find( '.control-panel-back' ),
+				panelTitle = section.find( '.accordion-section-title' ).first(),
+				content = section.find( '.control-panel-content' );
+
+			if ( expanded ) {
+
+				// Collapse any sibling sections/panels
+				api.section.each( function ( section ) {
+					if ( ! section.panel() ) {
+						section.collapse( { duration: 0 } );
+					}
+				});
+				api.panel.each( function ( otherPanel ) {
+					if ( panel !== otherPanel ) {
+						otherPanel.collapse( { duration: 0 } );
+					}
+				});
+
+				content.show( 0, function() {
+					position = content.offset().top;
+					scroll = container.scrollTop();
+					content.css( 'margin-top', ( 45 - position - scroll ) );
+					section.addClass( 'current-panel' );
+					overlay.addClass( 'in-sub-panel' );
+					container.scrollTop( 0 );
+					if ( args.completeCallback ) {
+						args.completeCallback();
+					}
 				} );
-			}) );
+				topPanel.attr( 'tabindex', '-1' );
+				backBtn.attr( 'tabindex', '0' );
+				backBtn.focus();
+			} else {
+				siblings.removeClass( 'open' );
+				section.removeClass( 'current-panel' );
+				overlay.removeClass( 'in-sub-panel' );
+				content.delay( 180 ).hide( 0, function() {
+					content.css( 'margin-top', 'inherit' ); // Reset
+					if ( args.completeCallback ) {
+						args.completeCallback();
+					}
+				} );
+				topPanel.attr( 'tabindex', '0' );
+				backBtn.attr( 'tabindex', '-1' );
+				panelTitle.focus();
+				container.scrollTop( 0 );
+			}
+		}
+	});
+
+	/**
+	 * @constructor
+	 * @augments wp.customize.Class
+	 */
+	api.Control = api.Class.extend({
+		defaultActiveArguments: { duration: 'fast' },
+
+		initialize: function( id, options ) {
+			var control = this,
+				nodes, radios, settings;
+
+			control.params = {};
+			$.extend( control, options || {} );
+
+			control.id = id;
+			control.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' );
+			control.container = control.params.content ? $( control.params.content ) : $( control.selector );
+
+			control.deferred = {
+				ready: new $.Deferred()
+			};
+			control.section = new api.Value();
+			control.priority = new api.Value();
+			control.active = new api.Value();
+			control.activeArgumentsQueue = [];
 
 			control.elements = [];
 
-			nodes  = this.container.find('[data-customize-setting-link]');
+			nodes  = control.container.find('[data-customize-setting-link]');
 			radios = {};
 
 			nodes.each( function() {
-				var node = $(this),
+				var node = $( this ),
 					name;
 
-				if ( node.is(':radio') ) {
-					name = node.prop('name');
-					if ( radios[ name ] )
+				if ( node.is( ':radio' ) ) {
+					name = node.prop( 'name' );
+					if ( radios[ name ] ) {
 						return;
+					}
 
 					radios[ name ] = true;
 					node = nodes.filter( '[name="' + name + '"]' );
 				}
 
-				api( node.data('customizeSettingLink'), function( setting ) {
+				api( node.data( 'customizeSettingLink' ), function( setting ) {
 					var element = new api.Element( node );
 					control.elements.push( element );
 					element.sync( setting );
@@ -93,9 +614,67 @@
 			});
 
 			control.active.bind( function ( active ) {
-				control.toggle( active );
+				var args = control.activeArgumentsQueue.shift();
+				args = $.extend( {}, control.defaultActiveArguments, args );
+				control.onChangeActive( active, args );
+			} );
+
+			control.section.set( control.params.section );
+			control.priority.set( isNaN( control.params.priority ) ? 10 : control.params.priority );
+			control.active.set( control.params.active );
+
+			bubbleChildValueChanges( control, [ 'section', 'priority', 'active' ] );
+
+			// Associate this control with its settings when they are created
+			settings = $.map( control.params.settings, function( value ) {
+				return value;
+			});
+			api.apply( api, settings.concat( function () {
+				var key;
+
+				control.settings = {};
+				for ( key in control.params.settings ) {
+					control.settings[ key ] = api( control.params.settings[ key ] );
+				}
+
+				control.setting = control.settings['default'] || null;
+				control.embed( function () {
+					control.renderContent( function () {
+						// Don't call ready() until the content has rendered.
+						control.ready();
+						control.deferred.ready.resolve();
+					});
+				});
+			}) );
+		},
+
+		/**
+		 * @param {Function} [readyCallback] Callback to fire when the embedding is done.
+		 */
+		embed: function ( readyCallback ) {
+			var section_id,
+				control = this;
+			readyCallback = readyCallback || function () {};
+
+			// Short-circuit if already embedded
+			if ( 'resolved' === control.deferred.ready.state() ) {
+				readyCallback();
+				return;
+			}
+
+			section_id = control.section.get();
+			if ( ! section_id ) {
+				throw new Error( 'A control must have an associated section.' );
+				// @todo Allow this to wait until control.section gets a value. Will require wp.customize.Value.once()
+			}
+
+			// Defer until the associated section is available
+			api.section( section_id, function ( section ) {
+				section.embed( function () {
+					section.container.find( 'ul:first' ).append( control.container );
+					readyCallback();
+				} );
 			} );
-			control.toggle( control.active() );
 		},
 
 		/**
@@ -104,20 +683,49 @@
 		ready: function() {},
 
 		/**
-		 * Callback for change to the control's active state.
-		 *
-		 * Override function for custom behavior for the control being active/inactive.
+		 * Bring the containing section and panel into view and then this control into view, focusing on the first input
+		 */
+		focus: focus,
+
+		/**
+		 * Update UI in response to a change in the control's active state.
+		 * This does not change the active state, it merely handles the behavior
+		 * for when it does change.
 		 *
 		 * @param {Boolean} active
+		 * @param {Object} args  merged on top of this.defaultActiveArguments
 		 */
-		toggle: function ( active ) {
+		onChangeActive: function ( active, args ) {
 			if ( active ) {
-				this.container.slideDown();
+				this.container.slideDown( args.duration );
 			} else {
-				this.container.slideUp();
+				this.container.slideUp( args.duration );
 			}
 		},
 
+		/**
+		 * @deprecated alias of onChangeActive
+		 */
+		toggle: function ( active ) {
+			return this.onChangeActive( active, this.defaultActiveArguments );
+		},
+
+		/**
+		 * Shorthand way to enable the active state.
+		 *
+		 * @param {Object} [params]
+		 * @returns {Boolean} false if already active
+		 */
+		activate: Container.prototype.activate,
+
+		/**
+		 * Shorthand way to disable the active state.
+		 *
+		 * @param {Object} [params]
+		 * @returns {Boolean} false if already inactive
+		 */
+		deactivate: Container.prototype.deactivate,
+
 		dropdownInit: function() {
 			var control      = this,
 				statuses     = this.container.find('.dropdown-status'),
@@ -158,18 +766,21 @@
 		 * Render the control from its JS template, if it exists.
 		 *
 		 * The control's container must alreasy exist in the DOM.
+		 *
+		 * @param {Function} [callback]
 		 */
 		renderContent: function( callback ) {
 			var template,
-			    selector = 'customize-control-' + this.params.type + '-content',
-			    callback = callback || function(){};
+				selector = 'customize-control-' + this.params.type + '-content';
 			if ( 0 !== $( '#tmpl-' + selector ).length ) {
 				template = wp.template( selector );
 				if ( template && this.container ) {
 					this.container.append( template( this.params ) );
 				}
 			}
-			callback();
+			if ( callback ) {
+				callback();
+			}
 		}
 	});
 
@@ -596,6 +1207,8 @@
 
 	// Create the collection of Control objects.
 	api.control = new api.Values({ defaultConstructor: api.Control });
+	api.section = new api.Values({ defaultConstructor: api.Section });
+	api.panel = new api.Values({ defaultConstructor: api.Panel });
 
 	/**
 	 * @constructor
@@ -631,29 +1244,42 @@
 				loaded = false,
 				ready  = false;
 
-			if ( this._ready )
+			if ( this._ready ) {
 				this.unbind( 'ready', this._ready );
+			}
 
 			this._ready = function() {
 				ready = true;
 
-				if ( loaded )
+				if ( loaded ) {
 					deferred.resolveWith( self );
+				}
 			};
 
 			this.bind( 'ready', this._ready );
 
 			this.bind( 'ready', function ( data ) {
-				if ( ! data || ! data.activeControls ) {
+				if ( ! data ) {
 					return;
 				}
 
-				$.each( data.activeControls, function ( id, active ) {
-					var control = api.control( id );
-					if ( control ) {
-						control.active( active );
+				var constructs = {
+					panel: data.activePanels,
+					section: data.activeSections,
+					control: data.activeControls
+				};
+
+				$.each( constructs, function ( type, activeConstructs ) {
+					if ( activeConstructs ) {
+						$.each( activeConstructs, function ( id, active ) {
+							var construct = api[ type ]( id );
+							if ( construct ) {
+								construct.active( active );
+							}
+						} );
 					}
 				} );
+
 			} );
 
 			this.request = $.ajax( this.previewUrl(), {
@@ -675,7 +1301,7 @@
 
 				// Check if the location response header differs from the current URL.
 				// If so, the request was redirected; try loading the requested page.
-				if ( location && location != self.previewUrl() ) {
+				if ( location && location !== self.previewUrl() ) {
 					deferred.rejectWith( self, [ 'redirect', location ] );
 					return;
 				}
@@ -802,6 +1428,9 @@
 				rscheme = /^https?/;
 
 			$.extend( this, options || {} );
+			this.deferred = {
+				active: $.Deferred()
+			};
 
 			/*
 			 * Wrap this.refresh to prevent it from hammering the servers:
@@ -933,6 +1562,7 @@
 					self.targetWindow( this.targetWindow() );
 					self.channel( this.channel() );
 
+					self.deferred.active.resolve();
 					self.send( 'active' );
 				});
 
@@ -1000,6 +1630,8 @@
 		image:  api.ImageControl,
 		header: api.HeaderControl
 	};
+	api.panelConstructor = {};
+	api.sectionConstructor = {};
 
 	$( function() {
 		api.settings = window._wpCustomizeSettings;
@@ -1030,6 +1662,29 @@
 			}
 		});
 
+		// Expand/Collapse the main customizer customize info
+		$( '#customize-info' ).find( '> .accordion-section-title' ).on( 'click keydown', function( e ) {
+			if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key
+				return;
+			}
+			e.preventDefault(); // Keep this AFTER the key filter above
+
+			var section = $( this ).parent(),
+				content = section.find( '.accordion-section-content:first' );
+
+			if ( section.hasClass( 'cannot-expand' ) ) {
+				return;
+			}
+
+			if ( section.hasClass( 'open' ) ) {
+				section.toggleClass( 'open' );
+				content.slideUp( 150 );
+			} else {
+				content.slideDown( 150 );
+				section.toggleClass( 'open' );
+			}
+		});
+
 		// Initialize Previewer
 		api.previewer = new api.Previewer({
 			container:   '#customize-preview',
@@ -1123,6 +1778,7 @@
 			$.extend( this.nonce, nonce );
 		});
 
+		// Create Settings
 		$.each( api.settings.settings, function( id, data ) {
 			api.create( id, id, data.value, {
 				transport: data.transport,
@@ -1130,16 +1786,120 @@
 			} );
 		});
 
+		// Create Panels
+		$.each( api.settings.panels, function ( id, data ) {
+			var constructor = api.panelConstructor[ data.type ] || api.Panel,
+				panel;
+
+			panel = new constructor( id, {
+				params: data
+			} );
+			api.panel.add( id, panel );
+		});
+
+		// Create Sections
+		$.each( api.settings.sections, function ( id, data ) {
+			var constructor = api.sectionConstructor[ data.type ] || api.Section,
+				section;
+
+			section = new constructor( id, {
+				params: data
+			} );
+			api.section.add( id, section );
+		});
+
+		// Create Controls
 		$.each( api.settings.controls, function( id, data ) {
 			var constructor = api.controlConstructor[ data.type ] || api.Control,
 				control;
 
-			control = api.control.add( id, new constructor( id, {
+			control = new constructor( id, {
 				params: data,
 				previewer: api.previewer
-			} ) );
+			} );
+			api.control.add( id, control );
+		});
+
+		// Focus the autofocused element
+		_.each( [ 'panel', 'section', 'control' ], function ( type ) {
+			var instance, id = api.settings.autofocus[ type ];
+			if ( id && api[ type ]( id ) ) {
+				instance = api[ type ]( id );
+				// Wait until the element is embedded in the DOM
+				instance.deferred.ready.done( function () {
+					// Wait until the preview has activated and so active panels, sections, controls have been set
+					api.previewer.deferred.active.done( function () {
+						instance.focus();
+					});
+				});
+			}
 		});
 
+		/**
+		 * Sort panels, sections, controls by priorities. Hide empty sections and panels.
+		 */
+		api.reflowPaneContents = _.bind( function () {
+
+			var appendContainer, activeElement, rootNodes = [];
+
+			if ( document.activeElement ) {
+				activeElement = $( document.activeElement );
+			}
+
+			api.panel.each( function ( panel ) {
+				var sections = panel.sections();
+				rootNodes.push( panel );
+				appendContainer = panel.container.find( 'ul:first' );
+				// @todo Skip doing any DOM manipulation if the ordering is already correct
+				_( sections ).each( function ( section ) {
+					appendContainer.append( section.container );
+				} );
+			} );
+
+			api.section.each( function ( section ) {
+				var controls = section.controls();
+				if ( ! section.panel() ) {
+					rootNodes.push( section );
+				}
+				appendContainer = section.container.find( 'ul:first' );
+				// @todo Skip doing any DOM manipulation if the ordering is already correct
+				_( controls ).each( function ( control ) {
+					appendContainer.append( control.container );
+				} );
+			} );
+
+			// Sort the root elements
+			rootNodes.sort( function ( a, b ) {
+				return a.priority() - b.priority();
+			} );
+			appendContainer = $( '#customize-theme-controls > ul' );
+			// @todo Skip doing any DOM manipulation if the ordering is already correct
+			_( rootNodes ).each( function ( rootNode ) {
+				appendContainer.append( rootNode.container );
+			} );
+
+			// Now re-trigger the active Value callbacks to that the panels and sections can decide whether they can be rendered
+			api.panel.each( function ( panel ) {
+				var value = panel.active();
+				panel.active.callbacks.fireWith( panel.active, [ value, value ] );
+			} );
+			api.section.each( function ( section ) {
+				var value = section.active();
+				section.active.callbacks.fireWith( section.active, [ value, value ] );
+			} );
+
+			if ( activeElement ) {
+				activeElement.focus();
+			}
+		}, api );
+		api.reflowPaneContents = _.debounce( api.reflowPaneContents, 100 );
+		$( [ api.panel, api.section, api.control ] ).each( function ( i, values ) {
+			values.bind( 'add', api.reflowPaneContents );
+			values.bind( 'change', api.reflowPaneContents );
+			values.bind( 'remove', api.reflowPaneContents );
+		} );
+		api.bind( 'ready', api.reflowPaneContents );
+
 		// Check if preview url is valid and load the preview frame.
 		if ( api.previewer.previewUrl() ) {
 			api.previewer.refresh();
@@ -1204,6 +1964,18 @@
 			event.preventDefault();
 		});
 
+		// Go back to the top-level Customizer accordion.
+		$( '#customize-header-actions' ).on( 'click keydown', '.control-panel-back', function( e ) {
+			if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key
+				return;
+			}
+
+			e.preventDefault(); // Keep this AFTER the key filter above
+			api.panel.each( function ( panel ) {
+				panel.collapse();
+			});
+		});
+
 		closeBtn.keydown( function( event ) {
 			if ( 9 === event.which ) // tab
 				return;
diff --git src/wp-admin/js/customize-widgets.js src/wp-admin/js/customize-widgets.js
index 6be9a08..b9d8bd1 100644
--- src/wp-admin/js/customize-widgets.js
+++ src/wp-admin/js/customize-widgets.js
@@ -404,6 +404,23 @@
 	 * @augments wp.customize.Control
 	 */
 	api.Widgets.WidgetControl = api.Control.extend({
+		defaultExpandedArguments: {
+			duration: 'fast'
+		},
+
+		initialize: function ( id, options ) {
+			var control = this;
+			api.Control.prototype.initialize.call( control, id, options );
+			control.expanded = new api.Value();
+			control.expandedArgumentsQueue = [];
+			control.expanded.bind( function ( expanded ) {
+				var args = control.expandedArgumentsQueue.shift();
+				args = $.extend( {}, control.defaultExpandedArguments, args );
+				control.onChangeExpanded( expanded, args );
+			});
+			control.expanded.set( false );
+		},
+
 		/**
 		 * Set up the control
 		 */
@@ -529,13 +546,13 @@
 				if ( sidebarWidgetsControl.isReordering ) {
 					return;
 				}
-				self.toggleForm();
+				self.expanded( ! self.expanded() );
 			} );
 
 			$closeBtn = this.container.find( '.widget-control-close' );
 			$closeBtn.on( 'click', function( e ) {
 				e.preventDefault();
-				self.collapseForm();
+				self.collapse();
 				self.container.find( '.widget-top .widget-action:first' ).focus(); // keyboard accessibility
 			} );
 		},
@@ -778,7 +795,8 @@
 		 *
 		 * @param {Boolean} active
 		 */
-		toggle: function ( active ) {
+		onChangeActive: function ( active ) {
+			// Note: there is a second 'args' parameter being passed, merged on top of this.defaultActiveArguments
 			this.container.toggleClass( 'widget-rendered', active );
 		},
 
@@ -1101,51 +1119,80 @@
 		 * Expand the accordion section containing a control
 		 */
 		expandControlSection: function() {
-			var $section = this.container.closest( '.accordion-section' );
-
-			if ( ! $section.hasClass( 'open' ) ) {
-				$section.find( '.accordion-section-title:first' ).trigger( 'click' );
-			}
+			api.section( this.section() ).expand();
 		},
 
 		/**
+		 * @param {Boolean} expanded
+		 * @param {Object} [params]
+		 * @returns {Boolean} false if state already applied
+		 */
+		_toggleExpanded: api.Section.prototype._toggleExpanded,
+
+		/**
+		 * @param {Object} [params]
+		 * @returns {Boolean} false if already expanded
+		 */
+		expand: api.Section.prototype.expand,
+
+		/**
 		 * Expand the widget form control
+		 *
+		 * @deprecated alias of expand()
 		 */
 		expandForm: function() {
-			this.toggleForm( true );
+			this.expand();
 		},
 
 		/**
+		 * @param {Object} [params]
+		 * @returns {Boolean} false if already collapsed
+		 */
+		collapse: api.Section.prototype.collapse,
+
+		/**
 		 * Collapse the widget form control
+		 *
+		 * @deprecated alias of expand()
 		 */
 		collapseForm: function() {
-			this.toggleForm( false );
+			this.collapse();
 		},
 
 		/**
 		 * Expand or collapse the widget control
 		 *
+		 * @deprecated this is poor naming, and it is better to directly set control.expanded( showOrHide )
+		 *
 		 * @param {boolean|undefined} [showOrHide] If not supplied, will be inverse of current visibility
 		 */
 		toggleForm: function( showOrHide ) {
-			var self = this, $widget, $inside, complete;
+			if ( typeof showOrHide === 'undefined' ) {
+				showOrHide = ! this.expanded();
+			}
+			this.expanded( showOrHide );
+		},
 
+		/**
+		 * Respond to change in the expanded state.
+		 *
+		 * @param {Boolean} expanded
+		 * @param {Object} args  merged on top of this.defaultActiveArguments
+		 */
+		onChangeExpanded: function ( expanded, args ) {
+
+			var self = this, $widget, $inside, complete, prevComplete;
 			$widget = this.container.find( 'div.widget:first' );
 			$inside = $widget.find( '.widget-inside:first' );
-			if ( typeof showOrHide === 'undefined' ) {
-				showOrHide = ! $inside.is( ':visible' );
-			}
 
-			// Already expanded or collapsed, so noop
-			if ( $inside.is( ':visible' ) === showOrHide ) {
-				return;
-			}
+			if ( expanded ) {
+
+				self.expandControlSection();
 
-			if ( showOrHide ) {
 				// Close all other widget controls before expanding this one
 				api.control.each( function( otherControl ) {
 					if ( self.params.type === otherControl.params.type && self !== otherControl ) {
-						otherControl.collapseForm();
+						otherControl.collapse();
 					}
 				} );
 
@@ -1154,29 +1201,44 @@
 					self.container.addClass( 'expanded' );
 					self.container.trigger( 'expanded' );
 				};
+				if ( args.completeCallback ) {
+					prevComplete = complete;
+					complete = function () {
+						prevComplete();
+						args.completeCallback();
+					};
+				}
 
 				if ( self.params.is_wide ) {
-					$inside.fadeIn( 'fast', complete );
+					$inside.fadeIn( args.duration, complete );
 				} else {
-					$inside.slideDown( 'fast', complete );
+					$inside.slideDown( args.duration, complete );
 				}
 
 				self.container.trigger( 'expand' );
 				self.container.addClass( 'expanding' );
 			} else {
+
 				complete = function() {
 					self.container.removeClass( 'collapsing' );
 					self.container.removeClass( 'expanded' );
 					self.container.trigger( 'collapsed' );
 				};
+				if ( args.completeCallback ) {
+					prevComplete = complete;
+					complete = function () {
+						prevComplete();
+						args.completeCallback();
+					};
+				}
 
 				self.container.trigger( 'collapse' );
 				self.container.addClass( 'collapsing' );
 
 				if ( self.params.is_wide ) {
-					$inside.fadeOut( 'fast', complete );
+					$inside.fadeOut( args.duration, complete );
 				} else {
-					$inside.slideUp( 'fast', function() {
+					$inside.slideUp( args.duration, function() {
 						$widget.css( { width:'', margin:'' } );
 						complete();
 					} );
@@ -1185,16 +1247,6 @@
 		},
 
 		/**
-		 * Expand the containing sidebar section, expand the form, and focus on
-		 * the first input in the control
-		 */
-		focus: function() {
-			this.expandControlSection();
-			this.expandForm();
-			this.container.find( '.widget-content :focusable:first' ).focus();
-		},
-
-		/**
 		 * Get the position (index) of the widget in the containing sidebar
 		 *
 		 * @returns {Number}
@@ -1304,6 +1356,7 @@
 	 * @augments wp.customize.Control
 	 */
 	api.Widgets.SidebarControl = api.Control.extend({
+
 		/**
 		 * Set up the control
 		 */
@@ -1325,7 +1378,7 @@
 				registeredSidebar = api.Widgets.registeredSidebars.get( this.params.sidebar_id );
 
 			this.setting.bind( function( newWidgetIds, oldWidgetIds ) {
-				var widgetFormControls, $sidebarWidgetsAddControl, finalControlContainers, removedWidgetIds;
+				var widgetFormControls, removedWidgetIds, priority;
 
 				removedWidgetIds = _( oldWidgetIds ).difference( newWidgetIds );
 
@@ -1350,21 +1403,16 @@
 				widgetFormControls.sort( function( a, b ) {
 					var aIndex = _.indexOf( newWidgetIds, a.params.widget_id ),
 						bIndex = _.indexOf( newWidgetIds, b.params.widget_id );
+					return aIndex - bIndex;
+				});
 
-					if ( aIndex === bIndex ) {
-						return 0;
-					}
-
-					return aIndex < bIndex ? -1 : 1;
-				} );
-
-				// Append the controls to put them in the right order
-				finalControlContainers = _( widgetFormControls ).map( function( widgetFormControls ) {
-					return widgetFormControls.container[0];
-				} );
-
-				$sidebarWidgetsAddControl = self.$sectionContent.find( '.customize-control-sidebar_widgets' );
-				$sidebarWidgetsAddControl.before( finalControlContainers );
+				priority = 0;
+				_( widgetFormControls ).each( function ( control ) {
+					control.priority( priority );
+					control.section( self.section() );
+					priority += 1;
+				});
+				self.priority( priority ); // Make sure sidebar control remains at end
 
 				// Re-sort widget form controls (including widgets form other sidebars newly moved here)
 				self._applyCardinalOrderClassNames();
@@ -1434,36 +1482,9 @@
 			// Update the model with whether or not the sidebar is rendered
 			self.active.bind( function ( active ) {
 				registeredSidebar.set( 'is_rendered', active );
+				api.section( self.section.get() ).active( active );
 			} );
-		},
-
-		/**
-		 * Show the sidebar section when it becomes visible.
-		 *
-		 * Overrides api.Control.toggle()
-		 *
-		 * @param {Boolean} active
-		 */
-		toggle: function ( active ) {
-			var $section, sectionSelector;
-
-			sectionSelector = '#accordion-section-sidebar-widgets-' + this.params.sidebar_id;
-			$section = $( sectionSelector );
-
-			if ( active ) {
-				$section.stop().slideDown( function() {
-					$( this ).css( 'height', 'auto' ); // so that the .accordion-section-content won't overflow
-				} );
-
-			} else {
-				// Make sure that hidden sections get closed first
-				if ( $section.hasClass( 'open' ) ) {
-					// it would be nice if accordionSwitch() in accordion.js was public
-					$section.find( '.accordion-section-title' ).trigger( 'click' );
-				}
-
-				$section.stop().slideUp();
-			}
+			api.section( self.section.get() ).active( self.active() );
 		},
 
 		/**
@@ -1500,12 +1521,18 @@
 			this.$controlSection.find( '.accordion-section-title' ).droppable({
 				accept: '.customize-control-widget_form',
 				over: function() {
-					if ( ! self.$controlSection.hasClass( 'open' ) ) {
-						self.$controlSection.addClass( 'open' );
-						self.$sectionContent.toggle( false ).slideToggle( 150, function() {
-							self.$sectionContent.sortable( 'refreshPositions' );
-						} );
-					}
+					var section = api.section( self.section.get() );
+					section.expand({
+						allowMultiple: true, // Prevent the section being dragged from to be collapsed
+						completeCallback: function () {
+							// @todo It is not clear when refreshPositions should be called on which sections, or if it is even needed
+							api.section.each( function ( otherSection ) {
+								if ( otherSection.container.find( '.customize-control-sidebar_widgets' ).length ) {
+									otherSection.container.find( '.accordion-section-content:first' ).sortable( 'refreshPositions' );
+								}
+							} );
+						}
+					});
 				}
 			});
 
@@ -1548,16 +1575,30 @@
 		 * Add classes to the widget_form controls to assist with styling
 		 */
 		_applyCardinalOrderClassNames: function() {
-			this.$sectionContent.find( '.customize-control-widget_form' )
-				.removeClass( 'first-widget' )
-				.removeClass( 'last-widget' )
-				.find( '.move-widget-down, .move-widget-up' ).prop( 'tabIndex', 0 );
+			var widgetControls = [];
+			_.each( this.setting(), function ( widgetId ) {
+				var widgetControl = api.Widgets.getWidgetFormControlForWidget( widgetId );
+				if ( widgetControl ) {
+					widgetControls.push( widgetControl );
+				}
+			});
 
-			this.$sectionContent.find( '.customize-control-widget_form:first' )
+			if ( ! widgetControls.length ) {
+				return;
+			}
+
+			$( widgetControls ).each( function () {
+				$( this.container )
+					.removeClass( 'first-widget' )
+					.removeClass( 'last-widget' )
+					.find( '.move-widget-down, .move-widget-up' ).prop( 'tabIndex', 0 );
+			});
+
+			_.first( widgetControls ).container
 				.addClass( 'first-widget' )
 				.find( '.move-widget-up' ).prop( 'tabIndex', -1 );
 
-			this.$sectionContent.find( '.customize-control-widget_form:last' )
+			_.last( widgetControls ).container
 				.addClass( 'last-widget' )
 				.find( '.move-widget-down' ).prop( 'tabIndex', -1 );
 		},
@@ -1571,6 +1612,8 @@
 		 * Enable/disable the reordering UI
 		 *
 		 * @param {Boolean} showOrHide to enable/disable reordering
+		 *
+		 * @todo We should have a reordering state instead and rename this to onChangeReordering
 		 */
 		toggleReordering: function( showOrHide ) {
 			showOrHide = Boolean( showOrHide );
@@ -1584,7 +1627,7 @@
 
 			if ( showOrHide ) {
 				_( this.getWidgetFormControls() ).each( function( formControl ) {
-					formControl.collapseForm();
+					formControl.collapse();
 				} );
 
 				this.$sectionContent.find( '.first-widget .move-widget' ).focus();
@@ -1651,6 +1694,7 @@
 
 			$widget = $( controlHtml );
 
+			// @todo need to pass this in as the control's 'content' property
 			$control = $( '<li/>' )
 				.addClass( 'customize-control' )
 				.addClass( 'customize-control-' + controlType )
@@ -1674,6 +1718,7 @@
 			}
 			$control.attr( 'id', 'customize-control-' + settingId.replace( /\]/g, '' ).replace( /\[/g, '-' ) );
 
+			// @todo Eliminate this
 			this.container.after( $control );
 
 			// Only create setting if it doesn't already exist (if we're adding a pre-existing inactive widget)
@@ -1733,7 +1778,7 @@
 
 			$control.slideDown( function() {
 				if ( isExistingWidget ) {
-					widgetFormControl.expandForm();
+					widgetFormControl.expand();
 					widgetFormControl.updateWidget( {
 						instance: widgetFormControl.setting(),
 						complete: function( error ) {
diff --git src/wp-includes/class-wp-customize-control.php src/wp-includes/class-wp-customize-control.php
index cbb7cdd..8f24601 100644
--- src/wp-includes/class-wp-customize-control.php
+++ src/wp-includes/class-wp-customize-control.php
@@ -74,6 +74,7 @@ class WP_Customize_Control {
 	public $input_attrs = array();
 
 	/**
+	 * @deprecated It is better to just call the json() method
 	 * @access public
 	 * @var array
 	 */
@@ -218,9 +219,24 @@ class WP_Customize_Control {
 		}
 
 		$this->json['type']        = $this->type;
+		$this->json['priority']    = $this->priority;
+		$this->json['active']      = $this->active();
+		$this->json['section']     = $this->section;
+		$this->json['content']     = $this->get_content();
 		$this->json['label']       = $this->label;
 		$this->json['description'] = $this->description;
-		$this->json['active']      = $this->active();
+	}
+
+	/**
+	 * Get the data to export to the client via JSON.
+	 *
+	 * @since 4.1.0
+	 *
+	 * @return array
+	 */
+	public function json() {
+		$this->to_json();
+		return $this->json;
 	}
 
 	/**
@@ -244,6 +260,21 @@ class WP_Customize_Control {
 	}
 
 	/**
+	 * Get the control's content for insertion into the Customizer pane.
+	 *
+	 * @since 4.1.0
+	 *
+	 * @return string
+	 */
+	public final function get_content() {
+		ob_start();
+		$this->maybe_render();
+		$template = trim( ob_get_contents() );
+		ob_end_clean();
+		return $template;
+	}
+
+	/**
 	 * Check capabilities and render the control.
 	 *
 	 * @since 3.4.0
@@ -1071,6 +1102,7 @@ class WP_Customize_Header_Image_Control extends WP_Customize_Image_Control {
 /**
  * Widget Area Customize Control Class
  *
+ * @since 3.9.0
  */
 class WP_Widget_Area_Customize_Control extends WP_Customize_Control {
 	public $type = 'sidebar_widgets';
@@ -1112,6 +1144,8 @@ class WP_Widget_Area_Customize_Control extends WP_Customize_Control {
 
 /**
  * Widget Form Customize Control Class
+ *
+ * @since 3.9.0
  */
 class WP_Widget_Form_Customize_Control extends WP_Customize_Control {
 	public $type = 'widget_form';
diff --git src/wp-includes/class-wp-customize-manager.php src/wp-includes/class-wp-customize-manager.php
index 17f0bad..c7c3b4d 100644
--- src/wp-includes/class-wp-customize-manager.php
+++ src/wp-includes/class-wp-customize-manager.php
@@ -498,6 +498,8 @@ final class WP_Customize_Manager {
 		$settings = array(
 			'values'  => array(),
 			'channel' => wp_unslash( $_POST['customize_messenger_channel'] ),
+			'activePanels' => array(),
+			'activeSections' => array(),
 			'activeControls' => array(),
 		);
 
@@ -511,6 +513,12 @@ final class WP_Customize_Manager {
 		foreach ( $this->settings as $id => $setting ) {
 			$settings['values'][ $id ] = $setting->js_value();
 		}
+		foreach ( $this->panels as $id => $panel ) {
+			$settings['activePanels'][ $id ] = $panel->active();
+		}
+		foreach ( $this->sections as $id => $section ) {
+			$settings['activeSections'][ $id ] = $section->active();
+		}
 		foreach ( $this->controls as $id => $control ) {
 			$settings['activeControls'][ $id ] = $control->active();
 		}
@@ -911,11 +919,11 @@ final class WP_Customize_Manager {
 
 			if ( ! $section->panel ) {
 				// Top-level section.
-				$sections[] = $section;
+				$sections[ $section->id ] = $section;
 			} else {
 				// This section belongs to a panel.
 				if ( isset( $this->panels [ $section->panel ] ) ) {
-					$this->panels[ $section->panel ]->sections[] = $section;
+					$this->panels[ $section->panel ]->sections[ $section->id ] = $section;
 				}
 			}
 		}
@@ -932,8 +940,8 @@ final class WP_Customize_Manager {
 				continue;
 			}
 
-			usort( $panel->sections, array( $this, '_cmp_priority' ) );
-			$panels[] = $panel;
+			uasort( $panel->sections, array( $this, '_cmp_priority' ) );
+			$panels[ $panel->id ] = $panel;
 		}
 		$this->panels = $panels;
 
diff --git src/wp-includes/class-wp-customize-panel.php src/wp-includes/class-wp-customize-panel.php
index f289cb7..201c4b9 100644
--- src/wp-includes/class-wp-customize-panel.php
+++ src/wp-includes/class-wp-customize-panel.php
@@ -83,6 +83,28 @@ class WP_Customize_Panel {
 	public $sections;
 
 	/**
+	 * @since 4.1.0
+	 * @access public
+	 * @var string
+	 */
+	public $type;
+
+	/**
+	 * Callback.
+	 *
+	 * @since 4.1.0
+	 * @access public
+	 *
+	 * @see WP_Customize_Section::active()
+	 *
+	 * @var callable Callback is called with one argument, the instance of
+	 *               WP_Customize_Section, and returns bool to indicate whether
+	 *               the section is active (such as it relates to the URL
+	 *               currently being previewed).
+	 */
+	public $active_callback = '';
+
+	/**
 	 * Constructor.
 	 *
 	 * Any supplied $args override class property defaults.
@@ -103,6 +125,9 @@ class WP_Customize_Panel {
 
 		$this->manager = $manager;
 		$this->id = $id;
+		if ( empty( $this->active_callback ) ) {
+			$this->active_callback = array( $this, 'active_callback' );
+		}
 
 		$this->sections = array(); // Users cannot customize the $sections array.
 
@@ -110,6 +135,60 @@ class WP_Customize_Panel {
 	}
 
 	/**
+	 * Check whether panel is active to current Customizer preview.
+	 *
+	 * @since 4.1.0
+	 * @access public
+	 *
+	 * @return bool Whether the panel is active to the current preview.
+	 */
+	public final function active() {
+		$panel = $this;
+		$active = call_user_func( $this->active_callback, $this );
+
+		/**
+		 * Filter response of WP_Customize_Panel::active().
+		 *
+		 * @since 4.1.0
+		 *
+		 * @param bool                 $active  Whether the Customizer panel is active.
+		 * @param WP_Customize_Panel $panel WP_Customize_Panel instance.
+		 */
+		$active = apply_filters( 'customize_panel_active', $active, $panel );
+
+		return $active;
+	}
+
+	/**
+	 * Default callback used when invoking WP_Customize_Panel::active().
+	 *
+	 * Subclasses can override this with their specific logic, or they may
+	 * provide an 'active_callback' argument to the constructor.
+	 *
+	 * @since 4.1.0
+	 * @access public
+	 *
+	 * @return bool Always true.
+	 */
+	public function active_callback() {
+		return true;
+	}
+
+	/**
+	 * Gather the parameters passed to client JavaScript via JSON.
+	 *
+	 * @since 4.1.0
+	 *
+	 * @return array The array to be exported to the client as JSON
+	 */
+	public function json() {
+		$array = wp_array_slice_assoc( (array) $this, array( 'title', 'description', 'priority', 'type' ) );
+		$array['content'] = $this->get_content();
+		$array['active'] = $this->active();
+		return $array;
+	}
+
+	/**
 	 * Checks required user capabilities and whether the theme has the
 	 * feature support required by the panel.
 	 *
@@ -130,6 +209,21 @@ class WP_Customize_Panel {
 	}
 
 	/**
+	 * Get the panel's content template for insertion into the Customizer pane.
+	 *
+	 * @since 4.1.0
+	 *
+	 * @return string
+	 */
+	public final function get_content() {
+		ob_start();
+		$this->maybe_render();
+		$template = trim( ob_get_contents() );
+		ob_end_clean();
+		return $template;
+	}
+
+	/**
 	 * Check capabilities and render the panel.
 	 *
 	 * @since 4.0.0
@@ -189,7 +283,7 @@ class WP_Customize_Panel {
 	 */
 	protected function render_content() {
 		?>
-		<li class="accordion-section control-section<?php if ( empty( $this->description ) ) echo ' cannot-expand'; ?>">
+		<li class="panel-meta accordion-section control-section<?php if ( empty( $this->description ) ) { echo ' cannot-expand'; } ?>">
 			<div class="accordion-section-title" tabindex="0">
 				<span class="preview-notice"><?php
 					/* translators: %s is the site/panel title in the Customizer */
@@ -203,8 +297,5 @@ class WP_Customize_Panel {
 			<?php endif; ?>
 		</li>
 		<?php
-		foreach ( $this->sections as $section ) {
-			$section->maybe_render();
-		}
 	}
 }
diff --git src/wp-includes/class-wp-customize-section.php src/wp-includes/class-wp-customize-section.php
index d740ddb..3553285 100644
--- src/wp-includes/class-wp-customize-section.php
+++ src/wp-includes/class-wp-customize-section.php
@@ -92,6 +92,28 @@ class WP_Customize_Section {
 	public $controls;
 
 	/**
+	 * @since 4.1.0
+	 * @access public
+	 * @var string
+	 */
+	public $type;
+
+	/**
+	 * Callback.
+	 *
+	 * @since 4.1.0
+	 * @access public
+	 *
+	 * @see WP_Customize_Section::active()
+	 *
+	 * @var callable Callback is called with one argument, the instance of
+	 *               WP_Customize_Section, and returns bool to indicate whether
+	 *               the section is active (such as it relates to the URL
+	 *               currently being previewed).
+	 */
+	public $active_callback = '';
+
+	/**
 	 * Constructor.
 	 *
 	 * Any supplied $args override class property defaults.
@@ -105,12 +127,16 @@ class WP_Customize_Section {
 	public function __construct( $manager, $id, $args = array() ) {
 		$keys = array_keys( get_object_vars( $this ) );
 		foreach ( $keys as $key ) {
-			if ( isset( $args[ $key ] ) )
+			if ( isset( $args[ $key ] ) ) {
 				$this->$key = $args[ $key ];
+			}
 		}
 
 		$this->manager = $manager;
 		$this->id = $id;
+		if ( empty( $this->active_callback ) ) {
+			$this->active_callback = array( $this, 'active_callback' );
+		}
 
 		$this->controls = array(); // Users cannot customize the $controls array.
 
@@ -118,6 +144,60 @@ class WP_Customize_Section {
 	}
 
 	/**
+	 * Check whether section is active to current Customizer preview.
+	 *
+	 * @since 4.1.0
+	 * @access public
+	 *
+	 * @return bool Whether the section is active to the current preview.
+	 */
+	public final function active() {
+		$section = $this;
+		$active = call_user_func( $this->active_callback, $this );
+
+		/**
+		 * Filter response of WP_Customize_Section::active().
+		 *
+		 * @since 4.1.0
+		 *
+		 * @param bool                 $active  Whether the Customizer section is active.
+		 * @param WP_Customize_Section $section WP_Customize_Section instance.
+		 */
+		$active = apply_filters( 'customize_section_active', $active, $section );
+
+		return $active;
+	}
+
+	/**
+	 * Default callback used when invoking WP_Customize_Section::active().
+	 *
+	 * Subclasses can override this with their specific logic, or they may
+	 * provide an 'active_callback' argument to the constructor.
+	 *
+	 * @since 4.1.0
+	 * @access public
+	 *
+	 * @return bool Always true.
+	 */
+	public function active_callback() {
+		return true;
+	}
+
+	/**
+	 * Gather the parameters passed to client JavaScript via JSON.
+	 *
+	 * @since 4.1.0
+	 *
+	 * @return array The array to be exported to the client as JSON
+	 */
+	public function json() {
+		$array = wp_array_slice_assoc( (array) $this, array( 'title', 'description', 'priority', 'panel', 'type' ) );
+		$array['content'] = $this->get_content();
+		$array['active'] = $this->active();
+		return $array;
+	}
+
+	/**
 	 * Checks required user capabilities and whether the theme has the
 	 * feature support required by the section.
 	 *
@@ -126,23 +206,41 @@ class WP_Customize_Section {
 	 * @return bool False if theme doesn't support the section or user doesn't have the capability.
 	 */
 	public final function check_capabilities() {
-		if ( $this->capability && ! call_user_func_array( 'current_user_can', (array) $this->capability ) )
+		if ( $this->capability && ! call_user_func_array( 'current_user_can', (array) $this->capability ) ) {
 			return false;
+		}
 
-		if ( $this->theme_supports && ! call_user_func_array( 'current_theme_supports', (array) $this->theme_supports ) )
+		if ( $this->theme_supports && ! call_user_func_array( 'current_theme_supports', (array) $this->theme_supports ) ) {
 			return false;
+		}
 
 		return true;
 	}
 
 	/**
+	 * Get the section's content template for insertion into the Customizer pane.
+	 *
+	 * @since 4.1.0
+	 *
+	 * @return string
+	 */
+	public final function get_content() {
+		ob_start();
+		$this->maybe_render();
+		$template = trim( ob_get_contents() );
+		ob_end_clean();
+		return $template;
+	}
+
+	/**
 	 * Check capabilities and render the section.
 	 *
 	 * @since 3.4.0
 	 */
 	public final function maybe_render() {
-		if ( ! $this->check_capabilities() )
+		if ( ! $this->check_capabilities() ) {
 			return;
+		}
 
 		/**
 		 * Fires before rendering a Customizer section.
@@ -172,9 +270,6 @@ class WP_Customize_Section {
 	 */
 	protected function render() {
 		$classes = 'control-section accordion-section';
-		if ( $this->panel ) {
-			$classes .= ' control-subsection';
-		}
 		?>
 		<li id="accordion-section-<?php echo esc_attr( $this->id ); ?>" class="<?php echo esc_attr( $classes ); ?>">
 			<h3 class="accordion-section-title" tabindex="0">
@@ -183,12 +278,10 @@ class WP_Customize_Section {
 			</h3>
 			<ul class="accordion-section-content">
 				<?php if ( ! empty( $this->description ) ) : ?>
-				<li><p class="description customize-section-description"><?php echo $this->description; ?></p></li>
+					<li class="customize-section-description-container">
+						<p class="description customize-section-description"><?php echo $this->description; ?></p>
+					</li>
 				<?php endif; ?>
-				<?php
-				foreach ( $this->controls as $control )
-					$control->maybe_render();
-				?>
 			</ul>
 		</li>
 		<?php
diff --git src/wp-includes/js/customize-base.js src/wp-includes/js/customize-base.js
index d2488dd..6ea2822 100644
--- src/wp-includes/js/customize-base.js
+++ src/wp-includes/js/customize-base.js
@@ -184,8 +184,9 @@ window.wp = window.wp || {};
 			to = this.validate( to );
 
 			// Bail if the sanitized value is null or unchanged.
-			if ( null === to || _.isEqual( from, to ) )
+			if ( null === to || _.isEqual( from, to ) ) {
 				return this;
+			}
 
 			this._value = to;
 			this._dirty = true;
diff --git src/wp-includes/js/customize-preview.js src/wp-includes/js/customize-preview.js
index 6da26f4..1a82565 100644
--- src/wp-includes/js/customize-preview.js
+++ src/wp-includes/js/customize-preview.js
@@ -107,6 +107,8 @@
         });
 
 		preview.send( 'ready', {
+			activePanels: api.settings.activePanels,
+			activeSections: api.settings.activeSections,
 			activeControls: api.settings.activeControls
 		} );
 
diff --git tests/qunit/index.html tests/qunit/index.html
index c5afd52..ce11144 100644
--- tests/qunit/index.html
+++ tests/qunit/index.html
@@ -8,7 +8,7 @@
   <script src="../../src/wp-includes/js/underscore.min.js"></script>
   <script src="../../src/wp-includes/js/backbone.min.js"></script>
   <script src="../../src/wp-includes/js/zxcvbn.min.js"></script>
-	
+
   <!-- QUnit -->
   <link rel="stylesheet" href="vendor/qunit.css" type="text/css" media="screen" />
   <script src="vendor/qunit.js"></script>
@@ -28,14 +28,15 @@
 
     <!-- Tested files -->
     <script src="../../src/wp-admin/js/password-strength-meter.js"></script>
+    <script src="../../src/wp-includes/js/customize-base.js"></script>
     <script src="../../src/wp-includes/js/customize-models.js"></script>
     <script src="../../src/wp-includes/js/shortcode.js"></script>
 
     <!-- Unit tests -->
     <script src="wp-admin/js/password-strength-meter.js"></script>
+    <script src="wp-admin/js/customize-base.js"></script>
     <script src="wp-admin/js/customize-header.js"></script>
     <script src="wp-includes/js/shortcode.js"></script>
   </div>
 </body>
 </html>
-
diff --git tests/qunit/wp-admin/js/customize-base.js tests/qunit/wp-admin/js/customize-base.js
new file mode 100644
index 0000000..c253940
--- /dev/null
+++ tests/qunit/wp-admin/js/customize-base.js
@@ -0,0 +1,84 @@
+/* global wp, sinon */
+
+jQuery( function( $ ) {
+	var FooSuperClass, BarSubClass, foo, bar;
+
+	module( 'Customize Base: Class' );
+
+	FooSuperClass = wp.customize.Class.extend(
+		{
+			initialize: function ( instanceProps ) {
+				$.extend( this, instanceProps || {} );
+			},
+			protoProp: 'protoPropValue' },
+		{
+			staticProp: 'staticPropValue'
+		}
+	);
+	test( 'FooSuperClass is a function ', function () {
+		equal( typeof FooSuperClass, 'function' );
+	});
+	test( 'FooSuperClass prototype has protoProp', function () {
+		equal( FooSuperClass.prototype.protoProp, 'protoPropValue' );
+	});
+	test( 'FooSuperClass does not have protoProp', function () {
+		equal( typeof FooSuperClass.protoProp, 'undefined' );
+	});
+	test( 'FooSuperClass has staticProp', function () {
+		equal( FooSuperClass.staticProp, 'staticPropValue' );
+	});
+	test( 'FooSuperClass prototype does not have staticProp', function () {
+		equal( typeof FooSuperClass.prototype.staticProp, 'undefined' );
+	});
+
+	foo = new FooSuperClass( { instanceProp: 'instancePropValue' } );
+	test( 'FooSuperClass instance foo extended Class', function () {
+		equal( foo.extended( wp.customize.Class ), true );
+	});
+	test( 'foo instance has protoProp', function () {
+		equal( foo.protoProp, 'protoPropValue' );
+	});
+	test( 'foo instance does not have staticProp', function () {
+		equal( typeof foo.staticProp, 'undefined' );
+	});
+	test( 'FooSuperClass instance foo ran initialize() and has supplied instanceProp', function () {
+		equal( foo.instanceProp, 'instancePropValue' );
+	});
+
+	// @todo Test Class.constructor() manipulation
+	// @todo Test Class.applicator?
+	// @todo do we test object.instance?
+
+
+	module( 'Customize Base: Subclass' );
+
+	BarSubClass = FooSuperClass.extend(
+		{
+			initialize: function ( instanceProps ) {
+				FooSuperClass.prototype.initialize.call( this, instanceProps );
+				this.subInstanceProp = 'subInstancePropValue';
+			},
+			subProtoProp: 'subProtoPropValue'
+		},
+		{
+			subStaticProp: 'subStaticPropValue'
+		}
+	);
+	test( 'BarSubClass prototype has subProtoProp', function () {
+		equal( BarSubClass.prototype.subProtoProp, 'subProtoPropValue' );
+	});
+	test( 'BarSubClass prototype has parent FooSuperClass protoProp', function () {
+		equal( BarSubClass.prototype.protoProp, 'protoPropValue' );
+	});
+
+	bar = new BarSubClass( { instanceProp: 'instancePropValue' } );
+	test( 'BarSubClass instance bar its initialize() and parent initialize() run', function () {
+		equal( bar.instanceProp, 'instancePropValue' );
+		equal( bar.subInstanceProp, 'subInstancePropValue' );
+	});
+
+	test( 'BarSubClass instance bar extended FooSuperClass', function () {
+		equal( bar.extended( FooSuperClass ), true );
+	});
+
+});
