I’m using Emojis in the Categories section in the sidebar, but I also have a custom section that is public. I wanted to have the same colorful icons in that section so it doesn’t get so “bland” compared to the Categories section.
Is this possible?
I’m using Emojis in the Categories section in the sidebar, but I also have a custom section that is public. I wanted to have the same colorful icons in that section so it doesn’t get so “bland” compared to the Categories section.
Is this possible?
With the help of both ChatGPT and Claude, I was able to make it work and very customizable:
If you want to do it, create a new component and add this to the CSS tab:
.sidebar-section-link-prefix .emoji.prefix-emoji {
width: 1rem !important;
height: 1rem !important;
}
For my particular case
1rem
works great. Adjust to your forum/community
Then in the JS tab:
import { apiInitializer } from "discourse/lib/api";
import { emojiUrlFor } from "discourse/lib/text";
export default apiInitializer("0.11.1", (api) => {
// Map section names to IDs
const sectionIds = {
"community": 1,
"tiago": 2,
"personal-section": 3,
// Add more sections here
};
// Map of [sectionId, itemName] to emoji names
const iconReplacements = {
// Community section (ID: 1)
"1,admin": "wrench",
"1,review": "triangular_flag_on_post",
"1,everything": "books",
"1,my-posts": "writing_hand",
"1,my-messages": "envelope_with_arrow",
// Tiago section (ID: 2)
"2,Journal": "notebook_with_decorative_cover",
"2,Music": "musical_note",
"2,About": "bust_in_silhouette",
// Personal section (ID: 3)
"3,Backups": "floppy_disk",
"3,Scheduled": "clock3",
"3,Staff": "lock",
"3,Components": "electric_plug",
};
function replaceIcons() {
Object.entries(sectionIds).forEach(([sectionName, sectionId]) => {
Object.entries(iconReplacements).forEach(([key, emojiName]) => {
const [keyId, itemName] = key.split(',');
// Only process if this replacement is for the current section
if (parseInt(keyId) === sectionId) {
const url = emojiUrlFor(emojiName);
// Try multiple selectors to catch both hamburger menu and fixed sidebar
const selectors = [
// Fixed sidebar selector
`div[data-section-name="${sectionName}"] li[data-list-item-name="${itemName}"] .sidebar-section-link-prefix.icon`,
// Hamburger menu selector (more specific)
`.menu-panel div[data-section-name="${sectionName}"] li[data-list-item-name="${itemName}"] .sidebar-section-link-prefix.icon`,
// Generic fallback
`[data-section-name="${sectionName}"] [data-list-item-name="${itemName}"] .sidebar-section-link-prefix.icon`
];
for (const selector of selectors) {
const prefix = document.querySelector(selector);
if (prefix && url) {
// Check if it's already replaced to avoid unnecessary DOM manipulation
if (!prefix.querySelector('.prefix-emoji')) {
prefix.innerHTML = `
<img src="${url}"
title="${emojiName}"
alt="${emojiName}"
class="emoji prefix-emoji">
`;
}
break; // Exit loop once we find and replace the icon
}
}
}
});
});
}
// Run on page change
api.onPageChange(replaceIcons);
// Also run with delay to catch dynamically loaded content
api.onPageChange(() => {
setTimeout(replaceIcons, 100);
setTimeout(replaceIcons, 500);
});
// Enhanced MutationObserver to catch sidebar changes
const observer = new MutationObserver((mutations) => {
let shouldReplace = false;
mutations.forEach((mutation) => {
// Watch for added nodes (original functionality)
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.classList?.contains('menu-panel') ||
node.querySelector?.('.sidebar-sections') ||
node.classList?.contains('sidebar-sections') ||
node.querySelector?.('[data-section-name]')) {
shouldReplace = true;
}
}
});
// Watch for attribute changes on sidebar sections (collapse/expand)
if (mutation.type === 'attributes' && mutation.target.nodeType === Node.ELEMENT_NODE) {
const target = mutation.target;
if (target.matches('[data-section-name]') ||
target.closest('[data-section-name]') ||
target.matches('.sidebar-section') ||
target.closest('.sidebar-section')) {
shouldReplace = true;
}
}
// Watch for childList changes in sidebar sections
if (mutation.type === 'childList' && mutation.target.nodeType === Node.ELEMENT_NODE) {
const target = mutation.target;
if (target.matches('[data-section-name]') ||
target.closest('[data-section-name]') ||
target.querySelector('[data-section-name]')) {
shouldReplace = true;
}
}
});
if (shouldReplace) {
setTimeout(replaceIcons, 50);
}
});
// Start observing with enhanced options
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true, // Watch for attribute changes
attributeFilter: ['class', 'style', 'aria-expanded'], // Common attributes that change on collapse/expand
});
// Additional event listeners for common sidebar interactions
document.addEventListener('click', (event) => {
// Check if click was on a sidebar section header or toggle button
if (event.target.closest('.sidebar-section-header') ||
event.target.closest('[data-section-name]') ||
event.target.matches('.sidebar-section-toggle')) {
setTimeout(replaceIcons, 100);
}
});
});
I made it so I can easily add new custom sections by name, attributing it an id
and then creating each section’s mapping.
The reason for that is because I may have 2 sections with the title Journal
, for example, but maybe I want 2 different emojis for each.
I’m pretty sure there will be someone saying that there’s a component or plugin for this but it was still fun to go back and forth with ChatGPT and Claude until everything worked the way I wanted.
Hope this helps other users.
If you see anything that can be tweaked/improved, please share
I just updated the JS code, because I noticed that on mobile it was still showing the default icons. Everything is working as expected on both mobile and desktop.
Another edit: when I would collapse and expand the section, the emojis would be gone and replaced with the original icons. It’s working now and I updated the JS code again.
Glad you found a way to do this – I think this would be a pretty natural fit for a core feature, now that we support emoji for categories.
I agree. Hope this becomes a native feature eventually. Until that happens, this custom component is doing its job