diff --git src/wp-admin/css/customize-controls.css src/wp-admin/css/customize-controls.css
index b35bc3b..6b9d910 100644
--- src/wp-admin/css/customize-controls.css
+++ src/wp-admin/css/customize-controls.css
@@ -123,13 +123,17 @@ body {
 	color: #0073aa;
 }
 
-#customize-controls .customize-info .customize-panel-description {
+#customize-controls .customize-info .customize-panel-description,
+#customize-controls .no-widget-areas-rendered-notice {
 	color: #555;
 	display: none;
 	background: #fff;
 	padding: 12px 15px;
 	border-top: 1px solid #ddd;
 }
+#customize-controls .customize-info .customize-panel-description.open + .no-widget-areas-rendered-notice {
+	border-top: none;
+}
 
 #customize-controls .customize-info .customize-panel-description p:first-child {
 	margin-top: 0;
diff --git src/wp-admin/js/customize-controls.js src/wp-admin/js/customize-controls.js
index e0e4a4d..b1e7ba6 100644
--- src/wp-admin/js/customize-controls.js
+++ src/wp-admin/js/customize-controls.js
@@ -300,7 +300,7 @@
 		 * @param {Object}  args.completeCallback
 		 */
 		onChangeActive: function( active, args ) {
-			var duration, construct = this;
+			var duration, construct = this, expandedOtherPanel;
 			if ( args.unchanged ) {
 				if ( args.completeCallback ) {
 					args.completeCallback();
@@ -309,6 +309,24 @@
 			}
 
 			duration = ( 'resolved' === api.previewer.deferred.active.state() ? args.duration : 0 );
+
+			if ( construct.extended( api.Panel ) ) {
+				// If this is a panel is not currently expanded but another panel is expanded, do not animate.
+				api.panel.each(function ( panel ) {
+					if ( panel !== construct && panel.expanded() ) {
+						expandedOtherPanel = panel;
+						duration = 0;
+					}
+				});
+
+				// Collapse any expanded sections inside of this panel first before deactivating.
+				if ( ! active ) {
+					_.each( construct.sections(), function( section ) {
+						section.collapse( { duration: 0 } );
+					} );
+				}
+			}
+
 			if ( ! $.contains( document, construct.container[0] ) ) {
 				// jQuery.fn.slideUp is not hiding an element if it is not in the DOM
 				construct.container.toggle( active );
@@ -329,6 +347,11 @@
 					construct.container.stop( true, true ).slideUp( duration, args.completeCallback );
 				}
 			}
+
+			// Recalculate the margin-top immediately, not waiting for debounced reflow, to prevent momentary (100ms) vertical jiggle.
+			if ( expandedOtherPanel ) {
+				expandedOtherPanel._recalculateTopMargin();
+			}
 		},
 
 		/**
@@ -378,39 +401,48 @@
 		},
 
 		/**
-		 * @param {Boolean} expanded
-		 * @param {Object} [params]
-		 * @returns {Boolean} false if state already applied
+		 * Handle the toggle logic for expand/collapse.
+		 *
+		 * @param {Boolean}  expanded - The new state to apply.
+		 * @param {Object}   [params] - Object containing options for expand/collapse.
+		 * @param {Function} [params.completeCallback] - Function to call when expansion/collapse is complete.
+		 * @returns {Boolean} false if state already applied or active state is false
 		 */
