diff --git a/res/css/structures/_LeftPanel2.scss b/res/css/structures/_LeftPanel2.scss
index bdaada0d15..a73658d916 100644
--- a/res/css/structures/_LeftPanel2.scss
+++ b/res/css/structures/_LeftPanel2.scss
@@ -86,11 +86,15 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations
 
             .mx_RoomSearch_expanded + .mx_LeftPanel2_exploreButton {
                 // Cheaty way to return the occupied space to the filter input
+                flex-basis: 0;
                 margin: 0;
                 width: 0;
 
-                // Don't forget to hide the masked ::before icon
-                visibility: hidden;
+                // Don't forget to hide the masked ::before icon,
+                // using display:none or visibility:hidden would break accessibility
+                &::before {
+                    content: none;
+                }
             }
 
             .mx_LeftPanel2_exploreButton {
diff --git a/src/RoomNotifsTypes.ts b/src/RoomNotifsTypes.ts
new file mode 100644
index 0000000000..0e7093e434
--- /dev/null
+++ b/src/RoomNotifsTypes.ts
@@ -0,0 +1,24 @@
+/*
+Copyright 2020 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.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import {
+    ALL_MESSAGES,
+    ALL_MESSAGES_LOUD,
+    MENTIONS_ONLY,
+    MUTE,
+} from "./RoomNotifs";
+
+export type Volume = ALL_MESSAGES_LOUD | ALL_MESSAGES | MENTIONS_ONLY | MUTE;
diff --git a/src/components/structures/ContextMenu.js b/src/components/structures/ContextMenu.js
index e43b0d1431..bda194ddd0 100644
--- a/src/components/structures/ContextMenu.js
+++ b/src/components/structures/ContextMenu.js
@@ -23,6 +23,8 @@ import classNames from 'classnames';
 import {Key} from "../../Keyboard";
 import * as sdk from "../../index";
 import AccessibleButton from "../views/elements/AccessibleButton";
+import StyledCheckbox from "../views/elements/StyledCheckbox";
+import StyledRadioButton from "../views/elements/StyledRadioButton";
 
 // Shamelessly ripped off Modal.js.  There's probably a better way
 // of doing reusable widgets like dialog boxes & menus where we go and
@@ -421,6 +423,54 @@ MenuItemCheckbox.propTypes = {
     onClick: PropTypes.func.isRequired,
 };
 
+// Semantic component for representing a styled role=menuitemcheckbox
+export const StyledMenuItemCheckbox = ({children, label, onChange, onClose, checked, disabled=false, ...props}) => {
+    const onKeyDown = (e) => {
+        if (e.key === Key.ENTER || e.key === Key.SPACE) {
+            e.stopPropagation();
+            e.preventDefault();
+            onChange();
+            // Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
+            if (e.key === Key.ENTER) {
+                onClose();
+            }
+        }
+    };
+    const onKeyUp = (e) => {
+        // prevent the input default handler as we handle it on keydown to match
+        // https://www.w3.org/TR/wai-aria-practices/examples/menubar/menubar-2/menubar-2.html
+        if (e.key === Key.SPACE || e.key === Key.ENTER) {
+            e.stopPropagation();
+            e.preventDefault();
+        }
+    };
+    return (
+        <StyledCheckbox
+            {...props}
+            role="menuitemcheckbox"
+            aria-checked={checked}
+            checked={checked}
+            aria-disabled={disabled}
+            tabIndex={-1}
+            aria-label={label}
+            onChange={onChange}
+            onKeyDown={onKeyDown}
+            onKeyUp={onKeyUp}
+        >
+            { children }
+        </StyledCheckbox>
+    );
+};
+StyledMenuItemCheckbox.propTypes = {
+    ...StyledCheckbox.propTypes,
+    label: PropTypes.string, // optional
+    checked: PropTypes.bool.isRequired,
+    disabled: PropTypes.bool, // optional
+    className: PropTypes.string, // optional
+    onChange: PropTypes.func.isRequired,
+    onClose: PropTypes.func.isRequired, // gets called after onChange on Key.ENTER
+};
+
 // Semantic component for representing a role=menuitemradio
 export const MenuItemRadio = ({children, label, active=false, disabled=false, ...props}) => {
     const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
@@ -439,6 +489,54 @@ MenuItemRadio.propTypes = {
     onClick: PropTypes.func.isRequired,
 };
 
+// Semantic component for representing a styled role=menuitemradio
+export const StyledMenuItemRadio = ({children, label, onChange, onClose, checked=false, disabled=false, ...props}) => {
+    const onKeyDown = (e) => {
+        if (e.key === Key.ENTER || e.key === Key.SPACE) {
+            e.stopPropagation();
+            e.preventDefault();
+            onChange();
+            // Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
+            if (e.key === Key.ENTER) {
+                onClose();
+            }
+        }
+    };
+    const onKeyUp = (e) => {
+        // prevent the input default handler as we handle it on keydown to match
+        // https://www.w3.org/TR/wai-aria-practices/examples/menubar/menubar-2/menubar-2.html
+        if (e.key === Key.SPACE || e.key === Key.ENTER) {
+            e.stopPropagation();
+            e.preventDefault();
+        }
+    };
+    return (
+        <StyledRadioButton
+            {...props}
+            role="menuitemradio"
+            aria-checked={checked}
+            checked={checked}
+            aria-disabled={disabled}
+            tabIndex={-1}
+            aria-label={label}
+            onChange={onChange}
+            onKeyDown={onKeyDown}
+            onKeyUp={onKeyUp}
+        >
+            { children }
+        </StyledRadioButton>
+    );
+};
+StyledMenuItemRadio.propTypes = {
+    ...StyledMenuItemRadio.propTypes,
+    label: PropTypes.string, // optional
+    checked: PropTypes.bool.isRequired,
+    disabled: PropTypes.bool, // optional
+    className: PropTypes.string, // optional
+    onChange: PropTypes.func.isRequired,
+    onClose: PropTypes.func.isRequired, // gets called after onChange on Key.ENTER
+};
+
 // Placement method for <ContextMenu /> to position context menu to right of elementRect with chevronOffset
 export const toRightOf = (elementRect, chevronOffset=12) => {
     const left = elementRect.right + window.pageXOffset + 3;
diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx
index 6c790f3318..4b954d7843 100644
--- a/src/components/structures/LeftPanel2.tsx
+++ b/src/components/structures/LeftPanel2.tsx
@@ -56,6 +56,15 @@ interface IState {
     showTagPanel: boolean;
 }
 
+// List of CSS classes which should be included in keyboard navigation within the room list
+const cssClasses = [
+    "mx_RoomSearch_input",
+    "mx_RoomSearch_icon", // minimized <RoomSearch />
+    "mx_RoomSublist2_headerText",
+    "mx_RoomTile2",
+    "mx_RoomSublist2_showNButton",
+];
+
 export default class LeftPanel2 extends React.Component<IProps, IState> {
     private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
     private tagPanelWatcherRef: string;
@@ -212,10 +221,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
             if (element) {
                 classes = element.classList;
             }
-        } while (element && !(
-            classes.contains("mx_RoomTile2") ||
-            classes.contains("mx_RoomSublist2_headerText") ||
-            classes.contains("mx_RoomSearch_input")));
+        } while (element && !cssClasses.some(c => classes.contains(c)));
 
         if (element) {
             element.focus();
@@ -246,7 +252,12 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
 
     private renderSearchExplore(): React.ReactNode {
         return (
-            <div className="mx_LeftPanel2_filterContainer" onFocus={this.onFocus} onBlur={this.onBlur}>
+            <div
+                className="mx_LeftPanel2_filterContainer"
+                onFocus={this.onFocus}
+                onBlur={this.onBlur}
+                onKeyDown={this.onKeyDown}
+            >
                 <RoomSearch
                     onQueryUpdate={this.onSearch}
                     isMinimized={this.props.isMinimized}
@@ -256,7 +267,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
                     // TODO fix the accessibility of this: https://github.com/vector-im/riot-web/issues/14180
                     className="mx_LeftPanel2_exploreButton"
                     onClick={this.onExplore}
-                    alt={_t("Explore rooms")}
+                    title={_t("Explore rooms")}
                 />
             </div>
         );
diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx
index 7ed2acf276..15f3bd5b54 100644
--- a/src/components/structures/RoomSearch.tsx
+++ b/src/components/structures/RoomSearch.tsx
@@ -149,7 +149,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
         let clearButton = (
             <AccessibleButton
                 tabIndex={-1}
-                className='mx_RoomSearch_clearButton'
+                title={_t("Clear filter")}
+                className="mx_RoomSearch_clearButton"
                 onClick={this.clearInput}
             />
         );
@@ -157,8 +158,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
         if (this.props.isMinimized) {
             icon = (
                 <AccessibleButton
-                    tabIndex={-1}
-                    className='mx_RoomSearch_icon'
+                    title={_t("Search rooms")}
+                    className="mx_RoomSearch_icon"
                     onClick={this.openSearch}
                 />
             );
diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx
index 5955a046a4..221ec07439 100644
--- a/src/components/structures/UserMenu.tsx
+++ b/src/components/structures/UserMenu.tsx
@@ -329,7 +329,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
                     className={classes}
                     onClick={this.onOpenMenuClick}
                     inputRef={this.buttonRef}
-                    label={_t("Account settings")}
+                    label={_t("User menu")}
                     isExpanded={!!this.state.contextMenuPosition}
                     onContextMenu={this.onContextMenu}
                 >
diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js
index 508691e5fd..53e8d0072b 100644
--- a/src/components/views/avatars/BaseAvatar.js
+++ b/src/components/views/avatars/BaseAvatar.js
@@ -132,7 +132,7 @@ const BaseAvatar = (props) => {
             );
         } else {
             return (
-                <span className="mx_BaseAvatar" ref={inputRef} {...otherProps}>
+                <span className="mx_BaseAvatar" ref={inputRef} {...otherProps} role="presentation">
                     { textNode }
                     { imgNode }
                 </span>
diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx
index 91bef0fc3d..0d7c3b8ec1 100644
--- a/src/components/views/rooms/RoomList2.tsx
+++ b/src/components/views/rooms/RoomList2.tsx
@@ -274,9 +274,6 @@ export default class RoomList2 extends React.Component<IProps, IState> {
                         className="mx_RoomList2"
                         role="tree"
                         aria-label={_t("Rooms")}
-                        // Firefox sometimes makes this element focusable due to
-                        // overflow:scroll;, so force it out of tab order.
-                        tabIndex={-1}
                     >{sublists}</div>
                 )}
             </RovingTabIndexProvider>
diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx
index 90ca8b2d4b..18f8f4e2f6 100644
--- a/src/components/views/rooms/RoomSublist2.tsx
+++ b/src/components/views/rooms/RoomSublist2.tsx
@@ -26,8 +26,12 @@ import AccessibleButton from "../../views/elements/AccessibleButton";
 import RoomTile2 from "./RoomTile2";
 import { ResizableBox, ResizeCallbackData } from "react-resizable";
 import { ListLayout } from "../../../stores/room-list/ListLayout";
-import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
-import StyledRadioButton from "../elements/StyledRadioButton";
+import {
+    ContextMenu,
+    ContextMenuButton,
+    StyledMenuItemCheckbox,
+    StyledMenuItemRadio,
+} from "../../structures/ContextMenu";
 import RoomListStore from "../../../stores/room-list/RoomListStore2";
 import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorithms/models";
 import { DefaultTagID, TagID } from "../../../stores/room-list/models";
@@ -62,7 +66,7 @@ interface IProps {
     onAddRoom?: () => void;
     addRoomLabel: string;
     isInvite: boolean;
-    layout: ListLayout;
+    layout?: ListLayout;
     isMinimized: boolean;
     tagId: TagID;
     onResize: () => void;
@@ -136,11 +140,13 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
     };
 
     private onShowAllClick = () => {
+        // TODO a11y keep focus somewhere useful: https://github.com/vector-im/riot-web/issues/14180
         this.props.layout.visibleTiles = this.props.layout.tilesWithPadding(this.numTiles, MAX_PADDING_HEIGHT);
         this.forceUpdate(); // because the layout doesn't trigger a re-render
     };
 
     private onShowLessClick = () => {
+        // TODO a11y keep focus somewhere useful: https://github.com/vector-im/riot-web/issues/14180
         this.props.layout.visibleTiles = this.props.layout.defaultVisibleTiles;
         this.forceUpdate(); // because the layout doesn't trigger a re-render
     };
@@ -203,6 +209,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
             dis.dispatch({
                 action: 'view_room',
                 room_id: room.roomId,
+                show_room_tile: true, // to make sure the room gets scrolled into view
             });
         }
     };
@@ -323,22 +330,24 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
                         <hr />
                         <div>
                             <div className='mx_RoomSublist2_contextMenu_title'>{_t("Unread rooms")}</div>
-                            <StyledCheckbox
+                            <StyledMenuItemCheckbox
+                                onClose={this.onCloseMenu}
                                 onChange={this.onUnreadFirstChanged}
                                 checked={isUnreadFirst}
                             >
                                 {_t("Always show first")}
-                            </StyledCheckbox>
+                            </StyledMenuItemCheckbox>
                         </div>
                         <hr />
                         <div>
                             <div className='mx_RoomSublist2_contextMenu_title'>{_t("Show")}</div>
-                            <StyledCheckbox
+                            <StyledMenuItemCheckbox
+                                onClose={this.onCloseMenu}
                                 onChange={this.onMessagePreviewChanged}
                                 checked={this.props.layout.showPreviews}
                             >
                                 {_t("Message preview")}
-                            </StyledCheckbox>
+                            </StyledMenuItemCheckbox>
                         </div>
                     </React.Fragment>
                 );
@@ -354,20 +363,22 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
                     <div className="mx_RoomSublist2_contextMenu">
                         <div>
                             <div className='mx_RoomSublist2_contextMenu_title'>{_t("Sort by")}</div>
-                            <StyledRadioButton
+                            <StyledMenuItemRadio
+                                onClose={this.onCloseMenu}
                                 onChange={() => this.onTagSortChanged(SortAlgorithm.Recent)}
                                 checked={!isAlphabetical}
                                 name={`mx_${this.props.tagId}_sortBy`}
                             >
                                 {_t("Activity")}
-                            </StyledRadioButton>
-                            <StyledRadioButton
+                            </StyledMenuItemRadio>
+                            <StyledMenuItemRadio
+                                onClose={this.onCloseMenu}
                                 onChange={() => this.onTagSortChanged(SortAlgorithm.Alphabetic)}
                                 checked={isAlphabetical}
                                 name={`mx_${this.props.tagId}_sortBy`}
                             >
                                 {_t("A-Z")}
-                            </StyledRadioButton>
+                            </StyledMenuItemRadio>
                         </div>
                         {otherSections}
                     </div>
@@ -390,16 +401,22 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
 
     private renderHeader(): React.ReactElement {
         return (
-            <RovingTabIndexWrapper>
+            <RovingTabIndexWrapper inputRef={this.headerButton}>
                 {({onFocus, isActive, ref}) => {
                     const tabIndex = isActive ? 0 : -1;
 
+                    let ariaLabel = _t("Jump to first unread room.");
+                    if (this.props.tagId === DefaultTagID.Invite) {
+                        ariaLabel = _t("Jump to first invite.");
+                    }
+
                     const badge = (
                         <NotificationBadge
                             forceCount={true}
                             notification={this.state.notificationState}
                             onClick={this.onBadgeClick}
                             tabIndex={tabIndex}
+                            aria-label={ariaLabel}
                         />
                     );
 
@@ -440,7 +457,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
                     // doesn't become sticky.
                     // The same applies to the notification badge.
                     return (
-                        <div className={classes} onKeyDown={this.onHeaderKeyDown} onFocus={onFocus}>
+                        <div className={classes} onKeyDown={this.onHeaderKeyDown} onFocus={onFocus} aria-label={this.props.label}>
                             <div className="mx_RoomSublist2_stickable">
                                 <AccessibleButton
                                     onFocus={onFocus}
@@ -448,6 +465,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
                                     tabIndex={tabIndex}
                                     className="mx_RoomSublist2_headerText"
                                     role="treeitem"
+                                    aria-expanded={!this.props.layout || !this.props.layout.isCollapsed}
                                     aria-level={1}
                                     onClick={this.onHeaderClick}
                                     onContextMenu={this.onContextMenu}
@@ -503,12 +521,12 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
                 );
                 if (this.props.isMinimized) showMoreText = null;
                 showNButton = (
-                    <div onClick={this.onShowAllClick} className={showMoreBtnClasses}>
+                    <AccessibleButton onClick={this.onShowAllClick} className={showMoreBtnClasses} tabIndex={-1}>
                         <span className='mx_RoomSublist2_showMoreButtonChevron mx_RoomSublist2_showNButtonChevron'>
                             {/* set by CSS masking */}
                         </span>
                         {showMoreText}
