diff --git a/core/core.services.yml b/core/core.services.yml
index 737009bac5..d2486793b2 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -1052,6 +1052,11 @@ services:
     arguments: ['@title_resolver', '@renderer']
     tags:
       - { name: render.main_content_renderer, format: drupal_dialog.off_canvas }
+  main_content_renderer.off_canvas_top:
+    class: Drupal\Core\Render\MainContent\OffCanvasRenderer
+    arguments: ['@title_resolver', '@renderer', 'top']
+    tags:
+      - { name: render.main_content_renderer, format: drupal_dialog.off_canvas_top }
   main_content_renderer.modal:
     class: Drupal\Core\Render\MainContent\ModalRenderer
     arguments: ['@title_resolver']
diff --git a/core/lib/Drupal/Core/Ajax/OpenOffCanvasDialogCommand.php b/core/lib/Drupal/Core/Ajax/OpenOffCanvasDialogCommand.php
index da6a26e35a..78c406b3d8 100644
--- a/core/lib/Drupal/Core/Ajax/OpenOffCanvasDialogCommand.php
+++ b/core/lib/Drupal/Core/Ajax/OpenOffCanvasDialogCommand.php
@@ -34,19 +34,22 @@ class OpenOffCanvasDialogCommand extends OpenDialogCommand {
    *   (optional) Custom settings that will be passed to the Drupal behaviors
    *   on the content of the dialog. If left empty, the settings will be
    *   populated automatically from the current request.
+   * @param string $position
+   *   (optional) The position to render the off-canvas dialog.
    */
-  public function __construct($title, $content, array $dialog_options = [], $settings = NULL) {
+  public function __construct($title, $content, array $dialog_options = [], $settings = NULL, $position = 'side') {
     parent::__construct('#drupal-off-canvas', $title, $content, $dialog_options, $settings);
     $this->dialogOptions['modal'] = FALSE;
     $this->dialogOptions['autoResize'] = FALSE;
     $this->dialogOptions['resizable'] = 'w';
     $this->dialogOptions['draggable'] = FALSE;
     $this->dialogOptions['drupalAutoButtons'] = FALSE;
+    $this->dialogOptions['drupalOffCanvasPosition'] = $position;
     // @todo drupal.ajax.js does not respect drupalAutoButtons properly, pass an
     //   empty set of buttons until https://www.drupal.org/node/2793343 is in.
     $this->dialogOptions['buttons'] = [];
     if (empty($dialog_options['dialogClass'])) {
-      $this->dialogOptions['dialogClass'] = 'ui-dialog-off-canvas';
+      $this->dialogOptions['dialogClass'] = "ui-dialog-off-canvas ui-dialog-position-$position";
     }
     // If no width option is provided then use the default width to avoid the
     // dialog staying at the width of the previous instance when opened
diff --git a/core/lib/Drupal/Core/Render/MainContent/OffCanvasRenderer.php b/core/lib/Drupal/Core/Render/MainContent/OffCanvasRenderer.php
index 55bf8eb7d1..b8f0e73e5d 100644
--- a/core/lib/Drupal/Core/Render/MainContent/OffCanvasRenderer.php
+++ b/core/lib/Drupal/Core/Render/MainContent/OffCanvasRenderer.php
@@ -23,6 +23,13 @@ class OffCanvasRenderer extends DialogRenderer {
    */
   protected $renderer;
 
+  /**
+   * The position to render the off-canvas dialog.
+   *
+   * @var string
+   */
+  protected $position;
+
   /**
    * Constructs a new OffCanvasRenderer.
    *
@@ -30,10 +37,13 @@ class OffCanvasRenderer extends DialogRenderer {
    *   The title resolver.
    * @param \Drupal\Core\Render\RendererInterface $renderer
    *   The renderer.
+   * @param string $position
+   *   (optional) The position to render the off-canvas dialog.
    */
-  public function __construct(TitleResolverInterface $title_resolver, RendererInterface $renderer) {
+  public function __construct(TitleResolverInterface $title_resolver, RendererInterface $renderer, $position = 'side') {
     parent::__construct($title_resolver);
     $this->renderer = $renderer;
+    $this->position = $position;
   }
 
   /**
@@ -55,7 +65,7 @@ public function renderResponse(array $main_content, Request $request, RouteMatch
     // Determine the title: use the title provided by the main content if any,
     // otherwise get it from the routing information.
     $options = $request->request->get('dialogOptions', []);
-    $response->addCommand(new OpenOffCanvasDialogCommand($title, $content, $options));
+    $response->addCommand(new OpenOffCanvasDialogCommand($title, $content, $options, NULL, $this->position));
     return $response;
   }
 
diff --git a/core/misc/dialog/off-canvas.es6.js b/core/misc/dialog/off-canvas.es6.js
index 0068e44e1b..5058b72d8d 100644
--- a/core/misc/dialog/off-canvas.es6.js
+++ b/core/misc/dialog/off-canvas.es6.js
@@ -90,8 +90,7 @@
       $('body').removeClass('js-off-canvas-dialog-open');
       // Remove all *.off-canvas events
       Drupal.offCanvas.removeOffCanvasEvents($element);
-
-      Drupal.offCanvas.$mainCanvasWrapper.css(`padding-${Drupal.offCanvas.getEdge()}`, 0);
+      Drupal.offCanvas.resetPadding();
     },
 
     /**
@@ -171,8 +170,9 @@
       const offsets = displace.offsets;
       const $element = event.data.$element;
       const container = Drupal.offCanvas.getContainer($element);
+      const position = event.data.settings.drupalOffCanvasPosition;
 
-      const topPosition = (offsets.top !== 0 ? `+${offsets.top}` : '');
+      const topPosition = position === 'side' && offsets.top !== 0 ? `+${offsets.top}` : '';
       const adjustedOptions = {
         // @see http://api.jqueryui.com/position/
         position: {
@@ -182,9 +182,12 @@
         },
       };
 
+      const height = position === 'side' ? `${$(window).height() - (offsets.top + offsets.bottom)}px` : '300px';
+      const width = position === 'side' ? event.data.settings.width : '100%';
       container.css({
         position: 'fixed',
-        height: `${$(window).height() - (offsets.top + offsets.bottom)}px`,
+        height,
+        width,
       });
 
       $element
@@ -204,17 +207,25 @@
       if ($('body').outerWidth() < Drupal.offCanvas.minDisplaceWidth) {
         return;
       }
+      Drupal.offCanvas.resetPadding();
       const $element = event.data.$element;
       const $container = Drupal.offCanvas.getContainer($element);
       const $mainCanvasWrapper = Drupal.offCanvas.$mainCanvasWrapper;
 
       const width = $container.outerWidth();
       const mainCanvasPadding = $mainCanvasWrapper.css(`padding-${Drupal.offCanvas.getEdge()}`);
-      if (width !== mainCanvasPadding) {
+      if (event.data.settings.drupalOffCanvasPosition === 'side' && width !== mainCanvasPadding) {
         $mainCanvasWrapper.css(`padding-${Drupal.offCanvas.getEdge()}`, `${width}px`);
         $container.attr(`data-offset-${Drupal.offCanvas.getEdge()}`, width);
         displace();
       }
+
+      if (event.data.settings.drupalOffCanvasPosition === 'top') {
+        $('nav#toolbar-bar').css('margin-top', '300px');
+        $mainCanvasWrapper.css('padding-top', '300px');
+        $container.attr('data-offset-top', 300);
+        displace();
+      }
     },
 
     /**
@@ -238,6 +249,16 @@
     getEdge() {
       return document.documentElement.dir === 'rtl' ? 'left' : 'right';
     },
+
+    /**
+     * Reset main canvas wrapper and toolbar padding / margin.
+     */
+    resetPadding() {
+      Drupal.offCanvas.$mainCanvasWrapper.css(`padding-${Drupal.offCanvas.getEdge()}`, 0);
+      Drupal.offCanvas.$mainCanvasWrapper.css('padding-top', 0);
+      $('nav#toolbar-bar').css('margin-top', '0');
+      displace();
+    },
   };
 
   /**
diff --git a/core/misc/dialog/off-canvas.js b/core/misc/dialog/off-canvas.js
index 1de5f67565..1ac3158e9e 100644
--- a/core/misc/dialog/off-canvas.js
+++ b/core/misc/dialog/off-canvas.js
@@ -41,8 +41,7 @@
       $('body').removeClass('js-off-canvas-dialog-open');
 
       Drupal.offCanvas.removeOffCanvasEvents($element);
-
-      Drupal.offCanvas.$mainCanvasWrapper.css('padding-' + Drupal.offCanvas.getEdge(), 0);
+      Drupal.offCanvas.resetPadding();
     },
     afterCreate: function afterCreate(_ref3) {
       var $element = _ref3.$element,
@@ -82,8 +81,9 @@
       var offsets = displace.offsets;
       var $element = event.data.$element;
       var container = Drupal.offCanvas.getContainer($element);
+      var position = event.data.settings.drupalOffCanvasPosition;
 
-      var topPosition = offsets.top !== 0 ? '+' + offsets.top : '';
+      var topPosition = position === 'side' && offsets.top !== 0 ? '+' + offsets.top : '';
       var adjustedOptions = {
         position: {
           my: Drupal.offCanvas.getEdge() + ' top',
@@ -92,9 +92,12 @@
         }
       };
 
+      var height = position === 'side' ? $(window).height() - (offsets.top + offsets.bottom) + 'px' : '300px';
+      var width = position === 'side' ? event.data.settings.width : '100%';
       container.css({
         position: 'fixed',
-        height: $(window).height() - (offsets.top + offsets.bottom) + 'px'
+        height: height,
+        width: width
       });
 
       $element.dialog('option', adjustedOptions).trigger('dialogContentResize.off-canvas');
@@ -103,23 +106,37 @@
       if ($('body').outerWidth() < Drupal.offCanvas.minDisplaceWidth) {
         return;
       }
+      Drupal.offCanvas.resetPadding();
       var $element = event.data.$element;
       var $container = Drupal.offCanvas.getContainer($element);
       var $mainCanvasWrapper = Drupal.offCanvas.$mainCanvasWrapper;
 
       var width = $container.outerWidth();
       var mainCanvasPadding = $mainCanvasWrapper.css('padding-' + Drupal.offCanvas.getEdge());
-      if (width !== mainCanvasPadding) {
+      if (event.data.settings.drupalOffCanvasPosition === 'side' && width !== mainCanvasPadding) {
         $mainCanvasWrapper.css('padding-' + Drupal.offCanvas.getEdge(), width + 'px');
         $container.attr('data-offset-' + Drupal.offCanvas.getEdge(), width);
         displace();
       }
+
+      if (event.data.settings.drupalOffCanvasPosition === 'top') {
+        $('nav#toolbar-bar').css('margin-top', '300px');
+        $mainCanvasWrapper.css('padding-top', '300px');
+        $container.attr('data-offset-top', 300);
+        displace();
+      }
     },
     getContainer: function getContainer($element) {
       return $element.dialog('widget');
     },
     getEdge: function getEdge() {
       return document.documentElement.dir === 'rtl' ? 'left' : 'right';
+    },
+    resetPadding: function resetPadding() {
+      Drupal.offCanvas.$mainCanvasWrapper.css('padding-' + Drupal.offCanvas.getEdge(), 0);
+      Drupal.offCanvas.$mainCanvasWrapper.css('padding-top', 0);
+      $('nav#toolbar-bar').css('margin-top', '0');
+      displace();
     }
   };
 
diff --git a/core/modules/system/tests/modules/off_canvas_test/src/Controller/TestController.php b/core/modules/system/tests/modules/off_canvas_test/src/Controller/TestController.php
index ea310fa0bd..aec2d82c9f 100644
--- a/core/modules/system/tests/modules/off_canvas_test/src/Controller/TestController.php
+++ b/core/modules/system/tests/modules/off_canvas_test/src/Controller/TestController.php
@@ -67,6 +67,16 @@ public function linksDisplay() {
           ]),
         ],
       ],
+      'off_canvas_top_link' => [
+        '#title' => 'Open top panel',
+        '#type' => 'link',
+        '#url' => Url::fromRoute('off_canvas_test.thing1'),
+        '#attributes' => [
+          'class' => ['use-ajax'],
+          'data-dialog-type' => 'dialog',
+          'data-dialog-renderer' => 'off_canvas_top',
+        ],
+      ],
       'other_dialog_links' => [
         '#title' => 'Display more links!',
         '#type' => 'link',
diff --git a/core/modules/system/tests/src/Functional/Ajax/OffCanvasDialogTest.php b/core/modules/system/tests/src/Functional/Ajax/OffCanvasDialogTest.php
index 222ebc45d7..aa4edaa924 100644
--- a/core/modules/system/tests/src/Functional/Ajax/OffCanvasDialogTest.php
+++ b/core/modules/system/tests/src/Functional/Ajax/OffCanvasDialogTest.php
@@ -46,7 +46,7 @@ public function testDialog() {
           'draggable' => FALSE,
           'drupalAutoButtons' => FALSE,
           'buttons' => [],
-          'dialogClass' => 'ui-dialog-off-canvas',
+          'dialogClass' => 'ui-dialog-off-canvas ui-dialog-position-side',
           'width' => 300,
         ],
       'effect' => 'fade',
diff --git a/core/modules/system/tests/src/FunctionalJavascript/OffCanvasTest.php b/core/modules/system/tests/src/FunctionalJavascript/OffCanvasTest.php
index 567919c3ee..789ccf8630 100644
--- a/core/modules/system/tests/src/FunctionalJavascript/OffCanvasTest.php
+++ b/core/modules/system/tests/src/FunctionalJavascript/OffCanvasTest.php
@@ -42,6 +42,8 @@ public function testOffCanvasLinks() {
 
         // Check that the canvas is not on the page.
         $web_assert->elementExists('css', '#drupal-off-canvas');
+        // Check that the canvas is positioned on the side.
+        $web_assert->elementExists('css', '.ui-dialog-position-side');
         // Check that response text is on page.
         $web_assert->pageTextContains("Thing $link_index says hello");
         $off_canvas_tray = $this->getOffCanvasDialog();
@@ -71,6 +73,15 @@ public function testOffCanvasLinks() {
           $web_assert->elementNotExists('css', '.ui-dialog-empty-title');
         }
       }
+
+      // Test an off_canvas_top tray.
+      $page->clickLink('Open top panel');
+      $this->waitForOffCanvasToOpen();
+
+      // Check that the canvas is not on the page.
+      $web_assert->elementExists('css', '#drupal-off-canvas');
+      // Check that the canvas is positioned at the top.
+      $web_assert->elementExists('css', '.ui-dialog-position-top');
     }
   }
 
diff --git a/core/tests/Drupal/Tests/Core/Ajax/OpenOffCanvasDialogCommandTest.php b/core/tests/Drupal/Tests/Core/Ajax/OpenOffCanvasDialogCommandTest.php
index e2d933a657..c68368a4f4 100644
--- a/core/tests/Drupal/Tests/Core/Ajax/OpenOffCanvasDialogCommandTest.php
+++ b/core/tests/Drupal/Tests/Core/Ajax/OpenOffCanvasDialogCommandTest.php
@@ -31,7 +31,7 @@ public function testRender() {
         'draggable' => FALSE,
         'drupalAutoButtons' => FALSE,
         'buttons' => [],
-        'dialogClass' => 'ui-dialog-off-canvas',
+        'dialogClass' => 'ui-dialog-off-canvas ui-dialog-position-side',
         'width' => 300,
       ],
       'effect' => 'fade',
