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.

57 лайков

Как можно выбрать конкретную иконку в конкретном элементе? В моём случае я хочу заменить иконку Docs в боковом меню на другую иконку FA.

1 лайк

Я бы скрыл это с помощью CSS и добавил новую кнопку.

Общие / CSS

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

Добавьте новую кнопку в разделе «Ещё» > «Настроить этот раздел»

4 лайка

Это не работает. Я попробовал сделать следующее:

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

отсюда, но, похоже, это тоже не сработало. Мне кажется, что метод с тегом script сломан, так как это не работает с превью-ссылкой. Честно говоря, не уверен.

1 лайк

У меня работает :woman_shrugging:t2:

Вы добавляете это во вкладку «Head»? Я также заменил «robot» в своём заголовке:

Возможно, вам потребуется добавить иконку в настройку администратора «Подмножество SVG-иконок».

2 лайка

Да, во вкладку head. И во вкладку header, так как в руководстве так указано.

Готово. Теперь это работает. Спасибо!

1 лайк

@NateDhaliwal Не могли бы вы написать мне в личные сообщения? Мне нужна помощь с чем-то, и я не вижу опцию чата в вашем профиле. Спасибо!

1 лайк
Для левого меню мы переопределяем фон элемента с классом prefix-span внутри категории 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;
}

При переходе на главную страницу иконка-квадрат снова появляется! Подскажите, пожалуйста, что я делаю не так? А на странице самой категории всё работает.

Привет! Не могли бы вы прояснить связь между именем темы/компонента, именем файла, именем переменной SCSS и идентификатором символа…?

Я пытаюсь заменить иконку модератора shield-halved на свою собственную, но инструкции немного неясны.

На шаге 2:

  • На скриншоте раздела «Через интерфейс» показано имя файла baticonsprite.svg и имя переменной SCSS icons-sprite.
  • Однако в разделе «Записать в тему/компонент вручную» говорится, что нужно записать это вручную в тему или компонент.
    • Но как? В редакторе я не вижу файла assets.json. Если я экспортирую компонент, то вижу файл about.json, в котором действительно отображается спрайт, загруженный через интерфейс.
    • Однако в этом примере также указано другое имя файла — /assets/my-icons.svg. Это тот же файл, что и baticonsprite.svg?
    • Являются ли эти два способа альтернативными вариантами выполнения одной и той же задачи, то есть нужно сделать что-то одно, а не оба сразу…?

На шаге 3:

  • Теперь в api.replaceIcon() второй параметр не использует ни один из предыдущих идентификаторов: ни icons-sprite, ни bat-icon, ни baticonsprite.svg, ни my-icons.svg. Вместо этого мы получаем совершенно новый идентификатор my-theme-icon-bars… я запутался.
    • Обязателен ли префикс my-theme? Если да, то откуда берется строка с «именем темы»? Например, должно ли быть my-theme-bat-icon? А что, если это компонент, а не тема?
    • И что должно стоять в части icon-bars:
      • Идентификатор символа из XML-файла спрайта SVG?
      • Имя файла SVG?
      • Имя переменной SCSS, которое вы ему присвоили?
      • Какая-то комбинация вышеперечисленного (например, icons-sprite-bat-icon)?

И где именно нужно размещать вызов api.replaceIcon()? Можно ли поместить его во вкладку «JS» пользовательского компонента, где уже есть шаблонный код:

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

export default apiInitializer((api) => {
   // ваш код здесь
});

Или необходимо создать пользовательский тег <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")
});

Непонятно, откуда берётся второй ID (“my-logo”), если загружать файл через интерфейс:

Ни $my-logo, ни $test, ни my-logo.svg, ни абсолютный URL к SVG на диске и т.д. не работают. Щит заменяется, но на пустоту. SVG просто превращается в пустой <use href="#my-logo"> без содержимого.

1 лайк

Наконец-то разобрался (спасибо, Claude)!

Итак:

  1. Имя файла SVG не имеет значения.

  2. Внутри спрайта SVG именно id символа определяет окончательное имя иконки, например <symbol id="my-logo" …>.

  3. Но имя переменной SCSS ОБЯЗАТЕЛЬНО должно быть именно icons-sprite, а не иметь никакого отношения к финальным ID спрайта:

  4. После загрузки это будет выглядеть так:


    (Обратите внимание, что имя спрайта / переменной нигде не встречается. Оно должно быть в точности $icons-sprite, где $ автоматически добавляется в начало и не является частью того, что вы вводили на предыдущем шаге).

  5. Затем, наконец, во вкладке «JS» темы (а не в <head>), именно там вы используете ID спрайта из шага 1:

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

Итак, имя переменной SCSS должно быть в точности icons-sprite, имя файла не имеет значения, а имя иконки в API определяется ID символа внутри спрайта.

3 лайка