diff --git a/core/includes/ajax.inc b/core/includes/ajax.inc
index 1edd871..c67e981 100644
--- a/core/includes/ajax.inc
+++ b/core/includes/ajax.inc
@@ -473,8 +473,13 @@ function ajax_prepare_response($page_callback_result) {
     // manipulation method is used. The method used is specified by
     // #ajax['method']. The default method is 'replaceWith', which completely
     // replaces the old wrapper element and its content with the new HTML.
+    // Since this is the primary response content returned to the client, we
+    // also attach the page title. It is up to client code to determine if and
+    // how to display that. For example, if the requesting element is configured
+    // to display the response in a dialog (via #ajax['dialog']), it can use
+    // this for the dialog title.
     $html = is_string($page_callback_result) ? $page_callback_result : drupal_render($page_callback_result);
-    $commands[] = ajax_command_insert(NULL, $html);
+    $commands[] = ajax_command_insert(NULL, $html) + array('title' => drupal_get_title());
     // Add the status messages inside the new content's wrapper element, so that
     // on subsequent Ajax requests, it is treated as old content.
     $commands[] = ajax_command_prepend(NULL, theme('status_messages'));
@@ -531,8 +536,8 @@ function ajax_pre_render_element($element) {
   // Initialize #ajax_processed, so we do not process this element again.
   $element['#ajax_processed'] = FALSE;
 
-  // Nothing to do if there is neither a callback nor a path.
-  if (!(isset($element['#ajax']['callback']) || isset($element['#ajax']['path']))) {
+  // Nothing to do if there are no Ajax settings.
+  if (empty($element['#ajax'])) {
     return $element;
   }
 
@@ -589,12 +594,24 @@ function ajax_pre_render_element($element) {
   if (isset($element['#ajax']['event'])) {
     $element['#attached']['library'][] = array('system', 'jquery.form');
     $element['#attached']['library'][] = array('system', 'drupal.ajax');
+    if (!empty($element['#ajax']['dialog'])) {
+      $element['#attached']['library'][] = array('system', 'drupal.dialog');
+    }
 
     $settings = $element['#ajax'];
 
-    // Assign default settings.
+    // Assign default settings. When 'path' is set to NULL, ajax.js submits the
+    // Ajax request to the same URL as the form or link destination is for
+    // someone with JavaScript disabled. This is generally preferred as a way to
+    // ensure consistent server processing for js and no-js users, and Drupal's
+    // content negotiation takes care of formatting the response appropriately.
+    // However, 'path' and 'options' may be set when wanting server processing
+    // to be substantially different for a JavaScript triggered submission.
+    // One such substantial difference is form elements that use
+    // #ajax['callback'] for determining which part of the form needs
+    // re-rendering. For that, we have a special 'system/ajax' route.
     $settings += array(
-      'path' => 'system/ajax',
+      'path' => isset($settings['callback']) ? 'system/ajax' : NULL,
       'options' => array(),
     );
 
@@ -604,7 +621,7 @@ function ajax_pre_render_element($element) {
     }
 
     // Change path to URL.
-    $settings['url'] = url($settings['path'], $settings['options']);
+    $settings['url'] = isset($settings['path']) ? url($settings['path'], $settings['options']) : NULL;
     unset($settings['path'], $settings['options']);
 
     // Add special data to $settings['submit'] so that when this element
diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index 3ce399b..c10a3ac 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -1739,8 +1739,24 @@ function theme_links($variables) {
         if ($is_current_path && $is_current_language) {
           $class[] = 'active';
         }
-        // Pass in $link as $options, they share the same keys.
-        $item = l($link['title'], $link['href'], $link);
+        // @todo Reconcile Views usage of 'ajax' as a boolean with the rest of
+        //   core's usage of it as an array.
+        if (isset($link['ajax']) && is_array($link['ajax'])) {
+          // To attach Ajax behavior, render a link element, rather than just
+          // call l().
+          $link_element = array(
+            '#type' => 'link',
+            '#title' => $link['title'],
+            '#href' => $link['href'],
+            '#ajax' => $link['ajax'],
+            '#options' => array_diff_key($link, drupal_map_assoc(array('title', 'href', 'ajax'))),
+          );
+          $item = drupal_render($link_element);
+        }
+        else {
+          // Pass in $link as $options, they share the same keys.
+          $item = l($link['title'], $link['href'], $link);
+        }
       }
       // Handle title-only text items.
       else {
diff --git a/core/misc/ajax.js b/core/misc/ajax.js
index 58db308..4a1e327 100644
--- a/core/misc/ajax.js
+++ b/core/misc/ajax.js
@@ -101,7 +101,6 @@ Drupal.behaviors.AJAX = {
  */
 Drupal.ajax = function (base, element, element_settings) {
   var defaults = {
-    url: 'system/ajax',
     event: 'mousedown',
     keypress: true,
     selector: '#' + base,
@@ -119,9 +118,55 @@ Drupal.ajax = function (base, element, element_settings) {
 
   $.extend(this, defaults, element_settings);
 
+  // @todo Remove this after refactoring the PHP code to:
+  //   - Call this 'selector'.
+  //   - Include the '#' for ID-based selectors.
+  //   - Support non-ID-based selectors.
+  if (this.wrapper) {
+    this.wrapper = '#' + this.wrapper;
+  }
+
+  // For Ajax responses that are wanted in a dialog, use the needed method.
+  // If wanted in a modal dialog, also use the needed wrapper.
+  if (this.dialog) {
+    this.method = 'html';
+    if (this.dialog.modal) {
+      this.wrapper = '#drupal-modal';
+    }
+  }
+
   this.element = element;
   this.element_settings = element_settings;
 
+  // If there isn't a form, jQuery.ajax() will be used instead, allowing us to
+  // bind Ajax to links as well.
+  if (this.element.form) {
+    this.form = $(this.element.form);
+  }
+
+  // If no Ajax callback URL was given, use the link href or form action.
+  if (!this.url) {
+    if ($(element).is('a')) {
+      this.url = $(element).attr('href');
+    }
+    else if (element.form) {
+      this.url = this.form.attr('action');
+
+      // @todo If there's a file input on this form, then jQuery will submit the
+      //   AJAX response with a hidden Iframe rather than the XHR object. If the
+      //   response to the submission is an HTTP redirect, then the Iframe will
+      //   follow it, but the server won't content negotiate it correctly,
+      //   because there won't be an ajax_iframe_upload POST variable. Until we
+      //   figure out a work around to this problem, we prevent AJAX-enabling
+      //   elements that submit to the same URL as the form when there's a file
+      //   input. For example, this means the Delete button on the edit form of
+      //   an Article node doesn't open its confirmation form in a dialog.
+      if (this.form.find(':file').length) {
+        return;
+      }
+    }
+  }
+
   // Replacing 'nojs' with 'ajax' in the URL allows for an easy method to let
   // the server detect when it needs to degrade gracefully.
   // There are four scenarios to check for:
@@ -129,14 +174,7 @@ Drupal.ajax = function (base, element, element_settings) {
   // 2. /nojs$ - The end of a URL string.
   // 3. /nojs? - Followed by a query (e.g. path/nojs?destination=foobar).
   // 4. /nojs# - Followed by a fragment (e.g.: path/nojs#myfragment).
-  this.url = element_settings.url.replace(/\/nojs(\/|$|\?|#)/g, '/ajax$1');
-  this.wrapper = '#' + element_settings.wrapper;
-
-  // If there isn't a form, jQuery.ajax() will be used instead, allowing us to
-  // bind Ajax to links as well.
-  if (this.element.form) {
-    this.form = $(this.element.form);
-  }
+  this.url = this.url.replace(/\/nojs(\/|$|\?|#)/g, '/ajax$1');
 
   // Set the options for the ajaxSubmit function.
   // The 'this' variable will not persist inside of the options object.
@@ -520,6 +558,18 @@ Drupal.ajax.prototype.commands = {
     // Add the new content to the page.
     wrapper[method](new_content);
 
+    // If the requesting object wanted the response in a dialog, open that
+    // dialog. However, a single server response can include multiple insert
+    // commands (e.g., one for the primary content and another one for status
+    // messages), but we only want to open the dialog once, so we assume that
+    // only commands with a title property are dialog eligible.
+    // @todo Consider whether this is overloading title inappropriately, and
+    //   if so, find another way to determine dialog eligibility.
+    if (ajax.dialog && ('title' in response)) {
+      var dialog = Drupal.dialog(wrapper, {title: response.title});
+      ajax.dialog.modal ? dialog.showModal() : dialog.show();
+    }
+
     // Immediately hide the new content if we're using any effects.
     if (effect.showEffect !== 'show') {
       new_content.hide();
diff --git a/core/misc/dialog.js b/core/misc/dialog.js
new file mode 100644
index 0000000..c0bccf1
--- /dev/null
+++ b/core/misc/dialog.js
@@ -0,0 +1,85 @@
+/**
+ * @file
+ *
+ * Dialog API inspired by HTML5 dialog element:
+ * http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#the-dialog-element
+ */
+(function ($, Drupal, drupalSettings) {
+
+"use strict";
+
+drupalSettings.dialog = {
+  autoOpen: true,
+  dialogClass: '',
+  close: function (e) {
+    Drupal.detachBehaviors(e.target, null, 'unload');
+  }
+};
+
+Drupal.behaviors.dialog = {
+  attach: function () {
+    // Provide a known 'drupal-modal' dom element for Drupal code to use for
+    // modal dialogs. Since there can be multiple non-modal dialogs at a time,
+    // it is the responsibility of calling code to create the elements it needs.
+    if (!$('#drupal-modal').length) {
+      $('<div id="drupal-modal" />').hide().appendTo('body');
+    }
+  }
+};
+
+Drupal.dialog = function (element, options) {
+
+  function openDialog (settings) {
+    settings = $.extend(settings, defaults);
+    // Trigger a global event to allow scripts to bind events to the dialog.
+    $(window).trigger('dialog:beforecreate', [dialog, $element, settings]);
+    $element.dialog(settings);
+    dialog.open = true;
+    $(window).trigger('dialog:aftercreate', [dialog, $element, settings]);
+  }
+
+  function closeDialog (value) {
+    $(window).trigger('dialog:beforeclose', [dialog, $element]);
+    $element.dialog('close');
+    dialog.returnValue = value;
+    dialog.open = false;
+    $(window).trigger('dialog:afterclose', [dialog, $element]);
+  }
+
+  var undef;
+  var $element = $(element);
+  var defaults = $.extend(options, drupalSettings.dialog);
+  var dialog = {
+    open: false,
+    returnValue: undef,
+    show: function () {
+      openDialog({modal: false});
+    },
+    showModal: function () {
+      openDialog({modal: true});
+    },
+    close: closeDialog
+  };
+
+  return dialog;
+};
+
+/**
+ * Binds a listener on dialog creation to handle the cancel link.
+ */
+$(window).on('dialog:aftercreate', function (e, dialog, $element, settings) {
+  $element.on('click.dialog', '.dialog-cancel', function (e) {
+    dialog.close('cancel');
+    e.preventDefault();
+    e.stopPropagation();
+  });
+});
+
+/**
+ * Removes all 'dialog' listeners.
+ */
+$(window).on('dialog:beforeclose', function (e, dialog, $element) {
+  $element.off('.dialog');
+});
+
+})(jQuery, Drupal, drupalSettings);
diff --git a/core/modules/system/lib/Drupal/system/Tests/Ajax/DialogTest.php b/core/modules/system/lib/Drupal/system/Tests/Ajax/DialogTest.php
new file mode 100644
index 0000000..d7d69da
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Ajax/DialogTest.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\system\Tests\Ajax\DialogTest.
+ */
+
+namespace Drupal\system\Tests\Ajax;
+
+/**
+ * Tests use of dialogs as wrappers for Ajax responses.
+ */
+class DialogTest extends AjaxTestBase {
+  public static function getInfo() {
+    return array(
+      'name' => 'Dialog',
+      'description' => 'Performs tests on #ajax[\'dialog\'].',
+      'group' => 'AJAX',
+    );
+  }
+
+  /**
+   * Ensure elements with #ajax['dialog'] render correctly.
+   */
+  function testDialog() {
+    // Ensure the elements render without notices or exceptions.
+    $this->drupalGet('ajax-test/dialog');
+
+    // @todo What else should we assert?
+  }
+
+}
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 950fee6..2826006 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -1171,6 +1171,21 @@ function system_library_info() {
     ),
   );
 
+  // Drupal's dialog component.
+  $libraries['drupal.dialog'] = array(
+    'title' => 'Drupal Dialog',
+    'version' => VERSION,
+    'js' => array(
+      'core/misc/dialog.js' => array('group' => JS_LIBRARY),
+    ),
+    'dependencies' => array(
+      array('system', 'jquery'),
+      array('system', 'drupal'),
+      array('system', 'drupalSettings'),
+      array('system', 'jquery.ui.dialog')
+    ),
+  );
+
   // Drupal's states library.
   $libraries['drupal.states'] = array(
     'title' => 'Drupal states',
diff --git a/core/modules/system/tests/modules/ajax_test/ajax_test.module b/core/modules/system/tests/modules/ajax_test/ajax_test.module
index c969d45..efb1966 100644
--- a/core/modules/system/tests/modules/ajax_test/ajax_test.module
+++ b/core/modules/system/tests/modules/ajax_test/ajax_test.module
@@ -26,6 +26,16 @@ function ajax_test_menu() {
     'page callback' => 'ajax_test_link',
     'access callback' => TRUE,
   );
+  $items['ajax-test/dialog'] = array(
+    'title' => 'AJAX Dialog',
+    'page callback' => 'ajax_test_dialog',
+    'access callback' => TRUE,
+  );
+  $items['ajax-test/dialog-contents'] = array(
+    'title' => 'AJAX Dialog contents',
+    'page callback' => 'ajax_test_dialog_contents',
+    'access callback' => TRUE,
+  );
   return $items;
 }
 
@@ -74,3 +84,100 @@ function ajax_test_link() {
   );
   return $build;
 }
+
+/**
+ * Menu callback: Renders a form elements and links with #ajax['dialog'].
+ */
+function ajax_test_dialog() {
+  // Add two wrapper elements for testing non-modal dialogs. Modal dialogs use
+  // the global drupal-modal wrapper by default.
+  $build['dialog_wrappers'] = array('#markup' => '<div id="ajax-test-dialog-wrapper-1"></div><div id="ajax-test-dialog-wrapper-2"></div>');
+
+  // Dialog behavior applied to a button.
+  $build['form'] = drupal_get_form('ajax_test_dialog_form');
+
+  // Dialog behavior applied to a #type => 'link'.
+  $build['link'] = array(
+    '#type' => 'link',
+    '#title' => 'Link 1 (modal)',
+    '#href' => 'ajax-test/dialog-contents',
+    '#ajax' => array(
+      'dialog' => array('modal' => TRUE),
+    ),
+  );
+
+  // Dialog behavior applied to links rendered by theme_links().
+  $build['links'] = array(
+    '#theme' => 'links',
+    '#links' => array(
+      'link2' => array(
+        'title' => 'Link 2 (modal)',
+        'href' => 'ajax-test/dialog-contents',
+        'ajax' => array(
+          'dialog' => array('modal' => TRUE),
+         ),
+      ),
+      'link3' => array(
+        'title' => 'Link 3 (non-modal)',
+        'href' => 'ajax-test/dialog-contents',
+        'ajax' => array(
+          'dialog' => array(),
+          'wrapper' => 'ajax-test-dialog-wrapper-2',
+        ),
+      ),
+    ),
+  );
+  return $build;
+}
+
+/**
+ * Form builder: Renders buttons with #ajax['dialog'].
+ */
+function ajax_test_dialog_form($form, &$form_state) {
+  $form['button1'] = array(
+    '#type' => 'submit',
+    '#value' => 'Button 1 (modal)',
+    '#ajax' => array(
+      'dialog' => array('modal' => TRUE),
+    ),
+  );
+  $form['button2'] = array(
+    '#type' => 'submit',
+    '#value' => 'Button 2 (non-modal)',
+    '#ajax' => array(
+      'dialog' => array(),
+      'wrapper' => 'ajax-test-dialog-wrapper-1',
+    ),
+  );
+  return $form;
+}
+
+/**
+ * Form submit handler for ajax_test_dialog_form().
+ */
+function ajax_test_dialog_form_submit($form, &$form_state) {
+  $form_state['redirect'] = 'ajax-test/dialog-contents';
+}
+
+/**
+ * Menu callback: Returns the contents for dialogs opened by ajax_test_dialog().
+ */
+function ajax_test_dialog_contents() {
+  // This is a regular render array; the keys do not have special meaning.
+  return array(
+    'content' => array(
+      '#markup' => 'Example message',
+    ),
+    'cancel'=> array(
+      '#type' => 'link',
+      '#title' => 'Cancel',
+      '#href' => '',
+      '#attributes' => array(
+        // This is a special class to which JavaScript assigns dialog closing
+        // behavior.
+        'class' => array('dialog-cancel'),
+      ),
+    ),
+  );
+}
+
