Replace Discourse's default SVG icons with custom icons in a theme

You can replace a Discourse’s default SVG icons individually or as a whole with your own custom SVG and override them within a theme or theme component.

Step 1 - Create an SVG Spritesheet

To get started, you must create an SVG Spritesheet. This can contain anything from a single additional custom SVG icon up to an entire replacement set of hundreds.

The spritesheet should be saved as an SVG file. In principle, you are nesting the <svg> tag contents from the original SVG icon file into <symbol> tags and giving them a nice identifier.

<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
  <symbol id="my-theme-icon-1">
    <!--
      Code inside the <svg> tag from the source SVG icon file
      this is typically everything between the <svg> tags
      (but not the SVG tag itself, that's replaced by <symbol> above)
      You can transfer any attributes (i.e. ViewBox="0 0 0 0") to the <symbol> tag
      -->
  </symbol>

  <symbol id="my-theme-icon-2">
    <!-- SVG code here. Add more <symbol> blocks as needed.
      -->
  </symbol>
</svg>
  • Be sure to add a custom ID to each symbol in the spritesheet. It’s probably helpful for your sanity to prefix your IDs with your theme name my-theme-icon.

  • To have the icon color to be dynamic like the existing icons, set the fill to currentColor rather than a hardcoded color (like #333)

  • To scale or correctly centre your icon, utilise a viewBox attribute on the <symbol> tag. See How to Scale SVG | CSS-Tricks for more information.

  • Be on the lookout for style collisions within your SVGs. For example, SVGs will often have an inline style like .st0{fill:#FF0000;} defined. If you have multiple SVGs using the same classes this can cause issues (to fix these issues, edit the classes to be unique to each icon).

  • If you have many icons, there are ways to automate this. svg-sprite-generator - npm is a simple command line tool for combining SVGs into a spritesheet.

Example - single custom icon spritesheet

<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
  <symbol id="bat-icon" viewBox="6 6 36 36">
    <path
      fill="currentColor"
      d="M24,18.2c0.7,0,0.9,0.2,0.9,0.2l0.4-1.7c0,0,0.4,1.5,0.4,2.8c0.2,1.1,2.2,0.4,3.9,0C31.4,19.1,32,16,32,16h16c0,0-9.4,3.5-7,10c0,0-14.8-2-17,7l0,0c-2.2-9-17-7-17-7c2.4-6.5-7-10-7-10h16c0,0,0.6,3.1,2.3,3.5c1.7,0.4,3.9,1.1,3.9,0c0.2-1.1,0.4-2.8,0.4-2.8l0.4,1.7C23.1,18.4,23.4,18.2,24,18.2L24,18.2L24,18.2z"
    />
  </symbol>
</svg>

Step 2 - Add the spritesheet to your theme

Once your spritesheet is built, you need to add the SVG file to your component/theme. This is easy via the UI, or you can hard code it into a component/theme.

:information_source: Once it is uploaded to any installed component/theme, it is available throughout your instance using the ID in the <symbol> tag.

Via the UI

Go to the Uploads section of the theme/component settings and add your sprite file with a SCSS var name of icons-sprite:

Hardcode into a Theme / Component

Add the spritesheet file to the Theme’s /assets folder. Then update your assets.json file in the root folder.
For an SVG sprite called my-icons.svg, your about.json should include this:

"assets": {
  "icons-sprite": "/assets/my-icons.svg"
}

Step 3 (optional) - Overriding default icons

Now that your spritesheet is set, you can tell Discourse to replace icons. This is how you do it from an api-initializer:

// {theme}/javascripts/discourse/api-initializers/init-theme.gjs

import { apiInitializer } from "discourse/lib/api";

export default apiInitializer((api) => {
  api.replaceIcon("bars", "my-theme-icon-bars");
  api.replaceIcon("link", "my-theme-icon-link");
  // etc.
});

The first ID, bars, is the default icon ID in Discourse and the second is the ID of your replacement icon. The easiest way to find an ID of one of our icons is to inspect the icon in your browser.

Here the icon name follows the d-icon- prefix. So in this example it’s d-unliked

Most of our icons follow the icon names from https://fontawesome.com/, but there are exceptions (which is why checking the ID in your inspector is the most reliable method). You can see all the exceptions in the const REPLACEMENTS block here on github.

That’s it. You can now style Discourse with your own custom icons!


This document is version controlled - suggest changes on github.

57 Likes

How would one target a specific icon in a specific element? In my case, I would like to replace the Docs Icon in the sidebar menu with another FA icon.

I’d hide it with CSS and add a new button for it.

Common / CSS

.sidebar-section-wrapper {
  li[data-list-item-name=docs] {
    display: none !important;
  }
}

Add a new button in More > Customize this section
Screenshot 2024-08-23 at 9.45.46

3 Likes

This does not work. I tried doing a:

<script type="text/discourse-plugin" version="0.8">
    api.replaceIcon("shield-halved", "hat-wizard");
</script>

from here, but it doesn’t seem to work. I think the script tag method is broken, since this doesn’t work with the preview link. Honestly not sure.

works for me :woman_shrugging:t2:

are you putting it in the head tab? i also replace robot in my header:

you may have to add the icon to the admin SVG icon subset setting.

1 Like

Yep, the head tab. And the header tab, since the guide says that.

Done. It works now. Thanks!

1 Like

@NateDhaliwal Could you PM me please? I need help with something and I don’t see a chat option in your profile. Thank you!

1 Like
For the left menu, we redefine the background of the element with the prefix-span class inside the category Audi */
.navigation-category [data-category-id="6"] .prefix-span {
  background: url("https://raw.githubusercontent.com/tima4502/car-icons/bb0d0fae3e5b66c512a27a130b219ec0ee342ada/audi.svg") center/contain no-repeat !important;

when i click on the main page the square icon appears again! can you tell me what i am doing wrong? and on the category page itself it works

Hi there, could anyone please clarity the relationship between a theme/component name, filename, SCSS var name, and the symbol ID…?

I’m trying to replace the moderator shield-halved icon with one of our own, but the instructions are a bit unclear.

In Step 2:

  • The “Via the UI” screenshot shows a filename of baticonsprite.svg with an SCSS var name of icons-sprite
  • But then in “Hardcode into a Theme”, it tells you to hardcode it into a theme/component
    • But how? I don’t see an assets.json file in the editor. If I export the component, I see an about.json, which does show the sprite I uploaded via the UI
    • But this example also shows a different filename of /assets/my-icons.svg — is that meant to the same file as baticonsprite.svg?
    • Are these two alternative ways of doing the same thing, and you just need to do one OR the other, not both…?

In Step 3:

  • But then now, in api.replaceIcon(), the second parameter doesn’t use any of the previous IDs, not icons-sprite or bat-icon or baticonsprite.svg or my-icons.svg. Instead, we get an entirely new my-theme-icon-bars… confused.
    • Is the my-theme prefix required, and if so, where does that “theme name” string come from? Like is it supposed to be my-theme-bat-icon? And what if it’s a component, not a theme?
    • And for the icon-bars part, is it supposed to be:
      • The symbol ID from the SVG spritesheet XML
      • The filename of the SVG file
      • The SCSS var name that you give it
      • Some combination of the above (like icons-sprite-bat-icon?)

And where do you actually put the api.replaceIcon() call? Is it ok to put it in the “JS” tab of a custom component, which already has the boilerplate:

import { apiInitializer } from "discourse/lib/api";

export default apiInitializer((api) => {
   // your code here
});

Or is it necessary to create a custom <script type=”discourse/plugin”> tag and put that in the <head> tab instead?


Sorry for my confusion here.

I’ve tried several combinations of the above and couldn’t get my sprite to show up no matter what…

My sprite XML looks like:

<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">


<symbol id="my-logo" viewBox="0 0 94.652 95.261"><defs><linearGradient id="a" y1="47.631" x2="94.652" y2="47.631" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ff593d"/><stop offset="1" stop-color="#ff7751"/></linearGradient></defs><title>d_only</title><path d="M47.326,0H0V95.261H47.326c23.67,0,47.326-21.326,47.326-47.624S71,0,47.326,0Zm0,69.274a21.644,21.644,0,1,1,21.65-21.637A21.635,21.635,0,0,1,47.326,69.274Z" fill="url(#a)"/></symbol>

</svg>

The filename is my-logo.svg and the SCSS var name is also my-logo

And in the JS tab of the custom component, I have:

import { apiInitializer } from "discourse/lib/api";

export default apiInitializer((api) => {
    api.replaceIcon("shield-halved", "my-logo")
});

But nothing seems to show up. Is there a step I’m missing, or some sort of magic string interpolation I’m misunderstanding…?