Customize the loading icon

AI-generated summary

The discussion is about customizing the loading icon in Discourse. Canapin shared a method to change the main loading icon by editing the theme component and adding custom HTML and CSS code. The example provided is a pulsing circles loader.

Heather_Dudley and Don expressed their appreciation for the customization option. Don asked if it’s possible to change the loader globally, and Canapin provided guidance on how to change the small spinner.

Don successfully implemented the change by targeting the sp inner class. Johnny_McIvor asked about replacing the loader with a GIF, but Heather_Dudley stated it’s not currently possible. Johnny_McIvor then asked about using a static image instead.

Customize the loading icon

Note that it changes the main loading icon, but not the small loading icon (composer, search menu, etc).

  1. Edit your current theme or create a new theme component.

  2. Go in </head> and paste this code:

    <script type="text/x-handlebars" data-template-name="components/conditional-loading-spinner">
    {{#if condition}}
    <div class="custom-loader-container">
    
      <div class="my-custom-loader">
      </div>
    
    </div>
    {{else}}
      {{yield}}
    {{/if}}
    </script>
    

    Replace or fill .my-custom-loader with your loader HTML code.

  3. Go in CSS and paste this code:

    .custom-loader-container {
        position: relative;
        margin: 20px 0 20px;
        display: flex;
        justify-content: center;
    }
    

    Add your custom loader CSS.

Examples

Pulsing circles

image

Preview: https://theme-creator.discourse.org/theme/canapin/loader-pulse

Original code from https://codepen.io/victordarras/pen/vHitB

</head>:

<script type="text/x-handlebars" data-template-name="components/conditional-loading-spinner">
{{#if condition}}
<div class="custom-loader-container">

  <div class="loader-pulser">
  </div>
  
</div>
{{else}}
  {{yield}}
{{/if}}
</script>

CSS:

.custom-loader-container {
    position: relative;
    margin: 20px 0 20px;
    display: flex;
    justify-content: center;
}
.loader-pulser {
  display: inline-block;
  vertical-align: middle;
  position: relative;
  margin-right: 3em;
  font-size: 10px;
  width: 3.2em;
  height: 3.2em;
  right: 2em;
  position: relative;
  &:before, &:after {
    content: '';
    border: 2px solid #ccc;
    border-radius: 50%;
    height: 100%;
    width: 100%;
    position: absolute;
    animation: pulse .5s ease-out;
    animation-iteration-count: infinite;
    opacity: 0;
  }
  &:before {
    border: 2px solid #aaa;
    animation-delay: .15s;
  }
}
@keyframes pulse {
  0% {
    transform: scale(0.1,.1);
    opacity: 0;
  }
  50% {
    opacity: 1;
  }
  100% {
    transform: scale(1.2,1.2);
    opacity: 0;
  }
}

Fading bars (svg animation)

image

Preview: https://theme-creator.discourse.org/theme/canapin/loader-bars

Original code from https://codepen.io/aurer/pen/jEGbA

</head>:

<script type="text/x-handlebars" data-template-name="components/conditional-loading-spinner">
{{#if condition}}
<div class="custom-loader-container">
    <div class="bars-loader">
      <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px" height="30px" viewBox="0 0 24 30" style="enable-background:new 0 0 50 50;" xml:space="preserve">
        <rect x="0" y="10" width="4" height="10" fill="#333" opacity="0.2">
          <animate attributeName="opacity" attributeType="XML" values="0.2; 1; .2" begin="0s" dur="0.4s" repeatCount="indefinite"></animate>
          <animate attributeName="height" attributeType="XML" values="10; 20; 10" begin="0s" dur="0.4s" repeatCount="indefinite"></animate>
          <animate attributeName="y" attributeType="XML" values="10; 5; 10" begin="0s" dur="0.4s" repeatCount="indefinite"></animate>
        </rect>
        <rect x="8" y="10" width="4" height="10" fill="#333" opacity="0.2">
          <animate attributeName="opacity" attributeType="XML" values="0.2; 1; .2" begin="0.15s" dur="0.4s" repeatCount="indefinite"></animate>
          <animate attributeName="height" attributeType="XML" values="10; 20; 10" begin="0.15s" dur="0.4s" repeatCount="indefinite"></animate>
          <animate attributeName="y" attributeType="XML" values="10; 5; 10" begin="0.15s" dur="0.4s" repeatCount="indefinite"></animate>
        </rect>
        <rect x="16" y="10" width="4" height="10" fill="#333" opacity="0.2">
          <animate attributeName="opacity" attributeType="XML" values="0.2; 1; .2" begin="0.3s" dur="0.4s" repeatCount="indefinite"></animate>
          <animate attributeName="height" attributeType="XML" values="10; 20; 10" begin="0.3s" dur="0.4s" repeatCount="indefinite"></animate>
          <animate attributeName="y" attributeType="XML" values="10; 5; 10" begin="0.3s" dur="0.4s" repeatCount="indefinite"></animate>
        </rect>
      </svg>
    </div>
</div>
{{else}}
  {{yield}}
{{/if}}
</script>

CSS:

.custom-loader-container {
    position: relative;
    margin: 20px 0 20px;
    display: flex;
    justify-content: center;
}
.bars-loader {
    svg path,
    svg rect{
      fill: #333;
    }
}

Unicycle

Preview: https://theme-creator.discourse.org/theme/canapin/loader-unicycle

Original code from https://codepen.io/Canapin/pen/gOrddVR

</head>:

<script type="text/x-handlebars" data-template-name="components/conditional-loading-spinner">
{{#if condition}}
<div class="custom-loader-container">

    <div class="unicycle-loader">
        <div class="saddle"></div>
        <div class="frame">
            <div class="hub"></div>
        </div>
        <div class="wheel">
            <div class="crank left">
                <div class="pedal"></div>
            </div>
            <div class="spokes">
                <div class="spoke"></div>
                <div class="spoke"></div>
                <div class="spoke"></div>
                <div class="spoke"></div>
                <div class="spoke"></div>
                <div class="spoke"></div>
                <div class="spoke"></div>
                <div class="spoke"></div>
                <div class="spoke"></div>
                <div class="spoke"></div>
                <div class="spoke"></div>
                <div class="spoke"></div>
            </div>
            <div class="crank">
                <div class="pedal"></div>
            </div>
        </div>
    </div>
    
</div>
{{else}}
  {{yield}}
{{/if}}
</script>

CSS:

.custom-loader-container {
    position: relative;
    margin: 20px 0 20px;
    display: flex;
    justify-content: center;
}
$color: black;
$speed: 1.5s;
.unicycle-loader {
    margin: 20px auto 20px auto;
    position: relative;
    transform: rotate(7.5deg) scale(0.70);
    opacity: .2;
    .saddle {
      width: 50px;
      height: 15px;
      background-color: $color;
      margin-left: 30px;
      position: relative;
      border-bottom-left-radius: 10px;
      border-bottom-right-radius: 20px;
      transform: rotate(-5deg);
      &:before {
        content: "";
        display: block;
        width: 30px;
        height: 20px;
        background-color: $color;
        border-radius: 50%;
        position: absolute;
        left: -5px;
        top: -5px;
      }
      &:after {
        content: "";
        display: block;
        width: 20px;
        height: 10px;
        background-color: $color;
        border-radius: 50%;
        position: absolute;
        right: -7px;
        top: 1px;
        transform: rotate(-35deg);
      }
    }
    
    .frame {
      width: 7px;
      height: 45px;
      background: $color;
      margin-left: 51px;
      position: relative;
      z-index: 1;
      margin-bottom: 15px;
      &:before {
        content: "";
        height: 75px;
        width: 11px;
        background: $color;
        display: block;
        position: relative;
        top: 100%;
        margin-left: -2px;
      }
    }
    
    .hub {
      position: absolute;
      bottom: calc(-100% - 30px);
      left: calc(50% - 0.5px);
      transform: translateX(-50%);
      width: 10px;
      height: 10px;
      border-radius: 50%;
      background-color: $color;
      border: 3px solid $color;
      z-index: 2;
    }
    
    .loader * {
      box-sizing: border-box;
    }
    $tire_size: 1;
    .wheel {
      width: 110px;
      height: 110px;
      border: 10px solid $color;
      border-radius: 50%;
      position: relative;
      animation: $speed rotate360 infinite linear;
      box-sizing: border-box;
      &:before {
        content: "";
        width: 100%;
        height: 100%;
        border: 12px dotted $color;
        border-radius: 50%;
        margin-left: -12px;
        margin-top: -12px;
        position: absolute;
      }
      &:after {
        content: "";
        width: 100%;
        height: 100%;
        border: 12px dotted $color;
        border-radius: 50%;
        margin-left: -12px;
        margin-top: -12px;
        position: absolute;
        transform: rotate(12deg);
      }
    }
    
    .spoke {
      top: 50%;
      transform: translateY(-50%);
      left: 50%;
      transform: translateX(-50%);
      position: absolute;
      &:before {
        left: 0;
        content: "";
        height: 1px;
        width: 45px;
        transform-origin: 20px center;
        background: rgba(0,0,0,.5);
        position: absolute;
        transform: rotate(10deg);
      }
      &:after {
        left: 0;
        content: "";
        height: 1px;
        width: 45px;
        transform-origin: 20px center;
        background: rgba(0,0,0,.5);
        position: absolute;
        transform: rotate(10deg);
        transform: rotate(-10deg);
      }
      &:nth-child(1) {
        transform: rotate(30deg);
      }
      &:nth-child(2) {
        transform: rotate(60deg);
      }
      &:nth-child(3) {
        transform: rotate(90deg);
      }
      &:nth-child(4) {
        transform: rotate(120deg);
      }
      &:nth-child(5) {
        transform: rotate(150deg);
      }
      &:nth-child(6) {
        transform: rotate(180deg);
      }
      &:nth-child(7) {
        transform: rotate(210deg);
      }
      &:nth-child(8) {
        transform: rotate(240deg);
      }
      &:nth-child(9) {
        transform: rotate(270deg);
      }
      &:nth-child(10) {
        transform: rotate(300deg);
      }
      &:nth-child(11) {
        transform: rotate(330deg);
      }
      &:nth-child(12) {
        transform: rotate(360deg);
      }
    }
    
    .crank {
      height: 4px;
      width: 30%;
      background: $color;
      transform-origin: left 1px;
      position: absolute;
      left: 50%;
      top: 50%;
      transform: translateY(-50%);
      .pedal {
        width: 20px;
        height: 6px;
        background: $color;
        position: absolute;
        right: -10px;
        margin-top: -1px;
        animation: $speed rotate360pedal infinite linear;
      }
      &.left {
        transform: rotate(180deg);
      }
    }
    
    @keyframes rotate360 {
      0% {
        transform: rotate(-50deg);
      }
    
      100% {
        transform: rotate(310deg);
      }
    }
    
    @keyframes rotate360pedal {
      0% {
        transform: rotate(410deg);
      }
    
      100% {
        transform: rotate(50deg);
      }
    }
}
26 Likes

I really, really love this. Because we have a large number of categories, our category list page takes a few seconds to load. The unicycle makes me smile!

5 Likes

Hi @Canapin,
Thank you for sharing us, I really like it. :heart: :slight_smile:
Is that possible to change the loader globally?

Thank you! :slight_smile:

1 Like

If you’re talking about the small spinner, maybe start from this:

2 Likes

Thank you @Canapin ,

I got it! I just target the spinner class and changed it to the pulser loader.

2 Likes

how do i replace it with a gif. for example:

Not currently possible.

1 Like

Can I replace it with a static image? I probably won’t change it though