diff --git a/core/includes/config.inc b/core/includes/config.inc
index 3591263..aa51559 100644
--- a/core/includes/config.inc
+++ b/core/includes/config.inc
@@ -4,6 +4,7 @@
 use Drupal\Core\Config\ConfigException;
 use Drupal\Core\Config\FileStorage;
 use Drupal\Core\Config\StorageInterface;
+use Symfony\Component\Yaml\Dumper;
 
 /**
  * @file
@@ -422,3 +423,43 @@ function config_get_entity_type_by_name($name) {
 function config_typed() {
   return drupal_container()->get('config.typed');
 }
+
+/**
+ * Return a formatted diff of a named config between two storages.
+ *
+ * @param Drupal\Core\Config\StorageInterface $source_storage
+ *   The storage to diff configuration from.
+ * @param Drupal\Core\Config\StorageInterface $target_storage
+ *   The storage to diff configuration to.
+ * @param string $name
+ *   The name of the configuration object to diff.
+ *
+ * @return core/lib/Drupal/Component/Diff
+ *   A formatted string showing the difference between the two storages.
+ *
+ * @todo Make renderer injectable
+ */
+function config_diff(StorageInterface $source_storage, StorageInterface $target_storage, $name) {
+  require_once DRUPAL_ROOT . '/core/lib/Drupal/Component/Diff/DiffEngine.php';
+
+  // The output should show configuration object differences formatted as YAML.
+  // But the configuration is not necessarily stored in files. Therefore, they
+  // need to be read and parsed, and lastly, dumped into YAML strings.
+  $dumper = new Dumper();
+  $dumper->setIndentation(2);
+
+  $source_data = explode("\n", $dumper->dump($source_storage->read($name), PHP_INT_MAX));
+  $target_data = explode("\n", $dumper->dump($target_storage->read($name), PHP_INT_MAX));
+
+  // Check for new or removed files.
+  if ($source_data === array('false')) {
+    // Added file.
+    $source_data === array(t('File added'));
+  }
+  if ($target_data === array('false')) {
+    // Deleted file.
+    $target_data = array(t('File removed'));
+  }
+
+  return new Diff($source_data, $target_data);
+}
diff --git a/core/includes/config.inc.orig b/core/includes/config.inc.orig
new file mode 100644
index 0000000..3591263
--- /dev/null
+++ b/core/includes/config.inc.orig
@@ -0,0 +1,424 @@
+<?php
+
+use Drupal\Core\Config\Config;
+use Drupal\Core\Config\ConfigException;
+use Drupal\Core\Config\FileStorage;
+use Drupal\Core\Config\StorageInterface;
+
+/**
+ * @file
+ * This is the API for configuration storage.
+ */
+
+/**
+ * Config import lock name used to prevent concurrent synchronizations.
+ */
+const CONFIG_IMPORT_LOCK = 'config_import';
+
+/**
+ * Installs the default configuration of a given extension.
+ *
+ * @param string $type
+ *   The extension type; e.g., 'module' or 'theme'.
+ * @param string $name
+ *   The name of the module or theme to install default configuration for.
+ */
+function config_install_default_config($type, $name) {
+  // Use the override free context for config importing so that any overrides do
+  // not change the data on import.
+  config_context_enter('config.context.free');
+
+  // If this module defines any ConfigEntity types then create an empty
+  // manifest file for each of them.
+  foreach (config_get_module_config_entities($name) as $entity_info) {
+    config('manifest.' . $entity_info['config_prefix'])->save();
+  }
+
+  $config_dir = drupal_get_path($type, $name) . '/config';
+  if (is_dir($config_dir)) {
+    $source_storage = new FileStorage($config_dir);
+    $target_storage = drupal_container()->get('config.storage');
+
+    // Ignore manifest files.
+    $config_changes = config_sync_get_changes($source_storage, $target_storage, FALSE);
+    if (empty($config_changes['create'])) {
+      return;
+    }
+
+    // Do not overwrite or delete pre-existing configuration.
+    $config_changes['change'] = array();
+    $config_changes['delete'] = array();
+    $remaining_changes = config_import_invoke_owner($config_changes, $source_storage, $target_storage);
+    config_sync_changes($remaining_changes, $source_storage, $target_storage);
+  }
+  // Exit the override free context.
+  config_context_leave();
+}
+
+/**
+ * Uninstalls the default configuration of a given extension.
+ *
+ * @param string $type
+ *   The extension type; e.g., 'module' or 'theme'.
+ * @param string $name
+ *   The name of the module or theme to install default configuration for.
+ */
+function config_uninstall_default_config($type, $name) {
+  $storage = drupal_container()->get('config.storage');
+  $config_names = $storage->listAll($name . '.');
+  foreach ($config_names as $config_name) {
+    config($config_name)->delete();
+  }
+
+  // If this module defines any ConfigEntity types, then delete the manifest
+  // file for each of them.
+  foreach (config_get_module_config_entities($name) as $entity_type) {
+    config('manifest.' . $entity_info['config_prefix'])->delete();
+  }
+}
+
+/**
+ * Gets configuration object names starting with a given prefix.
+ *
+ * @see Drupal\Core\Config\StorageInterface::listAll()
+ */
+function config_get_storage_names_with_prefix($prefix = '') {
+  return drupal_container()->get('config.storage')->listAll($prefix);
+}
+
+/**
+ * Retrieves a configuration object.
+ *
+ * This is the main entry point to the configuration API. Calling
+ * @code config('book.admin') @endcode will return a configuration object in
+ * which the book module can store its administrative settings.
+ *
+ * @param string $name
+ *   The name of the configuration object to retrieve. The name corresponds to
+ *   a configuration file. For @code config('book.admin') @endcode, the config
+ *   object returned will contain the contents of book.admin configuration file.
+ *
+ * @return Drupal\Core\Config\Config
+ *   A configuration object.
+ */
+function config($name) {
+  return drupal_container()->get('config.factory')->get($name);
+}
+
+/**
+ * Sets the config context on the config factory.
+ *
+ * This allows configuration objects to be created using special configuration
+ * contexts eg. global override free or locale using a user preferred language.
+ * Calling this function affects all subsequent calls to config() until
+ * config_context_leave() is called.
+ *
+ * @see config_context_leave()
+ * @see \Drupal\Core\Config\ConfigFactory
+ *
+ * @param string $context_name
+ *   The name of the config context service on the container or a fully
+ *   qualified class implementing \Drupal\Core\Config\Context\ContextInterface.
+ *
+ * @return \Drupal\Core\Config\Context\ContextInterface
+ *   The configuration context object.
+ */
+function config_context_enter($context_name) {
+  if (drupal_container()->has($context_name)) {
+    $context = drupal_container()->get($context_name);
+  }
+  elseif (class_exists($context_name) && in_array('Drupal\Core\Config\Context\ContextInterface', class_implements($context_name))) {
+    $context = drupal_container()
+      ->get('config.context.factory')
+      ->get($context_name);
+  }
+  else {
+    throw new ConfigException(sprintf('Unknown config context service or class: %s', $context_name));
+  }
+  drupal_container()
+    ->get('config.factory')
+    ->enterContext($context);
+  return $context;
+}
+
+/**
+ * Leaves the current config context returning to the previous context.
+ *
+ * @see config_context_enter()
+ * @see \Drupal\Core\Config\ConfigFactory
+ */
+function config_context_leave() {
+  drupal_container()
+    ->get('config.factory')
+    ->leaveContext();
+}
+
+/**
+ * Returns a list of differences between configuration storages.
+ *
+ * @param Drupal\Core\Config\StorageInterface $source_storage
+ *   The storage to synchronize configuration from.
+ * @param Drupal\Core\Config\StorageInterface $target_storage
+ *   The storage to synchronize configuration to.
+ * @param bool $use_manifest
+ *   (optional) Whether to determine changes based on manifest files. Defaults
+ *   to TRUE.
+ *
+ * @return array|bool
+ *   An associative array containing the differences between source and target
+ *   storage, or FALSE if there are no differences.
+ */
+function config_sync_get_changes(StorageInterface $source_storage, StorageInterface $target_storage, $use_manifest = TRUE) {
+  $config_changes = array(
+    'create' => array(),
+    'change' => array(),
+    'delete' => array(),
+  );
+  $all_source_names = $source_storage->listAll();
+  $all_target_names = $target_storage->listAll();
+
+  // Config entities maintain 'manifest' files that list the objects they
+  // are currently handling. Each file is a simple indexed array of config
+  // object names. In order to generate a list of objects that have been
+  // created or deleted we need to open these files in both the source and
+  // target storage, generate an array of the objects, and compare them.
+  if ($use_manifest) {
+    $source_config_data = array();
+    $target_config_data = array();
+    foreach ($source_storage->listAll('manifest') as $name) {
+      if ($source_manifest_data = $source_storage->read($name)) {
+        $source_config_data = array_merge($source_config_data, $source_manifest_data);
+      }
+
+      if ($target_manifest_data = $target_storage->read($name)) {
+        $target_config_data = array_merge($target_config_data, $target_manifest_data);
+      }
+    }
+
+    foreach (array_diff_key($target_config_data, $source_config_data) as $name => $value) {
+      $config_changes['delete'][] = $value['name'];
+    }
+
+    foreach (array_diff_key($source_config_data, $target_config_data) as $name => $value) {
+      $config_changes['create'][] = $value['name'];
+    }
+  }
+  else {
+    $config_changes['delete'] = array_diff($all_target_names, $all_source_names);
+    $config_changes['create'] = array_diff($all_source_names, $all_target_names);
+  }
+
+  foreach (array_intersect($all_source_names, $all_target_names) as $name) {
+    // Ignore manifest files
+    if (substr($name, 0, 9) != 'manifest.') {
+      $source_config_data = $source_storage->read($name);
+      $target_config_data = $target_storage->read($name);
+      if ($source_config_data !== $target_config_data) {
+        $config_changes['change'][] = $name;
+      }
+    }
+  }
+
+  // Do not trigger subsequent synchronization operations if there are no
+  // changes in any category.
+  if (empty($config_changes['create']) && empty($config_changes['change']) && empty($config_changes['delete'])) {
+    return FALSE;
+  }
+  return $config_changes;
+}
+
+/**
+ * Writes an array of config file changes from a source storage to a target storage.
+ *
+ * @param array $config_changes
+ *   An array of changes to be written.
+ * @param Drupal\Core\Config\StorageInterface $source_storage
+ *   The storage to synchronize configuration from.
+ * @param Drupal\Core\Config\StorageInterface $target_storage
+ *   The storage to synchronize configuration to.
+ */
+function config_sync_changes(array $config_changes, StorageInterface $source_storage, StorageInterface $target_storage) {
+  $target_context = drupal_container()->get('config.context.free');
+  $factory = drupal_container()->get('config.factory');
+  foreach (array('delete', 'create', 'change') as $op) {
+    foreach ($config_changes[$op] as $name) {
+      $config = new Config($name, $target_storage, $target_context);
+      if ($op == 'delete') {
+        $config->delete();
+      }
+      else {
+        $data = $source_storage->read($name);
+        $config->setData($data ? $data : array());
+        $config->save();
+      }
+      $factory->reset($name);
+    }
+  }
+}
+
+/**
+ * Imports configuration into the active configuration.
+ *
+ * @return bool|null
+ *   TRUE if configuration was imported successfully, FALSE in case of a
+ *   synchronization error, or NULL if there are no changes to synchronize.
+ */
+function config_import() {
+  // Retrieve a list of differences between staging and the active configuration.
+  $source_storage = drupal_container()->get('config.storage.staging');
+  $snapshot_storage = drupal_container()->get('config.storage.snapshot');
+  $target_storage = drupal_container()->get('config.storage');
+
+  $config_changes = config_sync_get_changes($source_storage, $target_storage);
+  if (empty($config_changes)) {
+    return;
+  }
+
+  if (!lock()->acquire(CONFIG_IMPORT_LOCK)) {
+    // Another request is synchronizing configuration.
+    // Return a negative result for UI purposes. We do not differentiate between
+    // an actual synchronization error and a failed lock, because concurrent
+    // synchronizations are an edge-case happening only when multiple developers
+    // or site builders attempt to do it without coordinating.
+    return FALSE;
+  }
+
+  $success = TRUE;
+  try {
+    // Use the override free context for config importing so that any overrides do
+    // not change the data on import.
+    config_context_enter('config.context.free');
+
+    $remaining_changes = config_import_invoke_owner($config_changes, $source_storage, $target_storage);
+    config_sync_changes($remaining_changes, $source_storage, $target_storage);
+    config_import_create_snapshot($target_storage, $snapshot_storage);
+
+    // Exit the override free context.
+    config_context_leave();
+  }
+  catch (ConfigException $e) {
+    watchdog_exception('config_import', $e);
+    $success = FALSE;
+  }
+  lock()->release(CONFIG_IMPORT_LOCK);
+
+  return $success;
+}
+
+/**
+ * Creates a configuration snapshot following a successful import.
+ *
+ * @param Drupal\Core\Config\StorageInterface $source_storage
+ *   The storage to synchronize configuration from.
+ * @param Drupal\Core\Config\StorageInterface $target_storage
+ *   The storage to synchronize configuration to.
+ */
+function config_import_create_snapshot(StorageInterface $source_storage, StorageInterface $snapshot_storage) {
+  $snapshot_storage->deleteAll();
+  foreach ($source_storage->listAll() as $name) {
+    $snapshot_storage->write($name, $source_storage->read($name));
+  }
+}
+
+/**
+ * Invokes MODULE_config_import() callbacks for configuration changes.
+ *
+ * @param array $config_changes
+ *   An array of changes to be loaded.
+ * @param Drupal\Core\Config\StorageInterface $source_storage
+ *   The storage to synchronize configuration from.
+ * @param Drupal\Core\Config\StorageInterface $target_storage
+ *   The storage to synchronize configuration to.
+ *
+ * @todo Add support for other extension types; e.g., themes etc.
+ */
+function config_import_invoke_owner(array $config_changes, StorageInterface $source_storage, StorageInterface $target_storage) {
+  $factory = drupal_container()->get('config.factory');
+  // Use the admin context for config importing so that any overrides do not
+  // change the data on import.
+  $free_context = drupal_container()->get('config.context.free');
+  // Allow modules to take over configuration change operations for
+  // higher-level configuration data.
+  // First pass deleted, then new, and lastly changed configuration, in order to
+  // handle dependencies correctly.
+  $manager = drupal_container()->get('plugin.manager.entity');
+  foreach (array('delete', 'create', 'change') as $op) {
+    foreach ($config_changes[$op] as $key => $name) {
+      // Call to the configuration entity's storage controller to handle the
+      // configuration change.
+      $handled_by_module = FALSE;
+      // Validate the configuration object name before importing it.
+      Config::validateName($name);
+      if ($entity_type = config_get_entity_type_by_name($name)) {
+        $old_config = new Config($name, $target_storage, $free_context);
+        $old_config->load();
+
+        $data = $source_storage->read($name);
+        $new_config = new Config($name, $source_storage, $free_context);
+        if ($data !== FALSE) {
+          $new_config->setData($data);
+        }
+
+        $method = 'import' . ucfirst($op);
+        $handled_by_module = $manager->getStorageController($entity_type)->$method($name, $new_config, $old_config);
+      }
+      if (!empty($handled_by_module)) {
+        $factory->reset($name);
+        // Reset the manifest config object for the config entity.
+        $entity_info = drupal_container()->get('plugin.manager.entity')->getDefinition($entity_type);
+        $factory->reset('manifest.' . $entity_info['config_prefix']);
+        unset($config_changes[$op][$key]);
+      }
+    }
+  }
+  return $config_changes;
+}
+
+/**
+ * Return a list of all config entity types provided by a module.
+ *
+ * @param string $module
+ *   The name of the module possibly providing config entities.
+ *
+ * @return array
+ *   An associative array containing the entity info for any config entities
+ *   provided by the requested module, keyed by the entity type.
+ */
+function config_get_module_config_entities($module) {
+  // While this is a lot of work to generate, it's not worth static caching
+  // since this function is only called at install/uninstall, and only
+  // once per module.
+  $info = entity_get_info();
+  return array_filter($info, function($entity_info) use ($module) {
+    return ($entity_info['module'] == $module) && is_subclass_of($entity_info['class'], 'Drupal\Core\Config\Entity\ConfigEntityInterface');
+  });
+}
+
+/**
+ * Returns the entity type of a configuration object.
+ *
+ * @param string $name
+ *   The configuration object name.
+ *
+ * @return string|null
+ *   Either the entity type name, or NULL if none match.
+ */
+function config_get_entity_type_by_name($name) {
+  $entities = array_filter(entity_get_info(), function($entity_info) use ($name) {
+    return (isset($entity_info['config_prefix']) && strpos($name, $entity_info['config_prefix'] . '.') === 0);
+  });
+  return key($entities);
+}
+
+/**
+ * Returns the typed config manager service.
+ *
+ * Use the typed data manager service for creating typed configuration objects.
+ *
+ * @see Drupal\Core\TypedData\TypedDataManager::create()
+ *
+ * @return Drupal\Core\TypedData\TypedConfigManager
+ */
+function config_typed() {
+  return drupal_container()->get('config.typed');
+}
diff --git a/core/lib/Drupal/Component/Diff/DiffEngine.php b/core/lib/Drupal/Component/Diff/DiffEngine.php
index 1236610..f426b96 100644
--- a/core/lib/Drupal/Component/Diff/DiffEngine.php
+++ b/core/lib/Drupal/Component/Diff/DiffEngine.php
@@ -1111,11 +1111,11 @@ function _end_diff() {
   function _block_header($xbeg, $xlen, $ybeg, $ylen) {
     return array(
       array(
-        'data' => theme('diff_header_line', array('lineno' => $xbeg + $this->line_stats['offset']['x'])),
+        'data' => $xbeg + $this->line_stats['offset']['x'],
         'colspan' => 2,
       ),
       array(
-        'data' => theme('diff_header_line', array('lineno' => $ybeg + $this->line_stats['offset']['y'])),
+        'data' => $ybeg + $this->line_stats['offset']['y'],
         'colspan' => 2,
       )
     );
@@ -1143,7 +1143,7 @@ function addedLine($line) {
         'class' => 'diff-marker',
       ),
       array(
-        'data' => theme('diff_content_line', array('line' => $line)),
+        'data' => $line,
         'class' => 'diff-context diff-addedline',
       )
     );
@@ -1159,7 +1159,7 @@ function deletedLine($line) {
         'class' => 'diff-marker',
       ),
       array(
-        'data' => theme('diff_content_line', array('line' => $line)),
+        'data' => $line,
         'class' => 'diff-context diff-deletedline',
       )
     );