-                    </div>
+                    </AccessibleButton>
                 );
             } else if (this.numTiles <= visibleTiles.length && this.numTiles > this.props.layout.defaultVisibleTiles) {
                 // we have all tiles visible - add a button to show less
@@ -518,13 +536,14 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
                     </span>
                 );
                 if (this.props.isMinimized) showLessText = null;
+                // TODO Roving tab index / treeitem?: https://github.com/vector-im/riot-web/issues/14180
                 showNButton = (
-                    <div onClick={this.onShowLessClick} className={showMoreBtnClasses}>
+                    <AccessibleButton onClick={this.onShowLessClick} className={showMoreBtnClasses} tabIndex={-1}>
                         <span className='mx_RoomSublist2_showLessButtonChevron mx_RoomSublist2_showNButtonChevron'>
                             {/* set by CSS masking */}
                         </span>
                         {showLessText}
-                    </div>
+                    </AccessibleButton>
                 );
             }
 
diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx
index 4af9ef7966..c6cd401803 100644
--- a/src/components/views/rooms/RoomTile2.tsx
+++ b/src/components/views/rooms/RoomTile2.tsx
@@ -26,17 +26,30 @@ import dis from '../../../dispatcher/dispatcher';
 import { Key } from "../../../Keyboard";
 import ActiveRoomObserver from "../../../ActiveRoomObserver";
 import { _t } from "../../../languageHandler";
