diff --git a/core/modules/ckeditor/ckeditor.libraries.yml b/core/modules/ckeditor/ckeditor.libraries.yml
index df6881b37ae721d3f9c8fb59f5002c37036c281c..27b44afb1442309a4e6f1ea22f150951f9078673 100644
--- a/core/modules/ckeditor/ckeditor.libraries.yml
+++ b/core/modules/ckeditor/ckeditor.libraries.yml
@@ -2,6 +2,7 @@ drupal.ckeditor:
   version: VERSION
   js:
     js/ckeditor.js: {}
+    js/ckeditor.off-canvas-css-reset.js: {}
   css:
     state:
       css/ckeditor.css: {}
diff --git a/core/modules/ckeditor/js/ckeditor.off-canvas-css-reset.es6.js b/core/modules/ckeditor/js/ckeditor.off-canvas-css-reset.es6.js
new file mode 100644
index 0000000000000000000000000000000000000000..e1d0324a44de9cba74286be68a10d5f03c366b4b
--- /dev/null
+++ b/core/modules/ckeditor/js/ckeditor.off-canvas-css-reset.es6.js
@@ -0,0 +1,107 @@
+/**
+ * @file
+ * Provides styles for CKEditor inside off-canvas dialogs.
+ */
+
+(($, CKEDITOR) => {
+  /**
+   * Takes a string of CKEditor CSS and modifies it for use in off-canvas.
+   *
+   * @param {string} originalCss
+   *   The CSS rules from CKEditor.
+   * @return {string}
+   *   The rules from originalCss with extra specificity for off-canvas.
+   */
+  const convertToOffCanvasCss = originalCss => {
+    const selectorPrefix = '#drupal-off-canvas ';
+    const skinPath = `${CKEDITOR.basePath}${CKEDITOR.skinName}/`;
+    const css = originalCss
+      .substring(originalCss.indexOf('*/') + 2)
+      .trim()
+      .replace(/}/g, `}${selectorPrefix}`)
+      .replace(/,/g, `,${selectorPrefix}`)
+      .replace(/url\(/g, skinPath);
+    return `${selectorPrefix}${css}`;
+  };
+
+  /**
+   * Inserts CSS rules into DOM.
+   *
+   * @param {string} cssToInsert
+   *   CSS rules to be inserted
+   */
+  const insertCss = cssToInsert => {
+    const offCanvasCss = document.createElement('style');
+    offCanvasCss.innerHTML = cssToInsert;
+    offCanvasCss.setAttribute('id', 'ckeditor-off-canvas-reset');
+    document.body.appendChild(offCanvasCss);
+  };
+
+  /**
+   * Adds CSS so CKEditor is styled properly in off-canvas.
+   */
+  const addCkeditorOffCanvasCss = () => {
+    // If #ckeditor-off-canvas-reset exists, this has already run.
+    if (document.getElementById('ckeditor-off-canvas-reset')) {
+      return;
+    }
+    // CKEDITOR.skin.getPath() requires the CKEDITOR.skinName property.
+    // @see https://stackoverflow.com/a/17336982
+    CKEDITOR.skinName = CKEDITOR.skin.name;
+
+    // Get the paths to the css CKEditor is using.
+    const editorCssPath = CKEDITOR.skin.getPath('editor');
+    const dialogCssPath = CKEDITOR.skin.getPath('dialog');
+
+    // The key for cached CSS in localStorage is based on the CSS paths.
+    const storedOffCanvasCss = window.localStorage.getItem(
+      `Drupal.off-canvas.css.${editorCssPath}${dialogCssPath}`,
+    );
+
+    // See if CSS is cached in localStorage, and use that when available.
+    if (storedOffCanvasCss) {
+      insertCss(storedOffCanvasCss);
+      return;
+    }
+
+    // If CSS unavailable in localStorage, get the files via AJAX and parse.
+    $.when($.get(editorCssPath), $.get(dialogCssPath)).done(
+      (editorCss, dialogCss) => {
+        const offCanvasEditorCss = convertToOffCanvasCss(editorCss[0]);
+        const offCanvasDialogCss = convertToOffCanvasCss(dialogCss[0]);
+        const cssToInsert = `#drupal-off-canvas .cke_inner * {background: transparent;}
+          ${offCanvasEditorCss}
+          ${offCanvasDialogCss}`;
+        insertCss(cssToInsert);
+
+        // The localStorage key for accessing the cached CSS is based on the
+        // paths of the CKEditor CSS files. This prevents localStorage from
+        // providing outdated CSS. If new files are used due to using a new
+        // skin, a new localStorage key is created.
+        //
+        // The CSS paths also include the cache-busting query string that is
+        // stored in state and CKEDITOR.timestamp. This query string changes on
+        // update and cache clear  and prevents localStorage from providing
+        // stale CKEditor CSS.
+        //
+        // Before adding the CSS rules to localStorage, there is a check that
+        // confirms the cache-busting query (CKEDITOR.timestamp) is in the CSS
+        // paths. This prevents localStorage from caching something unbustable.
+        //
+        // @see ckeditor_library_info_alter()
+        if (
+          CKEDITOR.timestamp &&
+          editorCssPath.indexOf(CKEDITOR.timestamp) !== -1 &&
+          dialogCssPath.indexOf(CKEDITOR.timestamp) !== -1
+        ) {
+          window.localStorage.setItem(
+            `Drupal.off-canvas.css.${editorCssPath}${dialogCssPath}`,
+            cssToInsert,
+          );
+        }
+      },
+    );
+  };
+
+  addCkeditorOffCanvasCss();
+})(jQuery, CKEDITOR);
diff --git a/core/modules/ckeditor/js/ckeditor.off-canvas-css-reset.js b/core/modules/ckeditor/js/ckeditor.off-canvas-css-reset.js
new file mode 100644
index 0000000000000000000000000000000000000000..c0738bb08f405bae98dd3f68ea3b7854f87ed0c0
--- /dev/null
+++ b/core/modules/ckeditor/js/ckeditor.off-canvas-css-reset.js
@@ -0,0 +1,53 @@
+/**
+* DO NOT EDIT THIS FILE.
+* See the following change record for more information,
+* https://www.drupal.org/node/2815083
+* @preserve
+**/
+
+(function ($, CKEDITOR) {
+  var convertToOffCanvasCss = function convertToOffCanvasCss(originalCss) {
+    var selectorPrefix = '#drupal-off-canvas ';
+    var skinPath = '' + CKEDITOR.basePath + CKEDITOR.skinName + '/';
+    var css = originalCss.substring(originalCss.indexOf('*/') + 2).trim().replace(/}/g, '}' + selectorPrefix).replace(/,/g, ',' + selectorPrefix).replace(/url\(/g, skinPath);
+    return '' + selectorPrefix + css;
+  };
+
+  var insertCss = function insertCss(cssToInsert) {
+    var offCanvasCss = document.createElement('style');
+    offCanvasCss.innerHTML = cssToInsert;
+    offCanvasCss.setAttribute('id', 'ckeditor-off-canvas-reset');
+    document.body.appendChild(offCanvasCss);
+  };
+
+  var addCkeditorOffCanvasCss = function addCkeditorOffCanvasCss() {
+    if (document.getElementById('ckeditor-off-canvas-reset')) {
+      return;
+    }
+
+    CKEDITOR.skinName = CKEDITOR.skin.name;
+
+    var editorCssPath = CKEDITOR.skin.getPath('editor');
+    var dialogCssPath = CKEDITOR.skin.getPath('dialog');
+
+    var storedOffCanvasCss = window.localStorage.getItem('Drupal.off-canvas.css.' + editorCssPath + dialogCssPath);
+
+    if (storedOffCanvasCss) {
+      insertCss(storedOffCanvasCss);
+      return;
+    }
+
+    $.when($.get(editorCssPath), $.get(dialogCssPath)).done(function (editorCss, dialogCss) {
+      var offCanvasEditorCss = convertToOffCanvasCss(editorCss[0]);
+      var offCanvasDialogCss = convertToOffCanvasCss(dialogCss[0]);
+      var cssToInsert = '#drupal-off-canvas .cke_inner * {background: transparent;}\n          ' + offCanvasEditorCss + '\n          ' + offCanvasDialogCss;
+      insertCss(cssToInsert);
+
+      if (CKEDITOR.timestamp && editorCssPath.indexOf(CKEDITOR.timestamp) !== -1 && dialogCssPath.indexOf(CKEDITOR.timestamp) !== -1) {
+        window.localStorage.setItem('Drupal.off-canvas.css.' + editorCssPath + dialogCssPath, cssToInsert);
+      }
+    });
+  };
+
+  addCkeditorOffCanvasCss();
+})(jQuery, CKEDITOR);
\ No newline at end of file
diff --git a/core/modules/ckeditor/tests/modules/ckeditor_test.routing.yml b/core/modules/ckeditor/tests/modules/ckeditor_test.routing.yml
index 833edc6a3bca35d3a1398d08ab9bffcd52f74ad3..01aeca0e2dfb9da71ad845701094109b105d0b0a 100644
--- a/core/modules/ckeditor/tests/modules/ckeditor_test.routing.yml
+++ b/core/modules/ckeditor/tests/modules/ckeditor_test.routing.yml
@@ -5,3 +5,10 @@ ckeditor_test.ajax_css:
     _form: '\Drupal\ckeditor_test\Form\AjaxCssForm'
   requirements:
     _access: 'TRUE'
