diff --git a/packages/widgets/src/dockpanel.ts b/packages/widgets/src/dockpanel.ts
index ed045d96d..28a1444de 100644
--- a/packages/widgets/src/dockpanel.ts
+++ b/packages/widgets/src/dockpanel.ts
@@ -54,6 +54,9 @@ export class DockPanel extends Widget {
     if (options.addButtonEnabled !== undefined) {
       this._addButtonEnabled = options.addButtonEnabled;
     }
+    if (options.tabScrollingEnabled !== undefined) {
+      this._tabScrollingEnabled = options.tabScrollingEnabled;
+    }
 
     // Toggle the CSS mode attribute.
     this.dataset['mode'] = this._mode;
@@ -255,6 +258,23 @@ export class DockPanel extends Widget {
     });
   }
 
+  /**
+   * Whether scrolling of tabs in tab bars is enabled.
+   */
+  get tabScrollingEnabled(): boolean {
+    return this._tabScrollingEnabled;
+  }
+
+  /**
+   * Set whether the add buttons for each tab bar are enabled.
+   */
+  set tabScrollingEnabled(value: boolean) {
+    this._tabScrollingEnabled = value;
+    each(this.tabBars(), tabbar => {
+      tabbar.scrollingEnabled = value;
+    });
+  }
+
   /**
    * Whether the dock panel is empty.
    */
