diff --git a/res/css/views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss b/res/css/views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss
index ac85782bbd0ac2f0e5f06ad1356dd880640716cc..42b2fd42caf80547fe9630b0cd992da6c437daeb 100644
--- a/res/css/views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss
+++ b/res/css/views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss
@@ -6,7 +6,39 @@
  */
 
 .mx_RoomListPrimaryFilters {
-    margin: unset;
-    list-style-type: none;
-    padding: var(--cpd-space-2x) var(--cpd-space-3x);
+    padding: var(--cpd-space-2x) var(--cpd-space-4x) var(--cpd-space-2x) var(--cpd-space-3x);
+    min-height: 46px;
+    max-height: 46px;
+    overflow: hidden;
+    box-sizing: border-box;
+
+    &[data-expanded="true"] {
+        min-height: unset;
+        max-height: unset;
+        overflow: unset;
+    }
+
+    ul {
+        margin: unset;
+        padding: unset;
+        list-style-type: none;
+        /**
+         * The InteractionObserver needs the height to be set for to work properly.
+         */
+        height: 100%;
+    }
+
+    .mx_RoomListPrimaryFilters_IconButton {
+        background-color: var(--cpd-color-bg-subtle-secondary);
+
+        svg {
+            transition: transform 0.1s linear;
+        }
+    }
+
+    .mx_RoomListPrimaryFilters_IconButton[aria-expanded="true"] {
+        svg {
+            transform: rotate(180deg);
+        }
+    }
 }
diff --git a/src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters.tsx b/src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters.tsx
index ebf972d36137f55f7c7c66a4267b5f13671c0f82..70b278d4e952144961268c6314fe40f7aa623b4d 100644
--- a/src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters.tsx
+++ b/src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters.tsx
@@ -5,12 +5,14 @@
  * Please see LICENSE files in the repository root for full details.
  */
 
-import React, { type JSX } from "react";
-import { ChatFilter } from "@vector-im/compound-web";
+import React, { type JSX, useEffect, useId, useState } from "react";
+import { ChatFilter, IconButton } from "@vector-im/compound-web";
+import ChevronDownIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-down";
 
 import type { RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel";
 import { Flex } from "../../../utils/Flex";
 import { _t } from "../../../../languageHandler";
+import { useIsNodeVisible } from "../../../../hooks/useIsNodeVisible";
 
 interface RoomListPrimaryFiltersProps {
     /**
@@ -23,23 +25,105 @@ interface RoomListPrimaryFiltersProps {
  * The primary filters for the room list
  */
 export function RoomListPrimaryFilters({ vm }: RoomListPrimaryFiltersProps): JSX.Element {
+    const id = useId();
+    const [isExpanded, setIsExpanded] = useState(false);
+
+    // threshold: 0.5 means that the filter is considered visible if at least 50% of it is visible
+    // this value is arbitrary, we want we to have a bit of flexibility
+    const { isVisible, rootRef, nodeRef } = useIsNodeVisible<HTMLLIElement, HTMLUListElement>({ threshold: 0.5 });
+    const { filters, onFilterChange } = useFilters(vm.primaryFilters, isExpanded, isVisible);
+
     return (
-        <Flex
-            as="ul"
-            role="listbox"
-            aria-label={_t("room_list|primary_filters")}
-            className="mx_RoomListPrimaryFilters"
-            align="center"
-            gap="var(--cpd-space-2x)"
-            wrap="wrap"
-        >
-            {vm.primaryFilters.map((filter) => (
-                <li role="option" aria-selected={filter.active} key={filter.name}>
-                    <ChatFilter selected={filter.active} onClick={filter.toggle}>
-                        {filter.name}
-                    </ChatFilter>
-                </li>
-            ))}
+        <Flex id={id} className="mx_RoomListPrimaryFilters" gap="var(--cpd-space-3x)" data-expanded={isExpanded}>
+            <Flex
+                as="ul"
+                role="listbox"
+                aria-label={_t("room_list|primary_filters")}
+                align="center"
+                gap="var(--cpd-space-2x)"
+                wrap="wrap"
+                ref={rootRef}
+            >
+                {filters.map((filter) => (
+                    <li
+                        ref={filter.active ? nodeRef : undefined}
+                        role="option"
+                        aria-selected={filter.active}
+                        key={filter.name}
+                    >
+                        <ChatFilter
+                            selected={filter.active}
+                            onClick={() => {
+                                onFilterChange();
+                                filter.toggle();
+                            }}
+                        >
+                            {filter.name}
+                        </ChatFilter>
+                    </li>
+                ))}
+            </Flex>
+            <IconButton
+                aria-expanded={isExpanded}
+                aria-controls={id}
+                className="mx_RoomListPrimaryFilters_IconButton"
+                aria-label={_t("room_list|room_options")}
+                size="28px"
+                onClick={() => setIsExpanded((_expanded) => !_expanded)}
+            >
+                <ChevronDownIcon color="var(--cpd-color-icon-secondary)" />
+            </IconButton>
         </Flex>
     );
 }
+
+/**
+ * A hook to sort the filters by active state.
+ * The list is sorted if the current filter is not visible when the list is unexpanded.
+ *
+ * @param filters - the list of filters to sort.
+ * @param isExpanded - the filter is expanded or not (fully visible).
+ * @param isVisible - `null` if there is not selected filter. `true` or `false` if the filter is visible or not.
+ */
+function useFilters(
+    filters: RoomListViewState["primaryFilters"],
+    isExpanded: boolean,
+    isVisible: boolean | null,
+): {
+    /**
+     * The new list of filters.
+     */
+    filters: RoomListViewState["primaryFilters"];
+    /**
+     * Reset the filter sorting when called.
+     */
+    onFilterChange: () => void;
+} {
+    // By default, the filters are not sorted
+    const [filterState, setFilterState] = useState({ filters, isSorted: false });
+
+    useEffect(() => {
+        // If there is no current filter (isVisible is null)
+        // or if the filter list is fully visible (isExpanded is true)
+        // or if the current filter is visible and the list isn't sorted
+        // then we don't need to sort the filters
+        if (isVisible === null || isExpanded || (isVisible && !filterState.isSorted)) {
+            setFilterState({ filters, isSorted: false });
+            return;
+        }
+
+        // Sort the filters with the current filter at first position
+        setFilterState({
+            filters: filters
+                .slice()
+                .sort((filterA, filterB) => (filterA.active === filterB.active ? 0 : filterA.active ? -1 : 1)),
+            isSorted: true,
+        });
+    }, [filters, isVisible, filterState.isSorted, isExpanded]);
+
+    const onFilterChange = (): void => {
+        // Reset the filter sorting
+        setFilterState({ filters, isSorted: false });
+    };
+    return { filters: filterState.filters, onFilterChange };
+}