diff --git a/res/css/views/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss
index 50d376a66f..7a5d69fc8a 100644
--- a/res/css/views/rooms/_RoomTile2.scss
+++ b/res/css/views/rooms/_RoomTile2.scss
@@ -21,6 +21,10 @@ limitations under the License.
     margin-bottom: 4px;
     padding: 4px;
 
+    // allow scrollIntoView to ignore the sticky headers, must match combined height of .mx_RoomSublist2_headerContainer
+    scroll-margin-top: 32px;
+    scroll-margin-bottom: 32px;
+
     // The tile is also a flexbox row itself
     display: flex;
 
@@ -165,6 +169,11 @@ limitations under the License.
     }
 }
 
+// do not apply scroll-margin-bottom to the sublist which will not have a sticky header below it
+.mx_RoomSublist2:last-child .mx_RoomTile2 {
+    scroll-margin-bottom: 0;
+}
+
 // We use these both in context menus and the room tiles
 .mx_RoomTile2_iconBell::before {
     mask-image: url('$(res)/img/feather-customised/bell.svg');
diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx
index bb61769f60..a45631553b 100644
--- a/src/components/views/rooms/RoomSublist2.tsx
+++ b/src/components/views/rooms/RoomSublist2.tsx
@@ -42,6 +42,8 @@ import { ListNotificationState } from "../../../stores/notifications/ListNotific
 import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
 import { Key } from "../../../Keyboard";
 import StyledCheckbox from "../elements/StyledCheckbox";
+import defaultDispatcher from "../../../dispatcher/dispatcher";
+import {ActionPayload} from "../../../dispatcher/payloads";
 
 // 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
@@ -90,6 +92,7 @@ interface IState {
 export default class RoomSublist2 extends React.Component<IProps, IState> {
     private headerButton = createRef<HTMLDivElement>();
     private sublistRef = createRef<HTMLDivElement>();
+    private dispatcherRef: string;
 
     constructor(props: IProps) {
         super(props);
@@ -100,6 +103,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
             isResizing: false,
         };
         this.state.notificationState.setRooms(this.props.rooms);
+        this.dispatcherRef = defaultDispatcher.register(this.onAction);
     }
 
     private get numTiles(): number {
@@ -118,8 +122,29 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
 
     public componentWillUnmount() {
         this.state.notificationState.destroy();
+        defaultDispatcher.unregister(this.dispatcherRef);
     }
 
+    private onAction = (payload: ActionPayload) => {
+        if (payload.action === "view_room" && payload.show_room_tile && this.props.rooms) {
+            // XXX: we have to do this a tick later because we have incorrect intermediate props during a room change
+            // where we lose the room we are changing from temporarily and then it comes back in an update right after.
+            setImmediate(() => {
+                const isCollapsed = this.props.layout.isCollapsed;
+                const roomIndex = this.props.rooms.findIndex((r) => r.roomId === payload.room_id);
+
+                if (isCollapsed && roomIndex > -1) {
+                    this.toggleCollapsed();
+                }
+                // extend the visible section to include the room if it is entirely invisible
+                if (roomIndex >= this.numVisibleTiles) {
+                    this.props.layout.visibleTiles = this.props.layout.tilesWithPadding(roomIndex + 1, MAX_PADDING_HEIGHT);
+                    this.forceUpdate(); // because the layout doesn't trigger a re-render
+                }
+            });
+        }
+    };
+
     private onAddRoom = (e) => {
         e.stopPropagation();
         if (this.props.onAddRoom) this.props.onAddRoom();
diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx
index 35167e3cf5..bba8ab15ba 100644
--- a/src/components/views/rooms/RoomTile2.tsx
+++ b/src/components/views/rooms/RoomTile2.tsx
@@ -17,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 { Room } from "matrix-js-sdk/src/models/room";
 import classNames from "classnames";
 import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
@@ -51,6 +51,8 @@ import { INotificationState } from "../../../stores/notifications/INotificationS
 import NotificationBadge from "./NotificationBadge";
 import { NotificationColor } from "../../../stores/notifications/NotificationColor";
 import { Volume } from "../../../RoomNotifsTypes";
+import defaultDispatcher from "../../../dispatcher/dispatcher";
+import {ActionPayload} from "../../../dispatcher/payloads";
 
 // 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
@@ -119,6 +121,10 @@ const NotifOption: React.FC<INotifOptionProps> = ({active, onClick, iconClassNam
 };
 
 export default class RoomTile2 extends React.Component<IProps, IState> {
+    private dispatcherRef: string;
+    private roomTileRef = createRef<HTMLDivElement>();
+    // TODO: a11y: https://github.com/vector-im/riot-web/issues/14180
+
     constructor(props: IProps) {
         super(props);
 
@@ -131,6 +137,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
         };
 
         ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate);
+        this.dispatcherRef = defaultDispatcher.register(this.onAction);
     }
 
     private get showContextMenu(): boolean {
@@ -141,12 +148,36 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
         return !this.props.isMinimized && this.props.showMessagePreview;
     }
 
+    public componentDidMount() {
+        // when we're first rendered (or our sublist is expanded) make sure we are visible if we're active
+        if (this.state.selected) {
+            this.scrollIntoView();
+        }
+    }
+
     public componentWillUnmount() {
         if (this.props.room) {
             ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
         }
+        defaultDispatcher.unregister(this.dispatcherRef);
     }
 
+    private onAction = (payload: ActionPayload) => {
+        if (payload.action === "view_room" && payload.room_id === this.props.room.roomId && payload.show_room_tile) {
+            setImmediate(() => {
+                this.scrollIntoView();
+            });
+        }
+    };
+
+    private scrollIntoView = () => {
+        if (!this.roomTileRef.current) return;
+        this.roomTileRef.current.scrollIntoView({
+            block: "nearest",
+            behavior: "auto",
+        });
+    };
+
     private onTileMouseEnter = () => {
         this.setState({hover: true});
     };
@@ -160,7 +191,6 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
         ev.stopPropagation();
         dis.dispatch({
             action: 'view_room',
-            // TODO: Support show_room_tile in new room list: https://github.com/vector-im/riot-web/issues/14233
             show_room_tile: true, // make sure the room is visible in the list
             room_id: this.props.room.roomId,
             clear_search: (ev && (ev.key === Key.ENTER || ev.key === Key.SPACE)),
@@ -478,7 +508,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
 
         return (
             <React.Fragment>
-                <RovingTabIndexWrapper>
+                <RovingTabIndexWrapper inputRef={this.roomTileRef}>
                     {({onFocus, isActive, ref}) =>
                         <AccessibleButton
                             onFocus={onFocus}