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. https://www.npmjs.com/package/svg-sprite-generator 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.

58개의 좋아요

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.

1개의 좋아요

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

4개의 좋아요

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.

1개의 좋아요

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.

2개의 좋아요

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

Done. It works now. Thanks!

1개의 좋아요

@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개의 좋아요
왼쪽 메뉴에서 Audi 카테고리 안의 prefix-span 클래스가 있는 요소의 배경을 다시 정의합니다 */
.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;

메인 페이지를 클릭하면 사각형 아이콘이 다시 나타납니다! 제가 무엇을 잘못하고 있는지 알려줄 수 있나요? 그리고 카테고리 페이지 자체에서는 정상적으로 동작합니다.

안녕하세요, 누군가 주제/컴포넌트 이름, 파일명, SCSS 변수 이름, 그리고 심볼 ID 사이의 관계를 설명해 주실 수 있을까요?

모더레이터의 shield-halved 아이콘을 자체 아이콘으로 교체하려고 하는데, 설명이 다소 불분명합니다.

2단계에서:

  • UI를 통해” 스크린샷에는 파일명이 baticonsprite.svg이고 SCSS 변수 이름이 icons-sprite로 표시되어 있습니다.
  • 하지만 “테마에 하드코딩” 부분에서는 테마/컴포넌트에 하드코딩하라고 안내합니다.
    • 그런데 어떻게 해야 하는 건가요? 편집기에서 assets.json 파일을 찾을 수 없습니다. 컴포넌트를 내보내면 about.json 파일이 보이며, 여기에는 UI를 통해 업로드한 스프라이트가 표시되어 있습니다.
    • 하지만 이 예시에는 /assets/my-icons.svg라는 다른 파일명이 나와 있습니다. 이는 baticonsprite.svg와 동일한 파일을 의미하는 건가요?
    • 이 두 가지 방법은 동일한 작업을 수행하는 대안이고, 둘 중 하나만 수행하면 되는 건가요?

3단계에서:

  • 그런데 이제 api.replaceIcon()에서 두 번째 매개변수는 이전의 어떤 ID도 사용하지 않습니다. icons-sprite, bat-icon, baticonsprite.svg, my-icons.svg도 아닙니다. 대신 완전히 새로운 my-theme-icon-bars가 나옵니다… 혼란스럽습니다.
    • my-theme 접두사는 필수적인가요? 그렇다면 그 “테마 이름” 문자열은 어디에서 오는 건가요. my-theme-bat-icon이어야 하나요? 그리고 만약 컴포넌트인 경우 테마가 아닌 경우엔 어떻게 되나요?
    • 그리고 icon-bars 부분은 다음 중 무엇이어야 하나요?
      • SVG 스프라이트 시트 XML의 심볼 ID
      • SVG 파일의 파일명
      • 부여하는 SCSS 변수 이름
      • 위의 조합 (예: icons-sprite-bat-icon?)

그리고 실제로 api.replaceIcon() 호출은 어디에 넣어야 하나요? 이미 다음 보일러플레이트가 있는 커스텀 컴포넌트의 “JS” 탭에 넣는 것이 괜찮나요?

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

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

아니면 커스텀 <script type=”discourse/plugin”> 태그를 만들어 <head> 탭에 넣어야 하나요?


혼란을 드려 죄송합니다.

위 조합을 여러 번 시도해 보았지만, 어떤 경우에도 제 스프라이트가 표시되지 않았습니다…

제 스프라이트 XML은 다음과 같습니다:

<?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>

파일명은 my-logo.svg이고, SCSS 변수 이름도 my-logo입니다.

그리고 커스텀 컴포넌트의 JS 탭에는 다음이 있습니다:

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

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

하지만 아무것도 표시되지 않습니다. 놓치고 있는 단계가 있거나, 제가 오해하고 있는 마법 같은 문자열 보간법이 있는 건가요…?

1개의 좋아요

이거 어떻게 해결할 수 있는 분 계신가요? 아직도 이 문제로 고생하고 있습니다…

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

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

UI를 통해 파일을 업로드할 경우, 두 번째 ID(my-logo)가 어디에서 오는지 잘 모르겠습니다:

$my-logo, $test, my-logo.svg, 디스크에 있는 SVG의 절대 URL 등 아무것도 동작하지 않습니다. 방패 아이콘은 대체되긴 하지만, 아무것도 표시되지 않습니다. SVG가 내용 없이 빈 <use href="#my-logo"> 태그만 남게 됩니다.

1개의 좋아요

드디어 해결했습니다 (클로드에게 감사드립니다)!

즉:

  1. SVG 파일명은 중요하지 않습니다.

  2. SVG 스프라이트 시트 내부에서는 심볼 id가 최종 아이콘 이름을 결정합니다. 예를 들어 <symbol id=”my-logo” …>

  3. 하지만 SCSS 변수 이름은 반드시 icons-sprite여야 합니다. 최종 스프라이트 시트 ID와 관련이 있는 이름으로는 절대 안 됩니다:

  4. 업로드 후 이 모습이 됩니다:


    (스프라이트 시트/변수 이름은 어디에도 보이지 않는다는 점을 주목하십시오. 이전 단계에서 입력한 내용에 $가 자동으로 붙어 $icons-sprite가 되어야 합니다.)

  5. 마지막으로, 테마의 “JS” 탭(<head>가 아님)에서 1단계의 스프라이트 시트 ID를 사용합니다:

    import { apiInitializer } from “discourse/lib/api”;
    
    export default apiInitializer((api) => {
    api.replaceIcon(“shield-halved”, “my-logo”)
    });
    

즉, SCSS 변수 이름은 반드시 icons-sprite여야 하고, 파일명은 중요하지 않으며, API 아이콘 이름은 스프라이트 시트 내부의 심볼 ID에 의해 결정됩니다.

3개의 좋아요