-		_toggleExpanded: function ( expanded, params ) {
-			var self = this;
+		_toggleExpanded: function( expanded, params ) {
+			var instance = this, previousCompleteCallback;
 			params = params || {};
-			var section = this, previousCompleteCallback = params.completeCallback;
-			params.completeCallback = function () {
+			previousCompleteCallback = params.completeCallback;
+
+			// Short-circuit expand() if the instance is not active.
+			if ( expanded && ! instance.active() ) {
+				return false;
+			}
+
+			params.completeCallback = function() {
 				if ( previousCompleteCallback ) {
-					previousCompleteCallback.apply( section, arguments );
+					previousCompleteCallback.apply( instance, arguments );
 				}
 				if ( expanded ) {
-					section.container.trigger( 'expanded' );
+					instance.container.trigger( 'expanded' );
 				} else {
-					section.container.trigger( 'collapsed' );
+					instance.container.trigger( 'collapsed' );
 				}
 			};
-			if ( ( expanded && this.expanded.get() ) || ( ! expanded && ! this.expanded.get() ) ) {
+			if ( ( expanded && instance.expanded.get() ) || ( ! expanded && ! instance.expanded.get() ) ) {
 				params.unchanged = true;
-				self.onChangeExpanded( self.expanded.get(), params );
+				instance.onChangeExpanded( instance.expanded.get(), params );
 				return false;
 			} else {
 				params.unchanged = false;
-				this.expandedArgumentsQueue.push( params );
-				this.expanded.set( expanded );
+				instance.expandedArgumentsQueue.push( params );
+				instance.expanded.set( expanded );
 				return true;
 			}
 		},
 
 		/**
 		 * @param {Object} [params]
-		 * @returns {Boolean} false if already expanded
+		 * @returns {Boolean} false if already expanded or if inactive.
 		 */
 		expand: function ( params ) {
 			return this._toggleExpanded( true, params );
@@ -418,7 +450,7 @@
 
 		/**
 		 * @param {Object} [params]
-		 * @returns {Boolean} false if already collapsed
+		 * @returns {Boolean} false if already collapsed.
 		 */
 		collapse: function ( params ) {
 			return this._toggleExpanded( false, params );
@@ -539,6 +571,13 @@
 			};
 			section.panel.bind( inject );
 			inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one
+
+			section.deferred.embedded.done(function() {
+				// Fix the top margin after reflow.
+				api.bind( 'pane-contents-reflowed', _.debounce( function() {
+					section._recalculateTopMargin();
+				}, 100 ) );
+			});
 		},
 
 		/**
@@ -646,13 +685,7 @@
 						// Fix the height after browser resize.
 						$( window ).on( 'resize.customizer-section', _.debounce( resizeContentHeight, 100 ) );
 
-						// Fix the top margin after reflow.
-						api.bind( 'pane-contents-reflowed', _.debounce( function() {
-							var offset = ( content.offset().top - headerActionsHeight );
-							if ( 0 < offset ) {
-								content.css( 'margin-top', ( parseInt( content.css( 'margin-top' ), 10 ) - offset ) );
-							}
-						}, 100 ) );
+						section._recalculateTopMargin();
 					};
 				}
 
@@ -693,6 +726,25 @@
 					args.completeCallback();
 				}
 			}
+		},
+
+		/**
+		 * Recalculate the top margin.
+		 *
+		 * @since 4.4.0
+		 * @private
+		 */
+		_recalculateTopMargin: function() {
+			var section = this, content, offset, headerActionsHeight;
+			content = section.container.find( '.accordion-section-content' );
+			if ( 0 === content.length ) {
+				return;
+			}
+			headerActionsHeight = $( '#customize-header-actions' ).height();
+			offset = ( content.offset().top - headerActionsHeight );
+			if ( 0 < offset ) {
+				content.css( 'margin-top', ( parseInt( content.css( 'margin-top' ), 10 ) - offset ) );
+			}
 		}
 	});
 
@@ -1155,6 +1207,11 @@
 				parentContainer.append( panel.container );
 				panel.renderContent();
 			}
+
+			api.bind( 'pane-contents-reflowed', _.debounce( function() {
+				panel._recalculateTopMargin();
+			}, 100 ) );
+
 			panel.deferred.embedded.resolve();
 		},
 
@@ -1253,7 +1310,7 @@
 		 * @param {Boolean}  expanded
 		 * @param {Object}   args
 		 * @param {Boolean}  args.unchanged
-		 * @param {Callback} args.completeCallback
+		 * @param {Function} args.completeCallback
 		 */
 		onChangeExpanded: function ( expanded, args ) {
 
@@ -1268,14 +1325,14 @@
 			// Note: there is a second argument 'args' passed
 			var position, scroll,
 				panel = this,
-				section = panel.container.closest( '.accordion-section' ), // This is actually the panel.
-				overlay = section.closest( '.wp-full-overlay' ),
-				container = section.closest( '.wp-full-overlay-sidebar-content' ),
+				accordionSection = panel.container.closest( '.accordion-section' ),
+				overlay = accordionSection.closest( '.wp-full-overlay' ),
+				container = accordionSection.closest( '.wp-full-overlay-sidebar-content' ),
 				siblings = container.find( '.open' ),
 				topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ),
