diff --git src/wp-admin/js/customize-controls.js src/wp-admin/js/customize-controls.js
index 45fb65f..70b9fbe 100644
--- src/wp-admin/js/customize-controls.js
+++ src/wp-admin/js/customize-controls.js
@@ -2357,8 +2357,7 @@
 		defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
 
 		initialize: function( id, options ) {
-			var control = this,
-				nodes, radios, settings;
+			var control = this, deferredSettingIds = [];
 
 			control.params = {};
 			$.extend( control, options || {} );
@@ -2378,31 +2377,6 @@
 
 			control.elements = [];
 
-			nodes  = control.container.find('[data-customize-setting-link]');
-			radios = {};
-
-			nodes.each( function() {
-				var node = $( this ),
-					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 ) {
-					var element = new api.Element( node );
-					control.elements.push( element );
-					element.sync( setting );
-					element.set( setting() );
-				});
-			});
-
 			control.active.bind( function ( active ) {
 				var args = control.activeArgumentsQueue.shift();
 				args = $.extend( {}, control.defaultActiveArguments, args );
@@ -2415,72 +2389,135 @@
 
 			api.utils.bubbleChildValueChanges( control, [ 'section', 'priority', 'active' ] );
 
+			control.settings = {};
+
 			/*
 			 * After all settings related to the control are available,
 			 * make them available on the control and embed the control into the page.
 			 */
-			settings = $.map( control.params.settings, function( value ) {
-				return value;
-			});
-
-			if ( 0 === settings.length ) {
-				control.setting = null;
-				control.settings = {};
-				control.embed();
-			} else {
-				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;
+			_.each( control.params.settings, function( setting, key ) {
+				if ( _.isObject( setting ) ) {
+					control.settings[ key ] = setting;
+				} else {
+					deferredSettingIds.push( setting );
+				}
+			} );
 
-					// Add setting notifications to the control notification.
-					_.each( control.settings, function( setting ) {
-						setting.notifications.bind( 'add', function( settingNotification ) {
-							var controlNotification, code, params;
-							code = setting.id + ':' + settingNotification.code;
-							params = _.extend(
-								{},
-								settingNotification,
-								{
-									setting: setting.id
-								}
-							);
-							controlNotification = new api.Notification( code, params );
-							control.notifications.add( controlNotification.code, controlNotification );
-						} );
-						setting.notifications.bind( 'remove', function( settingNotification ) {
-							control.notifications.remove( setting.id + ':' + settingNotification.code );
-						} );
+			if ( deferredSettingIds.length > 0 ) {
+				api.apply( api, deferredSettingIds.concat( function() {
+					_.each( control.params.settings, function( settingId, key ) {
+						if ( _.isString( settingId ) ) {
+							control.settings[ key ] = api( settingId );
+						}
 					} );
 
+					control.setting = control.settings['default'] || null;
 					control.embed();
 				}) );
+			} else {
+				control.setting = control.settings['default'] || null;
+				control.embed();
 			}
 
 			// After the control is embedded on the page, invoke the "ready" method.
 			control.deferred.embedded.done( function () {
-				/*
-				 * Note that this debounced/deferred rendering is needed for two reasons:
-				 * 1) The 'remove' event is triggered just _before_ the notification is actually removed.
-				 * 2) Improve performance when adding/removing multiple notifications at a time.
-				 */
-				var debouncedRenderNotifications = _.debounce( function renderNotifications() {
-					control.renderNotifications();
+				control.linkElements();
+				control.setupNotifications();
+				control.ready();
+			});
+		},
+
+		/**
+		 * Link elements between settings and inputs.
+		 *
+		 * @since 4.7.0
+		 * @access public
+		 *
+		 * @returns {void}
+		 */
+		linkElements: function() {
+			var control = this, nodes, radios, element;
+
+			nodes = control.container.find( '[data-customize-setting-link], [data-customize-value-link]' );
+			radios = {};
+
+			nodes.each( function() {
+				var node = $( this ), name, setting;
+
+				if ( node.data( 'customizeSettingLinked' ) ) {
+					return;
+				}
+				node.data( 'customizeSettingLinked', true ); // Prevent re-linking element.
+
+				if ( node.is( ':radio' ) ) {
+					name = node.prop( 'name' );
+					if ( radios[ name ] ) {
+						return;
+					}
+
+					radios[ name ] = true;
+					node = nodes.filter( '[name="' + name + '"]' );
+				}
+
+				// Let link by default refer to setting ID. If it doesn't exist, fallback to looking up by setting key.
+				if ( node.data( 'customizeSettingLink' ) ) {
+					setting = api( node.data( 'customizeSettingLink' ) );
+				}
+				if ( ! setting ) {
+					setting = control.settings[ node.data( 'customizeValueLink' ) ];
+				}
+
+				if ( setting ) {
+					element = new api.Element( node );
+					control.elements.push( element );
+					element.sync( setting );
+					element.set( setting() );
+				}
+			});
+		},
+
+		/**
+		 * Sync setting notifications to the control notifications and render when notifications are aded/removed.
+		 *
+		 * @since 4.7.0
+		 * @returns {void}
+		 */
+		setupNotifications: function() {
+			var control = this, debouncedRenderNotifications;
+
+			_.each( control.settings, function( setting, settingKey ) {
+				setting.notifications.bind( 'add', function( settingNotification ) {
+					var controlNotification, code, params;
+					code = ( setting.id || settingKey ) + ':' + settingNotification.code;
+					params = _.extend(
+						{},
+						settingNotification,
+						{
+							setting: setting.id || null // @todo Let this be the setting object itself?
+						}
+					);
+					controlNotification = new api.Notification( code, params );
+					control.notifications.add( controlNotification.code, controlNotification );
 				} );
-				control.notifications.bind( 'add', function( notification ) {
-					wp.a11y.speak( notification.message, 'assertive' );
-					debouncedRenderNotifications();
+				setting.notifications.bind( 'remove', function( settingNotification ) {
+					control.notifications.remove( ( setting.id || settingKey ) + ':' + settingNotification.code );
 				} );
-				control.notifications.bind( 'remove', debouncedRenderNotifications );
-				control.renderNotifications();
+			} );
 
-				control.ready();
-			});
+			/*
+			 * Note that this debounced/deferred rendering is needed for two reasons:
+			 * 1) The 'remove' event is triggered just _before_ the notification is actually removed.
+			 * 2) Improve performance when adding/removing multiple notifications at a time.
+			 */
+			debouncedRenderNotifications = _.debounce( function renderNotifications() {
+				control.renderNotifications();
+			} );
+			control.notifications.bind( 'add', function( notification ) {
+				wp.a11y.speak( notification.message, 'assertive' );
+				debouncedRenderNotifications();
+			} );
+			control.notifications.bind( 'remove', debouncedRenderNotifications );
+			control.renderNotifications();
 		},
 
 		/**
diff --git src/wp-includes/class-wp-customize-control.php src/wp-includes/class-wp-customize-control.php
index 7bed232..7f44b9f 100644
--- src/wp-includes/class-wp-customize-control.php
+++ src/wp-includes/class-wp-customize-control.php
@@ -419,15 +419,18 @@ class WP_Customize_Control {
 	 * Get the data link attribute for a setting.
 	 *
 	 * @since 3.4.0
+	 * @since 4.7.0 Returning a `data-customize-value-link` attribute if a setting is not registered for the key.
 	 *
 	 * @param string $setting_key
-	 * @return string Data link parameter, if $setting_key is a valid setting, empty string otherwise.
+	 * @return string Data link parameter, a `data-customize-setting-link` attribute if the `$setting_key` refers to a pre-registered setting,
+	 *                and a `data-customize-value-link` attribute if the setting is not registered.
 	 */
 	public function get_link( $setting_key = 'default' ) {
-		if ( ! isset( $this->settings[ $setting_key ] ) )
-			return '';
-
-		return 'data-customize-setting-link="' . esc_attr( $this->settings[ $setting_key ]->id ) . '"';
+		if ( isset( $this->settings[ $setting_key ] ) && $this->settings[ $setting_key ] instanceof WP_Customize_Setting ) {
+			return 'data-customize-setting-link="' . esc_attr( $this->settings[ $setting_key ]->id ) . '"';
+		} else {
+			return 'data-customize-value-link="' . esc_attr( $setting_key ) . '"';
+		}
 	}
 
 	/**
