diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss
index 2acddc233c..1814919b61 100644
--- a/res/css/views/rooms/_RoomTile.scss
+++ b/res/css/views/rooms/_RoomTile.scss
@@ -143,6 +143,8 @@ limitations under the License.
 
 // toggle menuButton and badge on hover/menu displayed
 .mx_RoomTile_menuDisplayed,
+// or on keyboard focus of room tile
+.mx_RoomTile.focus-visible:focus-within,
 .mx_LeftPanel_container:not(.collapsed) .mx_RoomTile:hover {
     .mx_RoomTile_menuButton {
         display: block;
diff --git a/src/Keyboard.js b/src/Keyboard.js
index 738da478e4..f63956777f 100644
--- a/src/Keyboard.js
+++ b/src/Keyboard.js
@@ -69,6 +69,8 @@ export const Key = {
     BACKSPACE: "Backspace",
     ARROW_UP: "ArrowUp",
     ARROW_DOWN: "ArrowDown",
+    ARROW_LEFT: "ArrowLeft",
+    ARROW_RIGHT: "ArrowRight",
     TAB: "Tab",
     ESCAPE: "Escape",
     ENTER: "Enter",
diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js
index 36dd3a7a61..d1d3bb1b63 100644
--- a/src/components/structures/LeftPanel.js
+++ b/src/components/structures/LeftPanel.js
@@ -186,6 +186,7 @@ const LeftPanel = createReactClass({
             }
         } while (element && !(
             classes.contains("mx_RoomTile") ||
+            classes.contains("mx_RoomSubList_label") ||
             classes.contains("mx_textinput_search")));
 
         if (element) {
diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js
index 60be1d7b34..92b9d91e0e 100644
--- a/src/components/structures/RoomSubList.js
+++ b/src/components/structures/RoomSubList.js
@@ -2,6 +2,7 @@
 Copyright 2015, 2016 OpenMarket Ltd
 Copyright 2017 Vector Creations Ltd
 Copyright 2018, 2019 New Vector Ltd
+Copyright 2019 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -16,7 +17,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React from 'react';
+import React, {createRef} from 'react';
 import createReactClass from 'create-react-class';
 import classNames from 'classnames';
 import sdk from '../../index';
@@ -25,7 +26,7 @@ import Unread from '../../Unread';
 import * as RoomNotifs from '../../RoomNotifs';
 import * as FormattingUtils from '../../utils/FormattingUtils';
 import IndicatorScrollbar from './IndicatorScrollbar';
-import { KeyCode } from '../../Keyboard';
+import {Key, KeyCode} from '../../Keyboard';
 import { Group } from 'matrix-js-sdk';
 import PropTypes from 'prop-types';
 import RoomTile from "../views/rooms/RoomTile";
@@ -56,7 +57,6 @@ const RoomSubList = createReactClass({
         collapsed: PropTypes.bool.isRequired, // is LeftPanel collapsed?
         onHeaderClick: PropTypes.func,
         incomingCall: PropTypes.object,
-        isFiltered: PropTypes.bool,
         extraTiles: PropTypes.arrayOf(PropTypes.node), // extra elements added beneath tiles
         forceExpand: PropTypes.bool,
     },
@@ -80,6 +80,7 @@ const RoomSubList = createReactClass({
     },
 
     componentWillMount: function() {
+        this._headerButton = createRef();
         this.dispatcherRef = dis.register(this.onAction);
     },
 
@@ -87,9 +88,9 @@ const RoomSubList = createReactClass({
         dis.unregister(this.dispatcherRef);
     },
 
-    // The header is collapsable if it is hidden or not stuck
+    // The header is collapsible if it is hidden or not stuck
     // The dataset elements are added in the RoomList _initAndPositionStickyHeaders method
-    isCollapsableOnClick: function() {
+    isCollapsibleOnClick: function() {
         const stuck = this.refs.header.dataset.stuck;
         if (!this.props.forceExpand && (this.state.hidden || stuck === undefined || stuck === "none")) {
             return true;
@@ -114,8 +115,8 @@ const RoomSubList = createReactClass({
     },
 
     onClick: function(ev) {
-        if (this.isCollapsableOnClick()) {
-            // The header isCollapsable, so the click is to be interpreted as collapse and truncation logic
+        if (this.isCollapsibleOnClick()) {
+            // The header isCollapsible, so the click is to be interpreted as collapse and truncation logic
             const isHidden = !this.state.hidden;
             this.setState({hidden: isHidden}, () => {
                 this.props.onHeaderClick(isHidden);
@@ -124,6 +125,30 @@ const RoomSubList = createReactClass({
             // The header is stuck, so the click is to be interpreted as a scroll to the header
             this.props.onHeaderClick(this.state.hidden, this.refs.header.dataset.originalPosition);
         }
+        this._headerButton.current.focus();
+    },
+
+    onKeyDown: function(ev) {
+        switch (ev.key) {
+            case Key.TAB:
+                // Prevent LeftPanel handling Tab if focus is on the sublist header itself
+                ev.stopPropagation();
+                break;
+            case Key.ARROW_LEFT:
+                ev.stopPropagation();
+                if (!this.state.hidden && !this.props.forceExpand) {
+                    this.onClick();
+                }
+                break;
+            case Key.ARROW_RIGHT:
+                ev.stopPropagation();
+                if (this.state.hidden && !this.props.forceExpand) {
+                    this.onClick();
+                } else {
+                    // TODO go to first element in subtree
+                }
+                break;
+        }
     },
 
     onRoomTileClick(roomId, ev) {
@@ -193,6 +218,11 @@ const RoomSubList = createReactClass({
         }
     },
 
+    onAddRoom: function(e) {
+        e.stopPropagation();
+        if (this.props.onAddRoom) this.props.onAddRoom();
+    },
+
     _getHeaderJsx: function(isCollapsed) {
         const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
         const AccessibleTooltipButton = sdk.getComponent('elements.AccessibleTooltipButton');
@@ -209,12 +239,18 @@ const RoomSubList = createReactClass({
                 'mx_RoomSubList_badgeHighlight': subListNotifHighlight,
             });
             if (subListNotifCount > 0) {
-                badge = <div className={badgeClasses} onClick={this._onNotifBadgeClick}>
-                    { FormattingUtils.formatCount(subListNotifCount) }
-                </div>;
+                badge = (
+                    <AccessibleButton className={badgeClasses} onClick={this._onNotifBadgeClick} aria-label={_t("Jump to first unread room.")}>
+                        { FormattingUtils.formatCount(subListNotifCount) }
+                    </AccessibleButton>
+                );
             } else if (this.props.isInvite && this.props.list.length) {
                 // no notifications but highlight anyway because this is an invite badge
-                badge = <div className={badgeClasses} onClick={this._onInviteBadgeClick}>{this.props.list.length}</div>;
+                badge = (
+                    <AccessibleButton className={badgeClasses} onClick={this._onInviteBadgeClick} aria-label={_t("Jump to first invite.")}>
+                        { this.props.list.length }
+                    </AccessibleButton>
+                );
             }
         }
 
@@ -237,7 +273,7 @@ const RoomSubList = createReactClass({
         if (this.props.onAddRoom) {
             addRoomButton = (
                 <AccessibleTooltipButton
-                    onClick={ this.props.onAddRoom }
+                    onClick={this.onAddRoom}
                     className="mx_RoomSubList_addRoom"
                     title={this.props.addRoomLabel || _t("Add room")}
                 />
@@ -255,10 +291,17 @@ const RoomSubList = createReactClass({
             chevron = (<div className={chevronClasses} />);
         }
 
-        const tabindex = this.props.isFiltered ? "0" : "-1";
         return (
-            <div className="mx_RoomSubList_labelContainer" title={ title } ref="header">
-                <AccessibleButton onClick={this.onClick} className="mx_RoomSubList_label" tabIndex={tabindex} aria-expanded={!isCollapsed}>
+            <div className="mx_RoomSubList_labelContainer" title={title} ref="header">
+                <AccessibleButton
+                    onClick={this.onClick}
+                    className="mx_RoomSubList_label"
+                    tabIndex={0}
+                    aria-expanded={!isCollapsed}
+                    inputRef={this._headerButton}
+                    // cancel out role so this button behaves as the toggle-header of this group
+                    role="none"
+                >
                     { chevron }
                     <span>{this.props.label}</span>
                     { incomingCall }
@@ -344,6 +387,7 @@ const RoomSubList = createReactClass({
                 role="group"
                 aria-label={this.props.label}
                 aria-expanded={!isCollapsed}
+                onKeyDown={this.onKeyDown}
             >
                 { this._getHeaderJsx(isCollapsed) }
                 { content }
diff --git a/src/components/views/elements/AccessibleButton.js b/src/components/views/elements/AccessibleButton.js
index bfc3e45246..1ccb7d0796 100644
--- a/src/components/views/elements/AccessibleButton.js
+++ b/src/components/views/elements/AccessibleButton.js
@@ -67,8 +67,6 @@ export default function AccessibleButton(props) {
     restProps.ref = restProps.inputRef;
     delete restProps.inputRef;
 
-    restProps.tabIndex = restProps.tabIndex || "0";
-    restProps.role = restProps.role || "button";
     restProps.className = (restProps.className ? restProps.className + " " : "") + "mx_AccessibleButton";
 
     if (kind) {
@@ -93,19 +91,30 @@ export default function AccessibleButton(props) {
  */
 AccessibleButton.propTypes = {
     children: PropTypes.node,
-    inputRef: PropTypes.func,
+    inputRef: PropTypes.oneOfType([
+        // Either a function
+        PropTypes.func,
+        // Or the instance of a DOM native element
+        PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
+    ]),
     element: PropTypes.string,
     onClick: PropTypes.func.isRequired,
 
     // The kind of button, similar to how Bootstrap works.
     // See available classes for AccessibleButton for options.
     kind: PropTypes.string,
+    // The ARIA role
+    role: PropTypes.string,
+    // The tabIndex
+    tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
 
     disabled: PropTypes.bool,
 };
 
 AccessibleButton.defaultProps = {
     element: 'div',
+    role: 'button',
+    tabIndex: "0",
 };
 
 AccessibleButton.displayName = "AccessibleButton";
diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js
index d8092eae22..036f50d899 100644
--- a/src/components/views/rooms/RoomList.js
+++ b/src/components/views/rooms/RoomList.js
@@ -771,7 +771,7 @@ module.exports = createReactClass({
         const subListComponents = this._mapSubListProps(subLists);
 
         return (
-            <div ref={this._collectResizeContainer} className="mx_RoomList" role="listbox" aria-label={_t("Rooms")}
+            <div ref={this._collectResizeContainer} className="mx_RoomList" role="tree" aria-label={_t("Rooms")}
                  onMouseMove={this.onMouseMove} onMouseLeave={this.onMouseLeave}>
                 { subListComponents }
             </div>
diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js
index b727abd261..1398e03b10 100644
--- a/src/components/views/rooms/RoomTile.js
+++ b/src/components/views/rooms/RoomTile.js
@@ -398,7 +398,8 @@ module.exports = createReactClass({
                                  onMouseLeave={this.onMouseLeave}
                                  onContextMenu={this.onContextMenu}
                                  aria-label={ariaLabel}
-                                 role="option"
+                                 aria-selected={this.state.selected}
+                                 role="treeitem"
         >
             <div className={avatarClasses}>
                 <div className="mx_RoomTile_avatar_container">
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index f524a22d4b..13945a9ce8 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -901,11 +901,6 @@
     "Forget room": "Forget room",
     "Search": "Search",
     "Share room": "Share room",
-    "Drop here to favourite": "Drop here to favourite",
-    "Drop here to tag direct chat": "Drop here to tag direct chat",
-    "Drop here to restore": "Drop here to restore",
-    "Drop here to demote": "Drop here to demote",
-    "Drop here to tag %(section)s": "Drop here to tag %(section)s",
     "Community Invites": "Community Invites",
     "Invites": "Invites",
     "Favourites": "Favourites",
@@ -1653,6 +1648,8 @@
     "Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
     "Active call": "Active call",
     "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?",
+    "Jump to first unread room.": "Jump to first unread room.",
+    "Jump to first invite.": "Jump to first invite.",
     "Add room": "Add room",
     "You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
     "You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?",