-				backBtn = section.find( '.customize-panel-back' ),
-				panelTitle = section.find( '.accordion-section-title' ).first(),
-				content = section.find( '.control-panel-content' ),
+				backBtn = accordionSection.find( '.customize-panel-back' ),
+				panelTitle = accordionSection.find( '.accordion-section-title' ).first(),
+				content = accordionSection.find( '.control-panel-content' ),
 				headerActionsHeight = $( '#customize-header-actions' ).height();
 
 			if ( expanded ) {
@@ -1297,7 +1354,7 @@
 					position = content.offset().top;
 					scroll = container.scrollTop();
 					content.css( 'margin-top', ( headerActionsHeight - position - scroll ) );
-					section.addClass( 'current-panel' );
+					accordionSection.addClass( 'current-panel' );
 					overlay.addClass( 'in-sub-panel' );
 					container.scrollTop( 0 );
 					if ( args.completeCallback ) {
@@ -1307,14 +1364,10 @@
 				topPanel.attr( 'tabindex', '-1' );
 				backBtn.attr( 'tabindex', '0' );
 				backBtn.focus();
-
-				// Fix the top margin after reflow.
-				api.bind( 'pane-contents-reflowed', _.debounce( function() {
-					content.css( 'margin-top', ( parseInt( content.css( 'margin-top' ), 10 ) - ( content.offset().top - headerActionsHeight ) ) );
-				}, 100 ) );
+				panel._recalculateTopMargin();
 			} else {
 				siblings.removeClass( 'open' );
-				section.removeClass( 'current-panel' );
+				accordionSection.removeClass( 'current-panel' );
 				overlay.removeClass( 'in-sub-panel' );
 				content.delay( 180 ).hide( 0, function() {
 					content.css( 'margin-top', 'inherit' ); // Reset
@@ -1330,6 +1383,20 @@
 		},
 
 		/**
+		 * Recalculate the top margin.
+		 *
+		 * @since 4.4.0
+		 * @private
+		 */
+		_recalculateTopMargin: function() {
+			var panel = this, headerActionsHeight, content, accordionSection;
+			headerActionsHeight = $( '#customize-header-actions' ).height();
+			accordionSection = panel.container.closest( '.accordion-section' );
+			content = accordionSection.find( '.control-panel-content' );
+			content.css( 'margin-top', ( parseInt( content.css( 'margin-top' ), 10 ) - ( content.offset().top - headerActionsHeight ) ) );
+		},
+
+		/**
 		 * Render the panel from its JS template, if it exists.
 		 *
 		 * The panel's container must already exist in the DOM.
diff --git src/wp-admin/js/customize-widgets.js src/wp-admin/js/customize-widgets.js
index bd25757..144365e 100644
--- src/wp-admin/js/customize-widgets.js
+++ src/wp-admin/js/customize-widgets.js
@@ -1506,6 +1506,80 @@
 	} );
 
 	/**
+	 * wp.customize.Widgets.WidgetsPanel
+	 *
+	 * Customizer panel containing the widget area sections.
+	 *
+	 * @since 4.4.0
+	 */
+	api.Widgets.WidgetsPanel = api.Panel.extend({
+
+		/**
+		 * Add and manage the display of the no-rendered-areas notice.
+		 *
+		 * @since 4.4.0
+		 */
+		ready: function () {
+			var panel = this;
+
+			api.Panel.prototype.ready.call( panel );
+
+			panel.deferred.embedded.done(function() {
+				var panelMetaContainer, noRenderedAreasNotice, shouldShowNotice;
+				panelMetaContainer = panel.container.find( '.panel-meta' );
+				noRenderedAreasNotice = $( '<div></div>', {
+					'class': 'no-widget-areas-rendered-notice'
+				});
+				noRenderedAreasNotice.append( $( '<em></em>', {
+					text: l10n.noAreasRendered
+				} ) );
+				panelMetaContainer.append( noRenderedAreasNotice );
+
+				shouldShowNotice = function() {
+					return ( 0 === _.filter( panel.sections(), function( section ) {
+						return section.active();
+					} ).length );
+				};
+
+				/*
+				 * Defer setting visibility of no-rendered-areas-notice until
+				 * preview finishes loading since this is when the active
+				 * sections become available.
+				 */
+				api.previewer.deferred.active.done( function () {
+					noRenderedAreasNotice.toggle( shouldShowNotice() );
+
+					// Update the visibility of the notice whenever a reflow happens.
+					api.bind( 'pane-contents-reflowed', function() {
+						api.previewer.deferred.active.done( function () {
+							if ( shouldShowNotice() ) {
+								noRenderedAreasNotice.slideDown( 'fast' );
+							} else {
+								noRenderedAreasNotice.slideUp( 'fast' );
+							}
+						});
+					});
+				});
+			});
+		},
+
+		/**
+		 * Allow an active widgets panel to be contextually active even when it has no active sections (widget areas).
+		 *
+		 * This ensures that the widgets panel appears even when there are no
+		 * sidebars displayed on the URL currently being previewed.
+		 *
+		 * @since 4.4.0
+		 *
+		 * @returns {boolean}
+		 */
+		isContextuallyActive: function() {
+			var panel = this;
+			return panel.active();
+		}
+	});
+
+	/**
 	 * wp.customize.Widgets.SidebarSection
 	 *
 	 * Customizer section representing a widget area widget
@@ -1968,7 +2042,10 @@
 		}
 	} );
 
-	// Register models for custom section and control types
+	// Register models for custom panel, section, and control types
+	$.extend( api.panelConstructor, {
+		widgets: api.Widgets.WidgetsPanel
+	});
 	$.extend( api.sectionConstructor, {
 		sidebar: api.Widgets.SidebarSection
 	});
diff --git src/wp-includes/class-wp-customize-widgets.php src/wp-includes/class-wp-customize-widgets.php
index 7b73dc2..c1a8e5e 100644
--- src/wp-includes/class-wp-customize-widgets.php
+++ src/wp-includes/class-wp-customize-widgets.php
@@ -355,9 +355,11 @@ final class WP_Customize_Widgets {
 		}
 
 		$this->manager->add_panel( 'widgets', array(
-			'title'       => __( 'Widgets' ),
-			'description' => __( 'Widgets are independent sections of content that can be placed into widgetized areas provided by your theme (commonly called sidebars).' ),
-			'priority'    => 110,
+			'type'            => 'widgets',
+			'title'           => __( 'Widgets' ),
+			'description'     => __( 'Widgets are independent sections of content that can be placed into widgetized areas provided by your theme (commonly called sidebars).' ),
+			'priority'        => 110,
+			'active_callback' => array( $this, 'is_panel_active' ),
 		) );
 
 		foreach ( $sidebars_widgets as $sidebar_id => $sidebar_widget_ids ) {
@@ -455,6 +457,22 @@ final class WP_Customize_Widgets {
 	}
 
 	/**
+	 * Return whether the widgets panel is active, based on whether there are sidebars registered.
+	 *
+	 * @since 4.4.0
+	 * @access public
+	 *
+	 * @see WP_Customize_Panel::$active_callback
+	 *
+	 * @global array $wp_registered_sidebars
+	 * @return bool Active.
+	 */
+	public function is_panel_active() {
+		global $wp_registered_sidebars;
+		return ! empty( $wp_registered_sidebars );
+	}
+
+	/**
 	 * Covert a widget_id into its corresponding Customizer setting ID (option name).
 	 *
 	 * @since 3.9.0
@@ -655,6 +673,7 @@ final class WP_Customize_Widgets {
 				'error'            => __( 'An error has occurred. Please reload the page and try again.' ),
 				'widgetMovedUp'    => __( 'Widget moved up' ),
 				'widgetMovedDown'  => __( 'Widget moved down' ),
+				'noAreasRendered'  => __( 'There are no widget areas currently rendered in the preview. Navigate in the preview to a template that makes use of a widget area in order to access its widgets here.' ),
 			),
 			'tpl' => array(
 				'widgetReorderNav' => $widget_reorder_nav_tpl,
diff --git tests/qunit/wp-admin/js/customize-controls.js tests/qunit/wp-admin/js/customize-controls.js
index 6ca4908..91a7d36 100644
--- tests/qunit/wp-admin/js/customize-controls.js
+++ tests/qunit/wp-admin/js/customize-controls.js
@@ -393,7 +393,7 @@ jQuery( window ).load( function (){
 	panelId = 'mockPanelId';
 	panelTitle = 'Mock Panel Title';
 	panelDescription = 'Mock panel description';
-	panelContent = '<li id="accordion-panel-widgets" class="control-section control-panel accordion-section">';
+	panelContent = '<li id="accordion-panel-mockPanelId" class="accordion-section control-section control-panel control-panel-default"> <h3 class="accordion-section-title" tabindex="0"> Fixture Panel <span class="screen-reader-text">Press return or enter to open this panel</span> </h3> <ul class="accordion-sub-container control-panel-content"> <li class="panel-meta customize-info accordion-section cannot-expand"> <button class="customize-panel-back" tabindex="-1"><span class="screen-reader-text">Back</span></button> <div class="accordion-section-title"> <span class="preview-notice">You are customizing <strong class="panel-title">Fixture Panel</strong></span> <button class="customize-help-toggle dashicons dashicons-editor-help" tabindex="0" aria-expanded="false"><span class="screen-reader-text">Help</span></button> </div> </li> </ul> </li>';
 	panelData = {
 		content: panelContent,
 		title: panelTitle,
diff --git tests/qunit/wp-admin/js/customize-widgets.js tests/qunit/wp-admin/js/customize-widgets.js
index 3a8a3e3..f5c99e3 100644
--- tests/qunit/wp-admin/js/customize-widgets.js
+++ tests/qunit/wp-admin/js/customize-widgets.js
@@ -34,9 +34,14 @@ jQuery( window ).load( function() {
 		ok( ! section.expanded() );
 		ok( 0 === control.container.find( '> .widget' ).length );
 
+		// Preview sets the active state.
+		section.active.set( true );
+		control.active.set( true );
+		api.control( 'sidebars_widgets[sidebar-1]' ).active.set( true );
+
 		section.expand();
-		ok( ! widgetAddedEvent );
-		ok( 1 === control.container.find( '> .widget' ).length );
+		ok( ! widgetAddedEvent, 'expected widget added event not fired' );
+		ok( 1 === control.container.find( '> .widget' ).length, 'expected there to be one .widget element in the container' );
 		ok( 0 === control.container.find( '.widget-content' ).children().length );
 
 		control.expand();