@@ -1172,7 +1172,7 @@ function contextLine($line) {
     return array(
       '&nbsp;',
       array(
-        'data' => theme('diff_content_line', array('line' => $line)),
+        'data' => $line,
         'class' => 'diff-context',
       )
     );
@@ -1181,7 +1181,7 @@ function contextLine($line) {
   function emptyLine() {
     return array(
       '&nbsp;',
-      theme('diff_empty_line', array('line' => '&nbsp;')),
+      '&nbsp;',
     );
   }
 
diff --git a/core/modules/config/config.admin.inc b/core/modules/config/config.admin.inc
index 3813885..718c773 100644
--- a/core/modules/config/config.admin.inc
+++ b/core/modules/config/config.admin.inc
@@ -41,6 +41,7 @@ function config_admin_sync_form(array &$form, array &$form_state, StorageInterfa
     if (empty($config_files)) {
       continue;
     }
+
     // @todo A table caption would be more appropriate, but does not have the
     //   visual importance of a heading.
     $form[$config_change_type]['heading'] = array(
@@ -62,10 +63,24 @@ function config_admin_sync_form(array &$form, array &$form_state, StorageInterfa
     }
     $form[$config_change_type]['list'] = array(
       '#theme' => 'table',
-      '#header' => array('Name'),
+      '#header' => array('Name', 'Operations'),
     );
+
     foreach ($config_files as $config_file) {
-      $form[$config_change_type]['list']['#rows'][] = array($config_file);
+      $links['view_diff'] = array(
+        'title' => t('View differences'),
+        'href' => 'admin/config/development/sync/diff/' . $config_file,
+        'ajax' => array('dialog' => array('modal' =>TRUE, 'width' => '700px')),
+      );
+      $form[$config_change_type]['list']['#rows'][] = array(
+        'name' => $config_file,
+        'operations' => array(
+          'data' => array(
+            '#type' => 'operations',
+            '#links' => $links,
+          ),
+        ),
+      );
     }
   }
 }
@@ -116,3 +131,54 @@ function config_admin_import_form_submit($form, &$form_state) {
     drupal_set_message(t('The import failed due to an error. Any errors have been logged.'), 'error');
   }
 }
+
+/**
+ * Page callback: Shows diff of specificed configuration file.
+ *
+ * @param string $config_file
+ *   The name of the configuration file.
+ *
+ * @return string
+ *   Table showing a two-way diff between the active and staged configuration.
+ */
+function config_admin_diff_page($config_file) {
+  // Retrieve a list of differences between last known state and active store.
+  $source_storage = drupal_container()->get('config.storage.staging');
+  $target_storage = drupal_container()->get('config.storage');
+
+  // Add the CSS for the inline diff.
+  $output['#attached']['css'][] = drupal_get_path('module', 'system') . '/system.diff.css';
+
+  $output['title'] = array(
+    '#theme' => 'html_tag',
+    '#tag' => 'h3',
+    '#value' => t('View changes of @config_file', array('@config_file' => $config_file)),
+  );
+
+  $diff = config_diff($target_storage, $source_storage, $config_file);
+  $formatter = new DrupalDiffFormatter();
+  $formatter->show_header = FALSE;
+
+  $variables = array(
+    'header' => array(
+      array('data' => t('Old'), 'colspan' => '2'),
+      array('data' => t('New'), 'colspan' => '2'),
+    ),
+    'rows' => $formatter->format($diff),
+  );
+
+  $output['diff'] = array(
+    '#markup' => theme('table', $variables),
+  );
+
+  $output['back'] = array(
+    '#type' => 'link',
+    '#title' => "Back to 'Synchronize configuration' page.",
+    '#href' => 'admin/config/development/sync',
+    '#attributes' => array(
+      'class' => array('dialog-cancel'),
+    ),
+  );
+
+  return $output;
+}
diff --git a/core/modules/config/config.module b/core/modules/config/config.module
index f8a1874..e3b6027 100644
--- a/core/modules/config/config.module
+++ b/core/modules/config/config.module
@@ -48,6 +48,14 @@ function config_menu() {
     'access arguments' => array('synchronize configuration'),
     'file' => 'config.admin.inc',
   );
+  $items['admin/config/development/sync/diff/%'] = array(
+    'title' => 'Configuration file diff',
+    'description' => 'Diff between active and staged configuraiton.',
+    'page callback' => 'config_admin_diff_page',
+    'page arguments' => array(5),
+    'access arguments' => array('synchronize configuration'),
+    'file' => 'config.admin.inc',
+  );
   $items['admin/config/development/sync/import'] = array(
     'title' => 'Import',
     'type' => MENU_DEFAULT_LOCAL_TASK,
diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigDiffTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigDiffTest.php
new file mode 100644
index 0000000..09f126b
--- /dev/null
+++ b/core/modules/config/lib/Drupal/config/Tests/ConfigDiffTest.php
@@ -0,0 +1,89 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config\Tests\ConfigDiffTest.
+ */
+
+namespace Drupal\config\Tests;
+
+use Drupal\simpletest\DrupalUnitTestBase;
+
+/**
+ * Tests config snapshot creation and updating.
+ */
+class ConfigDiffTest extends DrupalUnitTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('config_test', 'system');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Diff functionality',
+      'description' => 'Calculating the difference between two sets of configuration.',
+      'group' => 'Configuration',
+    );
+  }
+
+  /**
+   * Tests calculating the difference between two sets of configuration.
+   */
+  function testDiff() {
+    $active = $this->container->get('config.storage');
+    $staging = $this->container->get('config.storage.staging');
+    $config_name = 'config_test.system';
+    $change_key = 'foo';
+    $remove_key = '404';
+    $add_key = 'biff';
+    $add_data = 'bangpow';
+    $change_data = 'foobar';
+    $original_data = array(
+      'foo' => 'bar',
+      '404' => 'herp',
+    );
+
+    // Install the default config.
+    config_install_default_config('module', 'config_test');
+
+    // Change a configuration value in staging.
+    $staging_data = $original_data;
+    $staging_data[$change_key] = $change_data;
+    $staging_data[$add_key] = $add_data;
+    $staging->write($config_name, $staging_data);
+
+    // Verify that the diff reflects a change.
+    $diff = config_diff($active, $staging, $config_name);
+    $this->assertEqual($diff->edits[0]->type, 'change', 'The first item in the diff is a change.');
+    $this->assertEqual($diff->edits[0]->orig[0], $change_key . ': ' . $original_data[$change_key], format_string("The active value for key '%change_key' is '%original_data'.", array('%change_key' => $change_key, '%original_data' => $original_data[$change_key])));
+    $this->assertEqual($diff->edits[0]->closing[0], $change_key . ': ' . $change_data, format_string("The staging value for key '%change_key' is '%change_data'.", array('%change_key' => $change_key, '%change_data' => $change_data)));
+
+    // Reset data back to original, and remove a key
+    $staging_data = $original_data;
+    unset($staging_data[$remove_key]);
+    $staging->write($config_name, $staging_data);
+
+    // Verify that the diff reflects a removed key.
+    $diff = config_diff($active, $staging, $config_name);
+    $this->assertEqual($diff->edits[0]->type, 'copy', 'The first item in the diff is a copy.');
+    $this->assertEqual($diff->edits[1]->type, 'delete', 'The second item in the diff is a delete.');
+    $this->assertEqual($diff->edits[1]->orig[0], $remove_key . ': ' . $original_data[$remove_key], format_string("The active value for key '%remove_key' is '%original_data'.", array('%remove_key' => $remove_key, '%original_data' => $original_data[$remove_key])));
+    $this->assertFalse($diff->edits[1]->closing, format_string("The key '%remove_key' does not exist in staging.", array('%remove_key' => $remove_key)));
+
+    // Reset data back to original and add a key
+    $staging_data = $original_data;
+    $staging_data[$add_key] = $add_data;
+    $staging->write($config_name, $staging_data);
+
+    // Verify that the diff reflects an added key.
+    $diff = config_diff($active, $staging, $config_name);
+    $this->assertEqual($diff->edits[0]->type, 'copy', 'The first item in the diff is a copy.');
+    $this->assertEqual($diff->edits[1]->type, 'add', 'The second item in the diff is an add.');
+    $this->assertFalse($diff->edits[1]->orig, format_string("The key '%add_key' does not exist in active.", array('%add_key' => $add_key)));
+    $this->assertEqual($diff->edits[1]->closing[0], $add_key . ': ' . $add_data, format_string("The staging value for key '%add_key' is '%add_data'.", array('%add_key' => $add_key, '%add_data' => $add_data)));
+  }
+
+}
\ No newline at end of file
diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php
index 7900b7b..3788d67 100644
--- a/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php
+++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php
@@ -126,6 +126,49 @@ function testImportLock() {
     $this->assertNotEqual($new_site_name, config('system.site')->get('name'));
   }
 