-import { ContextMenu, ContextMenuButton, MenuItemRadio } from "../../structures/ContextMenu";
+import {
+    ContextMenu,
+    ContextMenuButton,
+    MenuItemRadio,
+    MenuItemCheckbox,
+    MenuItem,
+} from "../../structures/ContextMenu";
 import { DefaultTagID, TagID } from "../../../stores/room-list/models";
 import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
 import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
-import { getRoomNotifsState, ALL_MESSAGES, ALL_MESSAGES_LOUD, MENTIONS_ONLY, MUTE } from "../../../RoomNotifs";
+import {
+    getRoomNotifsState,
+    setRoomNotifsState,
+    ALL_MESSAGES,
+    ALL_MESSAGES_LOUD,
+    MENTIONS_ONLY,
+    MUTE,
+} from "../../../RoomNotifs";
 import { MatrixClientPeg } from "../../../MatrixClientPeg";
-import { setRoomNotifsState } from "../../../RoomNotifs";
 import { TagSpecificNotificationState } from "../../../stores/notifications/TagSpecificNotificationState";
 import { INotificationState } from "../../../stores/notifications/INotificationState";
 import NotificationBadge from "./NotificationBadge";
 import { NotificationColor } from "../../../stores/notifications/NotificationColor";
+import { Volume } from "../../../RoomNotifsTypes";
 
 // TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
 // TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
