# Collapsible Subcategories in Sidebar

**URL:** https://meta.discourse.org/t/collapsible-subcategories-in-sidebar/366657
**Category:** Support
**Tags:** sidebar
**Created:** [May 19, 2025, 6:00am UTC](https://meta.discourse.org/t/collapsible-subcategories-in-sidebar/366657 "2025-05-19T06:00:58Z")
**Posts on this page:** 4
**Page:** 1

<div class="post-metadata">

### Author: ![alkah3st](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/alkah3st/32/503573_2.png) [@alkah3st](https://meta.discourse.org/u/alkah3st)
#### Post date: [May 19, 2025, 6:00am UTC](https://meta.discourse.org/t/collapsible-subcategories-in-sidebar/366657/1 "2025-05-19T06:00:58Z")

</div>

I’ve seen a few posts about this but none have definitive answers.

Does the Unreal Engine Discourse (featured on the Discourse “Discover” page) have a completely custom theme, or are there components that can achieve this?

> **[Topics tagged unreal-engine](https://forums.unrealengine.com/tags/c/development-discussion/11/unreal-engine/705)**
>
> Topics tagged unreal-engine

I also notice they display the subcategories as top level links when you click into one, and then beneath it lists all the topics. It looks like what’s happening here is setting “Subcategory List Style” to “Rows” on the parent category to achieve that?

---

<div class="post-metadata">

### Author: ![alkah3st](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/alkah3st/32/503573_2.png) [@alkah3st](https://meta.discourse.org/u/alkah3st)
#### Post date: [May 19, 2025, 6:17am UTC](https://meta.discourse.org/t/collapsible-subcategories-in-sidebar/366657/3 "2025-05-19T06:17:47Z")

</div>

It looks like this may not be something solvable with a component.

However I was able to generate JS and CSS that achieves this with a little AI help, if anyone is interested I am happy to share.

---

<div class="post-metadata">

### Author: ![nathank](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/nathank/32/290039_2.png) [@nathank](https://meta.discourse.org/u/nathank)
#### Post date: [May 19, 2025, 10:22am UTC](https://meta.discourse.org/t/collapsible-subcategories-in-sidebar/366657/4 "2025-05-19T10:22:48Z")

</div>

You’ve seen this I take it:

> [@What is the name of this theme?](https://meta.discourse.org/t/what-is-the-name-of-this-theme/232404):
>
> What is the name of Unreal Engine’s community theme? Or how can i make a theme like that? [Unreal Engine Forum](https://forums.unrealengine.com/)

And this:

> [@What Themes are being used for these Discourse forums?](https://meta.discourse.org/t/what-themes-are-being-used-for-these-discourse-forums/275550/5):
>
> This is a heavily customised forum. Something like this will use plugins as well as theme modifications. This is something that our [partners](https://discourse.org/partners) can help you with; Discourse can help too of course, if you are on the Enterprise plans slight_smile

By all means, do share your code. Of course, the challenge with things like this is the ongoing maintenance.

---

<div class="post-metadata">

### Author: ![alkah3st](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/alkah3st/32/503573_2.png) [@alkah3st](https://meta.discourse.org/u/alkah3st)
#### Post date: [May 19, 2025, 3:57pm UTC](https://meta.discourse.org/t/collapsible-subcategories-in-sidebar/366657/5 "2025-05-19T15:57:47Z")

</div>

Yeah, it seems a lot of what they’re doing is custom stuff.

My script looks like this:

```plaintext
import { apiInitializer } from "discourse/lib/api";
import { ajax } from "discourse/lib/ajax";

export default apiInitializer("0.11.1", (api) => {
  ajax("/site.json").then((data) => {
    const categories = data.categories;

    // Build map of parent_id => [child_category_ids]
    const childMap = {};
    categories.forEach((cat) => {
      if (cat.parent_category_id) {
        if (!childMap[cat.parent_category_id]) {
          childMap[cat.parent_category_id] = [];
        }
        childMap[cat.parent_category_id].push(cat.id);
      }
    });

    const collapseState = {}; // Tracks collapse state per parent

    function applyCollapseState(parentId, childIds, collapsed) {
      childIds.forEach((childId) => {
        const childEl = document.querySelector(
          `.sidebar-section-link-wrapper[data-category-id="${childId}"]`
        );
        if (childEl) {
          childEl.classList.toggle("is-collapsed", collapsed);
          childEl.classList.add("is-subcategory");
        }
      });
    }

    function ensureToggleExists(parentId, childIds) {
      const parentEl = document.querySelector(
        `.sidebar-section-link-wrapper[data-category-id="${parentId}"]`
      );
      if (!parentEl || parentEl.classList.contains("has-toggle")) return;

      const toggle = document.createElement("span");
      toggle.innerText = collapseState[parentId] ? "▸" : "▾";
      toggle.className = "toggle-subcategories";
      toggle.style.cursor = "pointer";
      toggle.style.marginLeft = "0.5em";

      toggle.onclick = () => {
        const isNowCollapsed = !collapseState[parentId];
        applyCollapseState(parentId, childIds, isNowCollapsed);
        toggle.innerText = isNowCollapsed ? "▸" : "▾";
        collapseState[parentId] = isNowCollapsed;
      };

      parentEl.classList.add("has-toggle");
      const link = parentEl.querySelector(".sidebar-section-link");
      if (link) link.appendChild(toggle);
    }

    api.onPageChange(() => {
      Object.entries(childMap).forEach(([parentId, childIds]) => {
        // Set default collapse state
        if (collapseState[parentId] === undefined) {
          collapseState[parentId] = true;
        }

        applyCollapseState(parentId, childIds, collapseState[parentId]);
        ensureToggleExists(parentId, childIds);
      });

      const container = document.querySelector('[data-section-name="categories"]');
      if (!container) return;

      const observer = new MutationObserver(() => {
        Object.entries(childMap).forEach(([parentId, childIds]) => {
          applyCollapseState(parentId, childIds, collapseState[parentId]);
          ensureToggleExists(parentId, childIds); // <-- Restore toggles
        });
      });

      observer.observe(container, {
        childList: true,
        subtree: true,
      });
    });
  });
});

```

I just dropped that into the theme’s JS panel. The relevant CSS:

```plaintext
.sidebar-section-link-wrapper.is-collapsed {
  display: none !important;
}

.sidebar-section-link-wrapper.is-subcategory {
  padding-left: 1.5em;
}

.toggle-subcategories {
    float: right;
    display: flex;
    width: 30px;
    align-items: center;
    font-size: .9em;
    line-height: 1;
    height: 30px;
}

```

It works well enough for my purposes but I wouldn’t recommend this if you have lots of subcategories.
