Category Groups

:discourse2: Summary Category Groups allows you to assign categories to groups, which are then used to sort and collapse categories on the categories page.
:hammer_and_wrench: Repository Link
:open_book: New to Discourse Themes? Beginner’s guide to using Discourse Themes

Install this theme component

:warning: Important things to note:

  • This only works with the site setting desktop category page style set to boxes with subcategories (as it replaces that category page template).

  • The category collapsing works using your browser’s local storage, so the collapsed state will not be remembered between different devices.

:wrench: This theme comes with some settings:

  • category groups - this is how the groups are configured, the setting is formatted as: Category group name: category-slug, category-slug-2, category-slug-3. The category slug is the name of the category as it’s seen in URLs… usually all lowercase with no spaces.

  • show on mobile - if you want to use default category page on mobile, disable this

  • show ungrouped - this will show all categories not assigned to a group in an “Other” group. The name of this group is configurable.

  • fancy styling - these are some custom styles I added, they stray from our defaults so if you’re working on your own theme you may want to disable this.


This is pretty cool. Is there a way to show a subcategory? Ie

My main Parent category
Main slug)

Has 2 subs
sponsors (slug)

I have hidden the subs in Hamburger.

I would like to create a collapsable Category header
Main: sitenews, sponsors

When I try that the subs do not show

Url looks like c/main/sitenews

1 Like

Is there a way to mod this to show Subcategories in Boxes instead of the Parent Category?

1 Like

No, this will only work with top level categories at the moment (but the category box will show the related subcategories as it does by default).


Thanks still an awesome component. I am guessing to get that added functionality would need a more complete category view replacement?

Very clean-looking!


  • As already mentioned, ability to list subcategories directly on categories page would be ideal.
  • Ability to set default collapse state.
  • Remember user’s collapse state in account settings.
  • Subcategories listed inside grouped parent category are grouped as “Other” - should be grouped as parent category’s group.

Thanks for this TC!


There is another component that can group categories and/or subcategories grouped under headers. It is in the Air Full Theme called Modern Category Boxes.

However it could use the collapsible feature and more importantly @awesomerobot 's really cool option to setup category lists for groups.

As for the collapsed stated being remembered woild require additional work as Kris mentioned that this one uses the browser and as such cannot remember last state.

1 Like

This looks excellent. I’m getting a error message though.

_vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:4069 Uncaught TypeError: Cannot read properties of undefined (reading ‘indexOf’)
at a01db7fd83ffd5f3e61e767c85767c217dfa7e0b.js?
at _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:39290
at n.t.Mixin.create.c.forEach (_vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:39269)
at n.t.Mixin.create.c.filter (_vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:39289)
at a01db7fd83ffd5f3e61e767c85767c217dfa7e0b.js?
at Array.forEach ()
at n.catGroupList (a01db7fd83ffd5f3e61e767c85767c217dfa7e0b.js?
at n. (_application-ecf386baee119f56a21f6f58e1d96e70e4a129de99451ee32e6eb4304d6749fa.js:1980)
at n.i.get (_vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:28802)
at Je (_vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:27908)
(anonymous) @ a01db7fd83ffd5f3e61e767c85767c217dfa7e0b.js?
(anonymous) @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:39290
t.Mixin.create.c.forEach @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:39269
t.Mixin.create.c.filter @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:39289
(anonymous) @ a01db7fd83ffd5f3e61e767c85767c217dfa7e0b.js?
catGroupList @ a01db7fd83ffd5f3e61e767c85767c217dfa7e0b.js?
(anonymous) @ _application-ecf386baee119f56a21f6f58e1d96e70e4a129de99451ee32e6eb4304d6749fa.js:1980
i.get @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:28802
Je @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:27908
r.compute @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:13922
t.value @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:13745
t.iterate @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:17698
t.isEmpty @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:59327
e.value @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:61661
t.initialize @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:59239
t.peek @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:59214
(anonymous) @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:60425
t.evaluate @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:59648
t.evaluateSyscall @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:63063
t.evaluateInner @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:63009
t.evaluateOuter @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:63001 @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:65133 @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:65235
render @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:19347
V @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:26194
t._renderRoots @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:19646
t._renderRootsTransaction @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:19684
t._renderRoot @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:19607
t._appendDefinition @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:19525
t.appendOutletView @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:19511
t.invoke @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:66552
t.flush @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:66442
t.flush @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:66646
n._end @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:67222
n.end @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:66908
n._run @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:67277
n._join @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:67251
n.join @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:66968
f @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:53760
(anonymous) @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:53864
l @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:3776
c @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:3844
setTimeout (async)
w.readyException @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:4068
(anonymous) @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:4088
l @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:3776
c @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:3844
setTimeout (async)
(anonymous) @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:3882
u @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:3510
fireWith @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:3640
fire @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:3648
u @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:3510
fireWith @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:3640
c @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:3864
setTimeout (async)
(anonymous) @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:3882
u @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:3510
fireWith @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:3640
fire @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:3648
u @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:3510
fireWith @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:3640
ready @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:4120
z @ _vendor-ccc170ebd7d3ab2d70c3e0c52d1121e7ced023e6ed05de129e0060c2a71ba2e3.js:4130
Show 12 more frames
_dcs-discourse-plugin-3bb7d5768f0608bcdfcf8e6c4a9a3fcfec150551e407c8d9173d7ba14849ccc8.js:3078 Uncaught (in promise) TypeError: Cannot read properties of undefined (reading ‘setLayout’)
at _dcs-discourse-plugin-3bb7d5768f0608bcdfcf8e6c4a9a3fcfec150551e407c8d9173d7ba14849ccc8.js:3078

It worked initially, for 5 mins and then stopped working. I turned off anything that was likely to be conflicting with it with no luck. I’m running the latest version of this and discourse.

Update: I’m not confident, but i think the issue is related to category slugs. The slugs which were default slugs were showing up in every grouping, so i changed them from the number format to a text string and the component began behaving correctly.

Update 2: I’m pretty confident now as i’ve recreated it in different ways. If the slug contains numbers it breaks the component.

I also see a small conflict with Agenda, and the Kanban plugin.

In both cases the categories and group title are injected above the main element users would expect to see on that page. Its closed in this image but all sub categories in the group appear here when the dropdown is open. Expected behaviour would be either not to show at all on this page, or to be below the agenda/kanban board.

Hah, this is perfect Kris, I just moved to this from the css hack for my categories :slight_smile:

My one request if this is ever added too, would be to allow an easier way in the settings to re-order the groups. Right now it looks like if I wanted to move a category group to the top, I would have to delete them all. Luckily that’s not a big deal at the end of the day

Another setting that would be cool would be to let a var in the settings configure how many grid columns ( luckily just a 2 second override in css handles this) but could see that being useful for people who don’t know that


Thanks again for this fantastic component , already loving it and have it setup perfectly

1 Like

This may need to be a general Discourse feature request and this will be the incorrect place to ask/discuss this question:

Is there a way to have theme components (such as this) have their settings unique to each theme?

For example, We will have a single discourse forum that supports multiple sites. The end goal was to have a unique theme that the user can choose, based on the main “site” they are using the forum for.

In context to this component: It would be really powerful, if we could change the settings of a theme component, for each theme that uses it, In this case, I could change the order of the Category groups based on the theme picked. So if theme Y is picked, the categories based on Y would now be ordered at the top, as that is how it is configured for this theme component for that theme.

1 Like

Could you install multiple versions of the theme component, tweaked to how you want each, and then only attach each component to its specific theme?

1 Like

This I can answer is yes. Just be sure to change name of component for easier id of which theme it is attached to.

1 Like

i found the same need, so i opened a PR on GitHub.

Edit: Actually I thought of reordering within the group, which was completely missing before too. So sorry for this wrong reply. Deleting entire categories to reorder them also just isn’t a viable solution :smile:

Please feel free to check it out in case you need that functionality.

hi @awesomerobot I just discovered this theme component and find it really interesting. One thing that is weird though: how do you order the categories in each group? I put them in a certain order, but when it’s displayed, that order is not respected.
Did I do something wrong, or are they automagically ordered?

1 Like

I believe the categories will be ordered by most recent activity. To get a consistent order you’d have to enable the site setting fixed category positions — at that point you can order them from this menu on the upper right of the /categories page:

Screenshot 2022-11-30 at 10.09.59 AM


Ohh thanks, this also solves my problem. I had fixed category positions enabled, but i did not know i could also reorder them like this :+1:
This is definitely the better approach, i will close my PR.



Does this also work with the sidebar?
Would I be able to show the categories in groups on the left sidebar?

1 Like

No, this is only for the /categories page. There’s currently no way to group categories in the sidebar.

1 Like

Hi @awesomerobot ,can you further customize this them-component to support the “Boxer with featured topics” option?