+  /**
+   * Tests the screen that shows differences between active and staging.
+   */
+  function testImportDiff() {
+    $active = $this->container->get('config.storage');
+    $staging = $this->container->get('config.storage.staging');
+    $config_name = 'config_test.system';
+    $change_key = 'foo';
+    $remove_key = '404';
+    $add_key = 'biff';
+    $add_data = 'bangpow';
+    $change_data = 'foobar';
+    $original_data = array(
+      'foo' => 'bar',
+      '404' => 'herp',
+    );
+
+    // Change a configuration value in staging.
+    $staging_data = $original_data;
+    $staging_data[$change_key] = $change_data;
+    $staging_data[$add_key] = $add_data;
+    $staging->write($config_name, $staging_data);
+
+    // Load the diff UI and verify that the diff reflects the change.
+    $this->drupalGet('admin/config/development/sync/diff/' . $config_name);
+
+    // Reset data back to original, and remove a key
+    $staging_data = $original_data;
+    unset($staging_data[$remove_key]);
+    $staging->write($config_name, $staging_data);
+
+    // Load the diff UI and verify that the diff reflects a removed key.
+    $this->drupalGet('admin/config/development/sync/diff/' . $config_name);
+
+    // Reset data back to original and add a key
+    $staging_data = $original_data;
+    $staging_data[$add_key] = $add_data;
+    $staging->write($config_name, $staging_data);
+
+    // Load the diff UI and verify that the diff reflects an added key.
+    $this->drupalGet('admin/config/development/sync/diff/' . $config_name);
+  }
+
   function prepareSiteNameUpdate($new_site_name) {
     $staging = $this->container->get('config.storage.staging');
     // Create updated configuration object.
diff --git a/core/modules/system/system.diff.css b/core/modules/system/system.diff.css
new file mode 100644
index 0000000..1c73598
--- /dev/null
+++ b/core/modules/system/system.diff.css
@@ -0,0 +1,83 @@
+/**
+ * Inline diff metadata
+ */
+.diff-inline-metadata {
+  padding:4px;
+  border:1px solid #ddd;
+  background:#fff;
+  margin:0px 0px 10px;
+}
+
+.diff-inline-legend { font-size:11px; }
+
+.diff-inline-legend span,
+.diff-inline-legend label { margin-right:5px; }
+
+/**
+ * Inline diff markup
+ */
+span.diff-deleted { color:#ccc; }
+span.diff-deleted img { border: solid 2px #ccc; }
+span.diff-changed { background:#ffb; }
+span.diff-changed img { border:solid 2px #ffb; }
+span.diff-added { background:#cfc; }
+span.diff-added img { border: solid 2px #cfc; }
+
+/**
+ * Traditional split diff theming
+ */
+table.diff {
+  border-spacing: 4px;
+  margin-bottom: 20px;
+  table-layout: fixed;
+  width: 100%;
+}
+table.diff tr.even, table.diff tr.odd {
+  background-color: inherit;
+  border: none;
+}
+td.diff-prevlink {
+  text-align: left;
+}
+td.diff-nextlink {
+  text-align: right;
+}
+td.diff-section-title, div.diff-section-title {
+  background-color: #f0f0ff;
+  font-size: 0.83em;
+  font-weight: bold;
+  padding: 0.1em 1em;
+}
+td.diff-context {
+    background-color: #fafafa;
+}
+td.diff-deletedline {
+  background-color: #ffa;
+  width: 50%;
+}
+td.diff-addedline {
+  background-color: #afa;
+  width: 50%;
+}
+span.diffchange {
+  color: #f00;
+  font-weight: bold;
+}
+
+table.diff col.diff-marker {
+  width: 1.4em;
+}
+table.diff col.diff-content {
+  width: 50%;
+}
+table.diff th {
+  padding-right: inherit;
+}
+table.diff td div {
+  overflow: auto;
+  padding: 0.1ex 0.5em;
+  word-wrap: break-word;
+}
+table.diff td {
+  padding: 0.1ex 0.4em;
+}
