diff --git src/wp-admin/css/customize-nav-menus.css src/wp-admin/css/customize-nav-menus.css
index a95c8fcb92..589519fc27 100644
--- src/wp-admin/css/customize-nav-menus.css
+++ src/wp-admin/css/customize-nav-menus.css
@@ -578,6 +578,7 @@
 
 #custom-menu-item-name.invalid,
 #custom-menu-item-url.invalid,
+.edit-menu-item-url.invalid,
 .menu-name-field.invalid,
 .menu-name-field.invalid:focus,
 #available-menu-items .new-content-item .create-item-input.invalid,
diff --git src/wp-admin/js/customize-nav-menus.js src/wp-admin/js/customize-nav-menus.js
index 3b7af24302..2587fe475d 100644
--- src/wp-admin/js/customize-nav-menus.js
+++ src/wp-admin/js/customize-nav-menus.js
@@ -1381,7 +1381,8 @@
 		 */
 		_setupUpdateUI: function() {
 			var control = this,
-				settingValue = control.setting();
+				settingValue = control.setting(),
+				updateNotifications;
 
 			control.elements = {};
 			control.elements.url = new api.Element( control.container.find( '.edit-menu-item-url' ) );
@@ -1464,6 +1465,13 @@
 					}
 				}
 			});
+
+			// Style the URL field as invalid when there is an invalid_url notification.
+			updateNotifications = _.debounce( function() {
+				control.elements.url.element.toggleClass( 'invalid', control.setting.notifications.has( 'invalid_url' ) );
+			} );
+			control.setting.notifications.bind( 'add', updateNotifications );
+			control.setting.notifications.bind( 'remove', updateNotifications );
 		},
 
 		/**
diff --git src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php
index 11b5cd6d5a..2b4aabaffd 100644
--- src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php
+++ src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php
@@ -662,7 +662,7 @@ class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting {
 	 * @access public
 	 *
 	 * @param array $menu_item_value The value to sanitize.
-	 * @return array|false|null Null if an input isn't valid. False if it is marked for deletion.
+	 * @return array|false|null|WP_Error Null if an input isn't valid. False if it is marked for deletion.
 	 *                          Otherwise the sanitized value.
 	 */
 	public function sanitize( $menu_item_value ) {
@@ -722,7 +722,12 @@ class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting {
 		$menu_item_value['attr_title'] = wp_unslash( apply_filters( 'excerpt_save_pre', wp_slash( $menu_item_value['attr_title'] ) ) );
 		$menu_item_value['description'] = wp_unslash( apply_filters( 'content_save_pre', wp_slash( $menu_item_value['description'] ) ) );
 
-		$menu_item_value['url'] = esc_url_raw( $menu_item_value['url'] );
+		if ( '' !== $menu_item_value['url'] ) {
+			$menu_item_value['url'] = esc_url_raw( $menu_item_value['url'] );
+			if ( '' === $menu_item_value['url'] ) {
+				return new WP_Error( 'invalid_url', __( 'Invalid URL.' ) ); // Fail sanitization if URL is invalid.
+			}
+		}
 		if ( 'publish' !== $menu_item_value['status'] ) {
 			$menu_item_value['status'] = 'draft';
 		}
diff --git tests/phpunit/tests/customize/nav-menu-item-setting.php tests/phpunit/tests/customize/nav-menu-item-setting.php
index bcb3dc56de..04d80ca987 100644
--- tests/phpunit/tests/customize/nav-menu-item-setting.php
+++ tests/phpunit/tests/customize/nav-menu-item-setting.php
@@ -472,6 +472,43 @@ class Test_WP_Customize_Nav_Menu_Item_Setting extends WP_UnitTestCase {
 		$this->assertNull( $setting->sanitize( 'not an array' ) );
 		$this->assertNull( $setting->sanitize( 123 ) );
 
+		$valid_urls = array(
+			'http://example.com/',
+			'https://foo.example.com/hello.html',
+			'mailto:nobody@example.com?subject=hi',
+			'ftp://example.com/',
+			'ftps://example.com/',
+			'news://news.server.example/example.group.this',
+			'irc://irc.freenode.net/wordpress',
+			'gopher://example.com',
+			'nntp://news.server.example/example.group.this',
+			'feed://example.com/',
+			'telnet://example.com',
+			'mms://example.com',
+			'rtsp://example.com/',
+			'svn://develop.svn.wordpress.org/trunk',
+			'tel:000-000-000',
+			'fax:000-000-000',
+			'xmpp:user@host?message',
+			'webcal://example.com',
+			'urn:org.wordpress',
+		);
+		foreach ( $valid_urls as $valid_url ) {
+			$url_setting = $setting->sanitize( array( 'url' => $valid_url ) );
+			$this->assertInternalType( 'array', $url_setting );
+			$this->assertEquals( $valid_url, $url_setting['url'] );
+		}
+
+		$invalid_urls = array(
+			'javascript:alert(1)',
+			'unknown://something.out-there',
+		);
+		foreach ( $invalid_urls as $invalid_url ) {
+			$url_setting = $setting->sanitize( array( 'url' => $invalid_url ) );
+			$this->assertInstanceOf( 'WP_Error', $url_setting );
+			$this->assertEquals( 'invalid_url', $url_setting->get_error_code() );
+		}
+
 		$unsanitized = array(
 			'object_id' => 'bad',
 			'object' => '<b>hello</b>',
@@ -479,7 +516,7 @@ class Test_WP_Customize_Nav_Menu_Item_Setting extends WP_UnitTestCase {
 			'position' => -123,
 			'type' => 'custom<b>',
 			'title' => '\o/ o\'o Hi<script>unfilteredHtml()</script>',
-			'url' => 'javascript:alert(1)',
+			'url' => '',
 			'target' => '" onclick="',
 			'attr_title' => '\o/ o\'o <b>bolded</b><script>unfilteredHtml()</script>',
 			'description' => '\o/ o\'o <b>Hello world</b><script>unfilteredHtml()</script>',