+
+ckeditor_test.off_canvas:
+  path: '/ckeditor_test/off_canvas'
+  defaults:
+    _controller: '\Drupal\ckeditor_test\CkeditorOffCanvasTestController::testOffCanvas'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/modules/ckeditor/tests/modules/src/CkeditorOffCanvasTestController.php b/core/modules/ckeditor/tests/modules/src/CkeditorOffCanvasTestController.php
new file mode 100644
index 0000000000000000000000000000000000000000..7f02ced45a2cddc00956bffa5f0625c379014f08
--- /dev/null
+++ b/core/modules/ckeditor/tests/modules/src/CkeditorOffCanvasTestController.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Drupal\ckeditor_test;
+
+use Drupal\Core\Url;
+
+/**
+ * Provides controller for testing CKEditor in off-canvas dialogs.
+ */
+class CkeditorOffCanvasTestController {
+
+  /**
+   * Returns a link that can open a node add form in an off-canvas dialog.
+   *
+   * @return array
+   *   A render array.
+   */
+  public function testOffCanvas() {
+    $build['link'] = [
+      '#type' => 'link',
+      '#title' => 'Add Node',
+      '#url' => Url::fromRoute('node.add', ['node_type' => 'page']),
+      '#attributes' => [
+        'class' => ['use-ajax'],
+        'data-dialog-type' => 'dialog',
+        'data-dialog-renderer' => 'off_canvas',
+      ],
+    ];
+    $build['#attached']['library'][] = 'core/drupal.dialog.off_canvas';
+    return $build;
+  }
+
+}
diff --git a/core/modules/ckeditor/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php b/core/modules/ckeditor/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php
index 9e7b8df53a6ee2d22d166da6ff8175f62a7e922f..1a1a5cebaa19135318b8a8278b34cf1049d6d810 100644
--- a/core/modules/ckeditor/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php
+++ b/core/modules/ckeditor/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php
@@ -34,7 +34,7 @@ class CKEditorIntegrationTest extends WebDriverTestBase {
   /**
    * {@inheritdoc}
    */
-  public static $modules = ['node', 'ckeditor', 'filter'];
+  public static $modules = ['node', 'ckeditor', 'filter', 'ckeditor_test'];
 
   /**
    * {@inheritdoc}
@@ -177,4 +177,30 @@ public function testDrupalImageCaptionDialog() {
     $web_assert->elementExists('css', '.ui-dialog input[name="attributes[hasCaption]"]');
   }
 
+  /**
+   * Tests if CKEditor is properly styled inside an off-canvas dialog.
+   */
+  public function testOffCanvasStyles() {
+    $assert_session = $this->assertSession();
+    $page = $this->getSession()->getPage();
+
+    $this->drupalGet('/ckeditor_test/off_canvas');
+
+    // The "Add Node" link triggers an off-canvas dialog with an add node form
+    // that includes CKEditor.
+    $page->clickLink('Add Node');
+    $assert_session->waitForElementVisible('css', '#drupal-off-canvas');
+    $assert_session->assertWaitOnAjaxRequest();
+
+    // Check the background color of two CKEditor elements to confirm they are
+    // not overriden by the off-canvas css reset.
+    $assert_session->elementExists('css', '.cke_top');
+    $ckeditor_top_bg_color = $this->getSession()->evaluateScript('window.getComputedStyle(document.getElementsByClassName(\'cke_top\')[0]).backgroundColor');
+    $this->assertEqual($ckeditor_top_bg_color, 'rgb(248, 248, 248)');
+
+    $assert_session->elementExists('css', '.cke_button__source');
+    $ckeditor_source_button_bg_color = $this->getSession()->evaluateScript('window.getComputedStyle(document.getElementsByClassName(\'cke_button__source\')[0]).backgroundColor');
+    $this->assertEqual($ckeditor_source_button_bg_color, 'rgba(0, 0, 0, 0)');
+  }
+
 }