@@ -914,6 +934,7 @@ export class DockPanel extends Widget {
     tabBar.tabsMovable = this._tabsMovable;
     tabBar.allowDeselect = false;
     tabBar.addButtonEnabled = this._addButtonEnabled;
+    tabBar.scrollingEnabled = this._tabScrollingEnabled;
     tabBar.removeBehavior = 'select-previous-tab';
     tabBar.insertBehavior = 'select-tab-if-needed';
 
@@ -1054,6 +1075,7 @@ export class DockPanel extends Widget {
   private _tabsMovable: boolean = true;
   private _tabsConstrained: boolean = false;
   private _addButtonEnabled: boolean = false;
+  private _tabScrollingEnabled: boolean = false;
   private _pressData: Private.IPressData | null = null;
   private _layoutModified = new Signal<this, void>(this);
 
@@ -1136,6 +1158,13 @@ export namespace DockPanel {
      * The default is `'false'`.
      */
     addButtonEnabled?: boolean;
+
+    /**
+     * Enable scrolling in each of the dock panel's tab bars.
+     *
+     * The default is `'false'`.
+     */
+    tabScrollingEnabled?: boolean;
   }
 
   /**
diff --git a/packages/widgets/src/tabbar.ts b/packages/widgets/src/tabbar.ts
index 9f0e925a4..39d506ecb 100644
--- a/packages/widgets/src/tabbar.ts
+++ b/packages/widgets/src/tabbar.ts
@@ -54,6 +54,7 @@ export class TabBar<T> extends Widget {
     this._document = options.document || document;
     this.tabsMovable = options.tabsMovable || false;
     this.titlesEditable = options.titlesEditable || false;
+    this._scrollingEnabled = options.scrollingEnabled || false;
     this.allowDeselect = options.allowDeselect || false;
     this.addButtonEnabled = options.addButtonEnabled || false;
     this.insertBehavior = options.insertBehavior || 'select-tab-if-needed';
@@ -350,6 +351,30 @@ export class TabBar<T> extends Widget {
     }
   }
 
+  /**
+   * Whether scrolling is enabled.
+   *
+   * Note: for scrolling to work the tabs need to have `min-width` set.
+   */
+  get scrollingEnabled(): boolean {
+    return this._scrollingEnabled;
+  }
+
+  set scrollingEnabled(value: boolean) {
+    // Do nothing if the value does not change.
+    if (this._scrollingEnabled === value) {
+      return;
+    }
+
+    this._scrollingEnabled = value;
+    if (value) {
+      this.node.classList.add('lm-mod-scrollable');
+    } else {
+      this.node.classList.remove('lm-mod-scrollable');
+    }
+    this.maybeSwitchScrollButtons();
+  }
+
   /**
    * A read-only array of the titles in the tab bar.
    */
@@ -371,6 +396,20 @@ export class TabBar<T> extends Widget {
     )[0] as HTMLUListElement;
   }
 
+  /**
+   * The tab bar content wrapper node.
+   *
+   * #### Notes
+   * This is the node which wraps the content node and enables scrolling.
+   *
+   * Modifying this node directly can lead to undefined behavior.
+   */
+  get contentWrapperNode(): HTMLUListElement {
+    return this.node.getElementsByClassName(
+      'lm-TabBar-wrapper'
+    )[0] as HTMLUListElement;
+  }
+
   /**
    * The tab bar add button node.
    *
@@ -385,6 +424,18 @@ export class TabBar<T> extends Widget {
     )[0] as HTMLDivElement;
   }
 
+  get scrollBeforeButtonNode(): HTMLDivElement {
+    return this.node.getElementsByClassName(
+      'lm-TabBar-scrollBeforeButton'
+    )[0] as HTMLDivElement;
+  }
+
+  get scrollAfterButtonNode(): HTMLDivElement {
+    return this.node.getElementsByClassName(
+      'lm-TabBar-scrollAfterButton'
+    )[0] as HTMLDivElement;
+  }
+
   /**
    * Add a tab to the end of the tab bar.
    *
@@ -588,15 +639,19 @@ export class TabBar<T> extends Widget {
   handleEvent(event: Event): void {
     switch (event.type) {
       case 'pointerdown':
-        this._evtPointerDown(event as PointerEvent);
+        this._lastMouseEvent = event as MouseEvent;
+        this._evtPointerDown(event as MouseEvent);
         break;
       case 'pointermove':
-        this._evtPointerMove(event as PointerEvent);
+        this._lastMouseEvent = event as MouseEvent;
+        this._evtPointerMove(event as MouseEvent);
         break;
       case 'pointerup':
-        this._evtPointerUp(event as PointerEvent);
+        this._lastMouseEvent = event as MouseEvent;
+        this._evtPointerUp(event as MouseEvent);
         break;
       case 'dblclick':
+        this._lastMouseEvent = event as MouseEvent;
         this._evtDblClick(event as MouseEvent);
         break;
       case 'keydown':
@@ -606,6 +661,13 @@ export class TabBar<T> extends Widget {
         event.preventDefault();
         event.stopPropagation();
         break;
+      case 'scroll':
+        this._evtScroll(event);
+        break;
+      case 'wheel':
+        this._evtWheel(event as WheelEvent);
+        event.preventDefault();
+        break;
     }
   }
 
@@ -615,6 +677,8 @@ export class TabBar<T> extends Widget {
   protected onBeforeAttach(msg: Message): void {
     this.node.addEventListener('pointerdown', this);
     this.node.addEventListener('dblclick', this);
+    this.contentNode.addEventListener('scroll', this);
+    this.contentNode.addEventListener('wheel', this);
   }
 
   /**
@@ -623,6 +687,8 @@ export class TabBar<T> extends Widget {
   protected onAfterDetach(msg: Message): void {
     this.node.removeEventListener('pointerdown', this);
     this.node.removeEventListener('dblclick', this);
+    this.contentNode.removeEventListener('scroll', this);
+    this.contentNode.removeEventListener('wheel', this);
     this._releaseMouse();
   }
 
@@ -641,6 +707,114 @@ export class TabBar<T> extends Widget {
       content[i] = renderer.renderTab({ title, current, zIndex });
     }
     VirtualDOM.render(content, this.contentNode);
+
+    this.maybeSwitchScrollButtons();
+
+    // Scroll the current tab into view.
+    this.scrollCurrentIntoView();
+  }
+
+  protected onResize(msg: Widget.ResizeMessage): void {
+    super.onResize(msg);
+    this.maybeSwitchScrollButtons();
+  }
+
+  /**
+   * Scroll the current tab into view.
+   */
+  protected scrollCurrentIntoView() {
+    if (this.scrollingEnabled) {
+      const contentNode = this.contentNode;
+      const currentNode = contentNode.children.item(this.currentIndex);
+      if (currentNode) {
+        currentNode.scrollIntoView();
+        if (this.orientation == 'horizontal') {
+          contentNode.scrollTop = 0;
+        } else {
+          contentNode.scrollLeft = 0;
+        }
+      } else {
+        console.error('Current tab node not found');
+      }
+    }
+  }
+
+  /**
+   * Show/hide scroll buttons if needed.
+   */
+  protected maybeSwitchScrollButtons() {
+    const scrollBefore = this.scrollBeforeButtonNode;
+    const scrollAfter = this.scrollAfterButtonNode;
+    const state = this._scrollState;
+
+    if (this.scrollingEnabled && state.totalSize > state.displayedSize) {
+      // show both buttons
+      scrollBefore.classList.remove('lm-mod-hidden');
+      scrollAfter.classList.remove('lm-mod-hidden');
+    } else {
+      // hide both buttons
+      scrollBefore.classList.add('lm-mod-hidden');
+      scrollAfter.classList.add('lm-mod-hidden');
+    }
+    this.updateScrollingHints(state);
+  }
+
+  /**
+   * Adjust data reflecting the ability to scroll in each direction.
+   */
+  protected updateScrollingHints(scrollState: Private.IScrollState) {
+    const wrapper = this.contentWrapperNode;
+
+    if (!this.scrollingEnabled) {
+      delete wrapper.dataset['canScroll'];
+      return;
+    }
+
+    const canScrollBefore = scrollState.position != 0;
+    const canScrollAfter =
+      scrollState.position != scrollState.totalSize - scrollState.displayedSize;
+
+    if (canScrollBefore && canScrollAfter) {
+      wrapper.dataset['canScroll'] = 'both';
+    } else if (canScrollBefore) {
+      wrapper.dataset['canScroll'] = 'before';
+    } else if (canScrollAfter) {
+      wrapper.dataset['canScroll'] = 'after';
+    } else {
+      delete wrapper.dataset['canScroll'];
+    }
+  }
+
+  private get _scrollState(): Private.IScrollState {
+    const content = this.contentNode;
+    const contentRect = content.getBoundingClientRect();
+    const isHorizontal = this.orientation === 'horizontal';
+    const contentSize = isHorizontal ? contentRect.width : contentRect.height;
+    const scrollTotal = isHorizontal
+      ? content.scrollWidth
+      : content.scrollHeight;
+    const scroll = Math.round(
+      isHorizontal ? content.scrollLeft : content.scrollTop
+    );
+    return {
+      displayedSize: Math.round(contentSize),
+      totalSize: scrollTotal,
+      position: scroll
+    };
+  }
+
+  /**
+   * Handle the `'scroll'` event for the tab bar.
+   */
+  private _evtScroll(event: Event): void {
+    this.updateScrollingHints(this._scrollState);
+  }
+
+  /**
+   * Handle the `'wheel'` event for the tab bar.
+   */
+  private _evtWheel(event: WheelEvent): void {
+    this.scrollBy(event.deltaY);
   }
 
   /**
@@ -720,6 +894,91 @@ export class TabBar<T> extends Widget {
     }
   }
 
+  /**
+   * Scroll by a fixed number of pixels (sign defines direction).
+   */
+  protected scrollBy(change: number) {
+    const orientation = this.orientation;
+    const contentNode = this.contentNode;
+
+    if (orientation == 'horizontal') {
+      contentNode.scrollLeft += change;
+    } else {
+      contentNode.scrollTop += change;
+    }
+    // Force-update drag state by dispatching last recorded mouse event.
+    if (this._lastMouseEvent) {
+      this._evtPointerMove(this._lastMouseEvent);
+    }
+  }
+
+  /**
+   * Begin scrolling in given direction.
+   */
+  protected beginScrolling(direction: '-' | '+') {
+    // How many pixels should be scrolled per second initially?
+    const initialRate = 150;
+    // By how much should the scrolling rate increase per second?
+    const rateIncrease = 80;
+    // What should be the maximal scrolling speed (pixels/second?)
+    const maxRate = 450;
+
+    let previousTime = performance.now();
+
+    const step = () => {
+      if (!this._scrollData) {
+        this.stopScrolling();
+        return;
+      }
+      const stepTime = performance.now();
+      const secondsChange = (stepTime - previousTime) / 1000;
+      previousTime = stepTime;
+      const rate = this._scrollData.rate;
+      const direction = this._scrollData.direction;
+
+      const change = (direction == '+' ? 1 : -1) * rate * secondsChange;
+
+      this.scrollBy(change);
+      this._scrollData.rate = Math.min(
+        this._scrollData.rate + rateIncrease * secondsChange,
+        maxRate
+      );
+      const state = this._scrollState;
+      if (
+        (direction == '-' && state.position == 0) ||
+        (direction == '+' &&
+          state.totalSize == state.position + state.displayedSize)
+      ) {
+        this.stopScrolling();
+        return;
+      }
+      window.requestAnimationFrame(step);
+    };
+
+    const shouldRequest = !this._scrollData;
+
+    this._scrollData = {
+      direction: direction,
+      rate: initialRate
+    };
+
+    if (shouldRequest) {
+      window.requestAnimationFrame(step);
+    }
+  }
+
+  /**
+   * Stop scrolling which was started with `beginScrolling` method.
+   */
+  protected stopScrolling() {
+    if (!this._scrollData) {
+      return;
+    }
+    this._scrollData = null;
+    const state = this._scrollState;
+    this.updateScrollingHints(state);
+  }
+
   /**
    * Handle the `'pointerdown'` event for the tab bar.
    */
@@ -739,6 +998,19 @@ export class TabBar<T> extends Widget {
       this.addButtonEnabled &&
       this.addButtonNode.contains(event.target as HTMLElement);
 
+    let scrollBeforeButtonClicked =
+      this.scrollingEnabled &&
+      this.scrollBeforeButtonNode.contains(event.target as HTMLElement);
+
+    let scrollAfterButtonClicked =
+      this.scrollingEnabled &&
+      this.scrollAfterButtonNode.contains(event.target as HTMLElement);
+
+    const anyButtonClicked =
+      addButtonClicked || scrollAfterButtonClicked || scrollBeforeButtonClicked;
+
+    const contentNode = this.contentNode;
+
     // Lookup the tab nodes.
     let tabs = this.contentNode.children;
 
@@ -747,8 +1019,8 @@ export class TabBar<T> extends Widget {
       return ElementExt.hitTest(tab, event.clientX, event.clientY);
     });
 
-    // Do nothing if the press is not on a tab or the add button.
-    if (index === -1 && !addButtonClicked) {
+    // Do nothing if the press is not on a tab or any of the buttons.
+    if (index === -1 && !anyButtonClicked) {
       return;
     }
 
@@ -756,12 +1028,37 @@ export class TabBar<T> extends Widget {
     event.preventDefault();
     event.stopPropagation();
 
+    // Add the document mouse up listener.
+    this.document.addEventListener('pointerup', this, true);
+
+    if (scrollBeforeButtonClicked || scrollAfterButtonClicked) {
+      this.beginScrolling(scrollBeforeButtonClicked ? '-' : '+');
+      return;
+    }
+
+    this._clickedTabIndex = index;
+
+    // Do nothing else if the middle button or add button is clicked.
+    if (event.button === 1 || addButtonClicked) {
+      return;
+    }
+
+    // Do nothing else if the close icon is clicked.
+    let icon = tabs[index].querySelector(this.renderer.closeIconSelector);
+    if (icon && icon.contains(event.target as HTMLElement)) {
+      return;
+    }
+
     // Initialize the non-measured parts of the drag data.
     this._dragData = {
       tab: tabs[index] as HTMLElement,
       index: index,
       pressX: event.clientX,
       pressY: event.clientY,
+      initialScrollPosition:
+        this.orientation == 'horizontal'
+          ? contentNode.scrollLeft
+          : contentNode.scrollTop,
       tabPos: -1,
       tabSize: -1,
       tabPressPos: -1,
@@ -774,20 +1071,6 @@ export class TabBar<T> extends Widget {
       detachRequested: false
     };
 
-    // Add the document pointer up listener.
-    this.document.addEventListener('pointerup', this, true);
-
-    // Do nothing else if the middle button or add button is clicked.
-    if (event.button === 1 || addButtonClicked) {
-      return;
-    }
-
-    // Do nothing else if the close icon is clicked.
-    let icon = tabs[index].querySelector(this.renderer.closeIconSelector);
-    if (icon && icon.contains(event.target as HTMLElement)) {
-      return;
-    }
-
     // Add the extra listeners if the tabs are movable.
     if (this.tabsMovable) {
       this.document.addEventListener('pointermove', this, true);
@@ -818,6 +1101,21 @@ export class TabBar<T> extends Widget {
    * Handle the `'pointermove'` event for the tab bar.
    */
   private _evtPointerMove(event: PointerEvent | MouseEvent): void {
+    let overBeforeScrollButton =
+      this.scrollingEnabled &&
+      this.scrollBeforeButtonNode.contains(event.target as HTMLElement);
+
+    let overAfterScrollButton =
+      this.scrollingEnabled &&
+      this.scrollAfterButtonNode.contains(event.target as HTMLElement);
+
+    const isOverScrollButton = overBeforeScrollButton || overAfterScrollButton;
+
+    if (!isOverScrollButton) {
+      // Stop scrolling if mouse is not over scroll buttons
+      this.stopScrolling();
+    }
+
     // Do nothing if no drag is in progress.
     let data = this._dragData;
     if (!data) {
@@ -828,14 +1126,22 @@ export class TabBar<T> extends Widget {
     event.preventDefault();
     event.stopPropagation();
 
-    // Lookup the tab nodes.
-    let tabs = this.contentNode.children;
+    if (isOverScrollButton) {
+      // Start scrolling if the mouse is over scroll buttons
+      this.beginScrolling(overBeforeScrollButton ? '-' : '+');
+    }
 
     // Bail early if the drag threshold has not been met.
-    if (!data.dragActive && !Private.dragExceeded(data, event)) {
+    if (
+      !data.dragActive &&
+      !Private.dragExceeded(data, event, this._scrollState)
+    ) {
       return;
     }
 
+    // Lookup the tab nodes.
+    let tabs = this.contentNode.children;
+
     // Activate the drag if necessary.
     if (!data.dragActive) {
       // Fill in the rest of the drag data measurements.
@@ -883,7 +1189,7 @@ export class TabBar<T> extends Widget {
     }
 
     // Update the positions of the tabs.
-    Private.layoutTabs(tabs, data, event, this._orientation);
+    Private.layoutTabs(tabs, data, event, this._orientation, this._scrollState);
   }
 
   /**
@@ -895,11 +1201,7 @@ export class TabBar<T> extends Widget {
       return;
     }
 
-    // Do nothing if no drag is in progress.
     const data = this._dragData;
-    if (!data) {
-      return;
-    }
 
     // Stop the event propagation.
     event.preventDefault();
@@ -908,14 +1210,23 @@ export class TabBar<T> extends Widget {
     // Remove the extra mouse event listeners.
     this.document.removeEventListener('pointermove', this, true);
     this.document.removeEventListener('pointerup', this, true);
-    this.document.removeEventListener('keydown', this, true);
-    this.document.removeEventListener('contextmenu', this, true);
 
-    // Handle a release when the drag is not active.
-    if (!data.dragActive) {
+    // Remove extra mouse event listeners which are only added when drag is in progress.
+    if (data) {
+      this.document.removeEventListener('pointermove', this, true);
+      this.document.removeEventListener('keydown', this, true);
+      this.document.removeEventListener('contextmenu', this, true);
+    }
+
+    // Handle a release when the drag is not active or not in progress.
+    if (!data || !data.dragActive) {
       // Clear the drag data.
       this._dragData = null;
 
+      // Handle mouse release if scrolling was in progress.
+      if (this._scrollData) {
+        this.stopScrolling();
+      }
       // Handle clicking the add button.
       let addButtonClicked =
         this.addButtonEnabled &&
@@ -934,7 +1245,12 @@ export class TabBar<T> extends Widget {
       });
 
       // Do nothing if the release is not on the original pressed tab.
-      if (index !== data.index) {
+      if (index !== this._clickedTabIndex) {
+        return;
+      }
+
+      // Do nothing if neither press nor release was on a tab.
+      if (index === -1 && this._clickedTabIndex === -1) {
         return;
       }
 
@@ -967,7 +1283,7 @@ export class TabBar<T> extends Widget {
     }
 
     // Position the tab at its final resting position.
-    Private.finalizeTabPosition(data, this._orientation);
+    Private.finalizeTabPosition(data, this._orientation, this._scrollState);
 
     // Remove the dragging class from the tab so it can be transitioned.
     data.tab.classList.remove('lm-mod-dragging');
@@ -1200,14 +1516,18 @@ export class TabBar<T> extends Widget {
   }
 
   private _name: string;
+  private _clickedTabIndex: number = -1;
   private _currentIndex = -1;
   private _titles: Title<T>[] = [];
   private _orientation: TabBar.Orientation;
   private _document: Document | ShadowRoot;
+  private _lastMouseEvent: MouseEvent | null = null;
   private _titlesEditable: boolean = false;
   private _previousTitle: Title<T> | null = null;
   private _dragData: Private.IDragData | null = null;
+  private _scrollData: Private.IScrollData | null = null;
   private _addButtonEnabled: boolean = false;
+  private _scrollingEnabled: boolean = false;
   private _tabMoved = new Signal<this, TabBar.ITabMovedArgs<T>>(this);
   private _currentChanged = new Signal<this, TabBar.ICurrentChangedArgs<T>>(
     this
@@ -1346,6 +1666,13 @@ export namespace TabBar {
      */
     addButtonEnabled?: boolean;
 
+    /**
+     * Whether scrolling is enabled.
+     *
+     * The default is `false`.
+     */
+    scrollingEnabled?: boolean;
+
     /**
      * The selection behavior when inserting a tab.
      *
@@ -1709,6 +2036,32 @@ namespace Private {
    */
   export const DETACH_THRESHOLD = 20;
 
+  /**
+   * A struct which holds the scroll data for a tab bar.
+   */
+  export interface IScrollData {
+    direction: '+' | '-';
+    rate: number;
+  }
+
+  /**
+   * A struct which holds the scroll state for a tab bar.
+   */
+  export interface IScrollState {
+    /**
+     * The size of the container where scrolling occurs (the visible part of the total).
+     */
+    displayedSize: number;
+    /**
+     * The total size of the content to be scrolled.
+     */
+    totalSize: number;
+    /**
+     * The current position (offset) of the scroll state.
+     */
+    position: number;
+  }
+
   /**
    * A struct which holds the drag data for a tab bar.
    */
@@ -1728,6 +2081,11 @@ namespace Private {
      */
     pressX: number;
 
+    /**
+     * The initial scroll position
+     */
+    initialScrollPosition: number;
+
     /**
      * The mouse press client Y position.
      */
@@ -1823,13 +2181,32 @@ namespace Private {
    */
   export function createNode(): HTMLDivElement {
     let node = document.createElement('div');
+    let scrollBefore = document.createElement('div');
+    scrollBefore.className =
+      'lm-TabBar-button lm-TabBar-scrollButton lm-TabBar-scrollBeforeButton lm-mod-hidden';
+    scrollBefore.setAttribute('role', 'button');
+    scrollBefore.innerText = '<';
+    let scrollAfter = document.createElement('div');
+    scrollAfter.className =
+      'lm-TabBar-button lm-TabBar-scrollButton lm-TabBar-scrollAfterButton lm-mod-hidden';
+    scrollAfter.setAttribute('role', 'button');
+    scrollAfter.innerText = '>';
+    node.appendChild(scrollBefore);
+
     let content = document.createElement('ul');
     content.setAttribute('role', 'tablist');
     content.className = 'lm-TabBar-content';
-    node.appendChild(content);
+
+    let wrapper = document.createElement('div');
+    wrapper.className = 'lm-TabBar-wrapper';
+    wrapper.appendChild(content);
+    node.appendChild(wrapper);
+
+    node.appendChild(scrollAfter);
 
     let add = document.createElement('div');
-    add.className = 'lm-TabBar-addButton lm-mod-hidden';
+    add.setAttribute('role', 'button');
+    add.className = 'lm-TabBar-button lm-TabBar-addButton lm-mod-hidden';
     node.appendChild(add);
     return node;
   }
@@ -1878,12 +2255,19 @@ namespace Private {
   }
 
   /**
-   * Test if the event exceeds the drag threshold.
+   * Test if the event or scroll state exceeds the drag threshold.
    */
-  export function dragExceeded(data: IDragData, event: MouseEvent): boolean {
+  export function dragExceeded(
+    data: IDragData,
+    event: MouseEvent,
+    scrollState: IScrollState | null
+  ): boolean {
     let dx = Math.abs(event.clientX - data.pressX);
     let dy = Math.abs(event.clientY - data.pressY);
-    return dx >= DRAG_THRESHOLD || dy >= DRAG_THRESHOLD;
+    let ds = scrollState
+      ? Math.abs(data.initialScrollPosition - scrollState.position)
+      : 0;
+    return dx >= DRAG_THRESHOLD || dy >= DRAG_THRESHOLD || ds >= DRAG_THRESHOLD;
   }
 
   /**
@@ -1906,23 +2290,24 @@ namespace Private {
     tabs: HTMLCollection,
     data: IDragData,
     event: MouseEvent,
-    orientation: TabBar.Orientation
+    orientation: TabBar.Orientation,
+    scrollState: IScrollState
   ): void {
     // Compute the orientation-sensitive values.
     let pressPos: number;
     let localPos: number;
     let clientPos: number;
-    let clientSize: number;
+    const clientSize = scrollState.totalSize;
+    const scrollShift = scrollState.position - data.initialScrollPosition;
+
     if (orientation === 'horizontal') {
-      pressPos = data.pressX;
-      localPos = event.clientX - data.contentRect!.left;
+      pressPos = data.pressX - scrollShift;
+      localPos = event.clientX - data.contentRect!.left + scrollState.position;
       clientPos = event.clientX;
-      clientSize = data.contentRect!.width;
     } else {
-      pressPos = data.pressY;
-      localPos = event.clientY - data.contentRect!.top;
+      pressPos = data.pressY - scrollShift;
+      localPos = event.clientY - data.contentRect!.top + scrollState.position;
       clientPos = event.clientY;
-      clientSize = data.contentRect!.height;
     }
 
     // Compute the target data.
@@ -1964,15 +2349,10 @@ namespace Private {
    */
   export function finalizeTabPosition(
     data: IDragData,
-    orientation: TabBar.Orientation
+    orientation: TabBar.Orientation,
+    scrollState: IScrollState
   ): void {
-    // Compute the orientation-sensitive client size.
-    let clientSize: number;
-    if (orientation === 'horizontal') {
-      clientSize = data.contentRect!.width;
-    } else {
-      clientSize = data.contentRect!.height;
-    }
+    const clientSize = scrollState.totalSize;
 
     // Compute the ideal final tab position.
     let ideal: number;
diff --git a/packages/widgets/style/tabbar.css b/packages/widgets/style/tabbar.css
index fabd830e7..5ad59d53b 100644
--- a/packages/widgets/style/tabbar.css
+++ b/packages/widgets/style/tabbar.css
@@ -33,11 +33,15 @@
   list-style-type: none;
 }
 
-.lm-TabBar[data-orientation='horizontal'] > .lm-TabBar-content {
+.lm-TabBar[data-orientation='horizontal']
+  > .lm-TabBar-wrapper
+  > .lm-TabBar-content {
   flex-direction: row;
 }
 
-.lm-TabBar[data-orientation='vertical'] > .lm-TabBar-content {
+.lm-TabBar[data-orientation='vertical']
+  > .lm-TabBar-wrapper
+  > .lm-TabBar-content {
   flex-direction: column;
 }
 
@@ -70,7 +74,7 @@
   display: none !important;
 }
 
-.lm-TabBar-addButton.lm-mod-hidden {
+.lm-TabBar-button.lm-mod-hidden {
   display: none !important;
 }
 
@@ -98,3 +102,127 @@
   box-sizing: border-box;
   background: inherit;
 }
+
+.lm-TabBar-wrapper {
+  /* Remove wrapper from DOM when scrolling is not enabled */
+  display: contents;
+  --lm-tabbar-scrollshadow-end: rgba(0, 0, 0, 0.3);
+  position: relative;
+}
+
+.lm-TabBar.lm-mod-scrollable .lm-TabBar-wrapper {
+  /* Keep wrapper in DOM when scrolling is enabled */
+  display: block;
+}
+
+.lm-TabBar-content {
+  scrollbar-width: none;
+}
+
+.lm-TabBar-content::-webkit-scrollbar {
+  display: none;
+}
+
+.lm-TabBar[data-orientation='horizontal'].lm-mod-scrollable
+  > .lm-TabBar-wrapper {
+  overflow-x: hidden;
+  overflow-y: hidden;
+}
+
+.lm-TabBar[data-orientation='vertical'].lm-mod-scrollable > .lm-TabBar-wrapper {
+  overflow-x: hidden;
+  overflow-y: auto;
+}
+
+.lm-TabBar > .lm-TabBar-wrapper > .lm-TabBar-content {
+  width: 100%;
+}
+
+.lm-TabBar[data-orientation='horizontal'].lm-mod-scrollable
+  > .lm-TabBar-wrapper
+  > .lm-TabBar-content {
+  overflow-x: scroll;
+  overflow-y: hidden;
+}
+
+.lm-TabBar[data-orientation='vertical'].lm-mod-scrollable
+  > .lm-TabBar-wrapper
+  > .lm-TabBar-content {
+  overflow-x: hidden;
+  overflow-y: scroll;
+}
+
+.lm-TabBar-wrapper::before,
+.lm-TabBar-wrapper::after {
+  content: '';
+  z-index: 10000;
+  display: none;
+  position: absolute;
+}
+
+.lm-TabBar-wrapper[data-can-scroll='before']::before {
+  display: block;
+}
+
+.lm-TabBar-wrapper[data-can-scroll='after']::after {
+  display: block;
+}
+
+.lm-TabBar-wrapper[data-can-scroll='both']::before,
+.lm-TabBar-wrapper[data-can-scroll='both']::after {
+  display: block;
+}
+
+.lm-TabBar[data-orientation='horizontal'] > .lm-TabBar-wrapper::before,
+.lm-TabBar[data-orientation='horizontal'] > .lm-TabBar-wrapper::after {
+  width: 5px;
+  height: 100%;
+  top: 0;
+}
+
+.lm-TabBar[data-orientation='horizontal'] > .lm-TabBar-wrapper::before {
+  background: linear-gradient(
+    to left,
+    rgba(0, 0, 0, 0),
+    var(--lm-tabbar-scrollshadow-end)
+  );
+  left: 0;
+}
+
+.lm-TabBar[data-orientation='horizontal'] > .lm-TabBar-wrapper::after {
+  background: linear-gradient(
+    to right,
+    rgba(0, 0, 0, 0),
+    var(--lm-tabbar-scrollshadow-end)
+  );
+  right: 0;
+}
+
+.lm-TabBar[data-orientation='vertical'] > .lm-TabBar-wrapper::before,
+.lm-TabBar[data-orientation='vertical'] > .lm-TabBar-wrapper::after {
+  height: 5px;
+  width: 100%;
+  left: 0;
+}
+
+.lm-TabBar[data-orientation='vertical'] > .lm-TabBar-wrapper::before {
+  background: linear-gradient(
+    to top,
+    rgba(0, 0, 0, 0),
+    var(--lm-tabbar-scrollshadow-end)
+  );
+  top: 0;
+}
+
+.lm-TabBar[data-orientation='vertical'] > .lm-TabBar-wrapper::after {
+  background: linear-gradient(
+    to bottom,
+    rgba(0, 0, 0, 0),
+    var(--lm-tabbar-scrollshadow-end)
+  );
+  bottom: 0;
+}
+
+.lm-TabBar[data-orientation='vertical'] > .lm-TabBar-scrollButton {
+  transform: rotate(90deg);
+}
diff --git a/packages/widgets/tests/src/tabbar.spec.ts b/packages/widgets/tests/src/tabbar.spec.ts
index 082eb979c..3ec977012 100644
--- a/packages/widgets/tests/src/tabbar.spec.ts
+++ b/packages/widgets/tests/src/tabbar.spec.ts
@@ -144,6 +144,7 @@ describe('@lumino/widgets', () => {
           tabsMovable: true,
           allowDeselect: true,
           addButtonEnabled: true,
+          scrollingEnabled: true,
           insertBehavior: 'select-tab',
           removeBehavior: 'select-previous-tab',
           renderer
@@ -152,6 +153,7 @@ describe('@lumino/widgets', () => {
         expect(newBar.tabsMovable).to.equal(true);
         expect(newBar.renderer).to.equal(renderer);
         expect(newBar.addButtonEnabled).to.equal(true);
+        expect(newBar.scrollingEnabled).to.equal(true);
       });
 
       it('should add the `lm-TabBar` class', () => {
@@ -652,6 +654,37 @@ describe('@lumino/widgets', () => {
       });
     });
 
+    describe('#scrollingEnabled', () => {
+      it('should get whether the scroll buttons are enabled', () => {
+        let bar = new TabBar<Widget>();
+        expect(bar.scrollingEnabled).to.equal(false);
+      });
+
+      it('should set whether the scroll buttons are enabled', () => {
+        let bar = new TabBar<Widget>();
+        bar.scrollingEnabled = true;
+        expect(bar.scrollingEnabled).to.equal(true);
+      });
+
+      it('should not show the scroll buttons if not set', () => {
+        populateBar(bar);
+        expect(
+          bar.scrollBeforeButtonNode.classList.contains('lm-mod-hidden')
+        ).to.equal(true);
+        expect(
+          bar.scrollAfterButtonNode.classList.contains('lm-mod-hidden')
+        ).to.equal(true);
+
+        bar.scrollingEnabled = true;
+        expect(
+          bar.scrollBeforeButtonNode.classList.contains('lm-mod-hidden')
+        ).to.equal(false);
+        expect(
+          bar.scrollAfterButtonNode.classList.contains('lm-mod-hidden')
+        ).to.equal(true);
+      });
+    });
+
     describe('#allowDeselect', () => {
       it('should determine whether a tab can be deselected by the user', () => {
         populateBar(bar);