@@ -68,6 +81,8 @@ interface IState {
     generalMenuPosition: PartialDOMRect;
 }
 
+const messagePreviewId = (roomId: string) => `mx_RoomTile2_messagePreview_${roomId}`;
+
 const contextMenuBelow = (elementRect: PartialDOMRect) => {
     // align the context menu's icons with the icon which opened the context menu
     const left = elementRect.left + window.pageXOffset - 9;
@@ -123,6 +138,10 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
         return !this.props.isMinimized && this.props.tag !== DefaultTagID.Invite;
     }
 
+    private get showMessagePreview(): boolean {
+        return !this.props.isMinimized && this.props.showMessagePreview;
+    }
+
     public componentWillUnmount() {
         if (this.props.room) {
             ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
@@ -195,6 +214,11 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
 
         // TODO: Support tagging: https://github.com/vector-im/riot-web/issues/14211
         // TODO: XOR favourites and low priority: https://github.com/vector-im/riot-web/issues/14210
+
+        if ((ev as React.KeyboardEvent).key === Key.ENTER) {
+            // Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
+            this.setState({generalMenuPosition: null}); // hide the menu
+        }
     };
 
     private onLeaveRoomClick = (ev: ButtonEvent) => {
@@ -219,11 +243,13 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
         this.setState({generalMenuPosition: null}); // hide the menu
     };
 
-    private async saveNotifState(ev: ButtonEvent, newState: ALL_MESSAGES_LOUD | ALL_MESSAGES | MENTIONS_ONLY | MUTE) {
+    private async saveNotifState(ev: ButtonEvent, newState: Volume) {
         ev.preventDefault();
         ev.stopPropagation();
         if (MatrixClientPeg.get().isGuest()) return;
 
+        // get key before we go async and React discards the nativeEvent
+        const key = (ev as React.KeyboardEvent).key;
         try {
             // TODO add local echo - https://github.com/vector-im/riot-web/issues/14280
             await setRoomNotifsState(this.props.room.roomId, newState);
@@ -233,7 +259,10 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
             console.error(error);
         }
 
-        this.setState({notificationsMenuPosition: null}); // Close the context menu
+        if (key === Key.ENTER) {
+            // Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
+            this.setState({notificationsMenuPosition: null}); // hide the menu
+        }
     }
 
     private onClickAllNotifs = ev => this.saveNotifState(ev, ALL_MESSAGES);
@@ -322,20 +351,24 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
                 <ContextMenu {...contextMenuBelow(this.state.generalMenuPosition)} onFinished={this.onCloseGeneralMenu}>
                     <div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile2_contextMenu">
                         <div className="mx_IconizedContextMenu_optionList">
-                            <AccessibleButton onClick={(e) => this.onTagRoom(e, DefaultTagID.Favourite)}>
+                            <MenuItemCheckbox
+                                onClick={(e) => this.onTagRoom(e, DefaultTagID.Favourite)}
+                                active={false} // TODO: https://github.com/vector-im/riot-web/issues/14283
+                                label={_t("Favourite")}
+                            >
                                 <span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconStar" />
                                 <span className="mx_IconizedContextMenu_label">{_t("Favourite")}</span>
-                            </AccessibleButton>
-                            <AccessibleButton onClick={this.onOpenRoomSettings}>
+                            </MenuItemCheckbox>
+                            <MenuItem onClick={this.onOpenRoomSettings} label={_t("Settings")}>
                                 <span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSettings" />
                                 <span className="mx_IconizedContextMenu_label">{_t("Settings")}</span>
-                            </AccessibleButton>
+                            </MenuItem>
                         </div>
                         <div className="mx_IconizedContextMenu_optionList mx_RoomTile2_contextMenu_redRow">
-                            <AccessibleButton onClick={this.onLeaveRoomClick}>
+                            <MenuItem onClick={this.onLeaveRoomClick} label={_t("Leave Room")}>
                                 <span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSignOut" />
                                 <span className="mx_IconizedContextMenu_label">{_t("Leave Room")}</span>
-                            </AccessibleButton>
+                            </MenuItem>
                         </div>
                     </div>
                 </ContextMenu>
@@ -375,8 +408,9 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
 
         let badge: React.ReactNode;
         if (!this.props.isMinimized) {
+            // aria-hidden because we summarise the unread count/highlight status in a manual aria-label below
             badge = (
-                <div className="mx_RoomTile2_badgeContainer">
+                <div className="mx_RoomTile2_badgeContainer" aria-hidden="true">
                     <NotificationBadge
                         notification={this.state.notificationState}
                         forceCount={false}
@@ -392,24 +426,25 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
         name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
 
         let messagePreview = null;
-        if (this.props.showMessagePreview && !this.props.isMinimized) {
+        if (this.showMessagePreview) {
             // The preview store heavily caches this info, so should be safe to hammer.
             const text = MessagePreviewStore.instance.getPreviewForRoom(this.props.room, this.props.tag);
 
             // Only show the preview if there is one to show.
             if (text) {
                 messagePreview = (
-                    <div className="mx_RoomTile2_messagePreview">
+                    <div className="mx_RoomTile2_messagePreview" id={messagePreviewId(this.props.room.roomId)}>
                         {text}
                     </div>
                 );
             }
         }
 
+        const notificationColor = this.state.notificationState.color;
         const nameClasses = classNames({
             "mx_RoomTile2_name": true,
             "mx_RoomTile2_nameWithPreview": !!messagePreview,
-            "mx_RoomTile2_nameHasUnreadEvents": this.state.notificationState.color >= NotificationColor.Bold,
+            "mx_RoomTile2_nameHasUnreadEvents": notificationColor >= NotificationColor.Bold,
         });
 
         let nameContainer = (
@@ -422,6 +457,27 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
         );
         if (this.props.isMinimized) nameContainer = null;
 
+        let ariaLabel = name;
+        // The following labels are written in such a fashion to increase screen reader efficiency (speed).
+        if (this.props.tag === DefaultTagID.Invite) {
+            // append nothing
+        } else if (notificationColor >= NotificationColor.Red) {
+            ariaLabel += " " + _t("%(count)s unread messages including mentions.", {
+                count: this.state.notificationState.count,
+            });
+        } else if (notificationColor >= NotificationColor.Grey) {
+            ariaLabel += " " + _t("%(count)s unread messages.", {
+                count: this.state.notificationState.count,
+            });
+        } else if (notificationColor >= NotificationColor.Bold) {
+            ariaLabel += " " + _t("Unread messages.");
+        }
+
+        let ariaDescribedBy: string;
+        if (this.showMessagePreview) {
+            ariaDescribedBy = messagePreviewId(this.props.room.roomId);
+        }
+
         return (
             <React.Fragment>
                 <RovingTabIndexWrapper>
@@ -434,8 +490,11 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
                             onMouseEnter={this.onTileMouseEnter}
                             onMouseLeave={this.onTileMouseLeave}
                             onClick={this.onTileClick}
-                            role="treeitem"
                             onContextMenu={this.onContextMenu}
+                            role="treeitem"
+                            aria-label={ariaLabel}
+                            aria-selected={this.state.selected}
+                            aria-describedby={ariaDescribedBy}
                         >
                             {roomAvatar}
                             {nameContainer}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index b551805184..f16fb38f86 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1208,6 +1208,8 @@
     "Activity": "Activity",
     "A-Z": "A-Z",
     "List options": "List options",
+    "Jump to first unread room.": "Jump to first unread room.",
+    "Jump to first invite.": "Jump to first invite.",
     "Add room": "Add room",
     "Show %(count)s more|other": "Show %(count)s more",
     "Show %(count)s more|one": "Show %(count)s more",
@@ -2089,6 +2091,8 @@
     "Find a room…": "Find a room…",
     "Find a room… (e.g. %(exampleRoom)s)": "Find a room… (e.g. %(exampleRoom)s)",
     "If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.",
+    "Clear filter": "Clear filter",
+    "Search rooms": "Search rooms",
     "You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.": "You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.",
     "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.",
     "Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.",
@@ -2100,8 +2104,6 @@
     "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.",
     "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?",
     "Search failed": "Search failed",
@@ -2116,7 +2118,6 @@
     "Click to mute video": "Click to mute video",
     "Click to unmute audio": "Click to unmute audio",
     "Click to mute audio": "Click to mute audio",
-    "Clear filter": "Clear filter",
     "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.",
     "Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.",
     "Failed to load timeline position": "Failed to load timeline position",
@@ -2131,7 +2132,7 @@
     "All settings": "All settings",
     "Archived rooms": "Archived rooms",
     "Feedback": "Feedback",
-    "Account settings": "Account settings",
+    "User menu": "User menu",
     "Could not load user profile": "Could not load user profile",
     "Verify this login": "Verify this login",
     "Session verified": "Session verified",