Preloader

Loading Loading Loading the report
@use 'sprucecss/scss/spruce' as *;

.preloader-demo {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: spacer('l');
}

.preloader {
  --color: currentcolor;
  --size: 1.5rem;
  --animation-duration: 1s;
  block-size: var(--size);
  display: inline-flex;
  flex-shrink: 0;
  inline-size: var(--size);

  &--sm {
    --size: 1rem;
  }

  &--lg {
    --size: 2.5rem;
  }
}

.preloader--circle {
  --border-width: calc(var(--size) / 6);
  --size: 1.5rem;
  --animation-duration: 1s;
  block-size: var(--size);
  inline-size: var(--size);

  &::after {
    animation: preloader-rotation var(--animation-duration) linear infinite;
    block-size: var(--size);
    border: var(--border-width) solid var(--color, currentcolor);
    border-block-end-color: transparent;
    border-inline-start-color: transparent;
    border-radius: 50%;
    box-sizing: border-box;
    content: '';
    display: flex;
    inline-size: var(--size);
  }
}

.preloader--bars {
  --size: 1.5rem;
  --animation-duration: 1s;
  align-items: center;
  block-size: var(--size);
  display: inline-flex;
  inline-size: var(--size);
  justify-content: space-between;

  span:not(.sr-only) {
    animation: preloader-resize var(--animation-duration) infinite ease-in-out;
    background: var(--color, currentcolor);
    block-size: 70%;
    border-radius: 1px;
    inline-size: calc(var(--size) / 6);

    &:nth-of-type(2) { animation-delay: -0.3s; }
    &:nth-of-type(3) { animation-delay: -0.15s; }
  }
}

// Keep a slow, gentle motion with reduced motion: a loader that stops looks like a frozen page.
@include motion-reduce {
  .preloader--circle::after {
    animation-duration: 3s;
  }

  .preloader--bars span:not(.sr-only) {
    animation: preloader-fade 2s infinite ease-in-out;
  }
}

@keyframes preloader-rotation {
  to { rotate: 360deg; }
}

@keyframes preloader-resize {
  0%,
  80%,
  100% {
    scale: 1 0.6;
  }

  40% {
    scale: 1 1.2;
  }
}

@keyframes preloader-fade {
  50% { opacity: 0.4; }
}
<div class="preloader-demo">
    <span class="preloader preloader--circle" role="status">
        <span class="sr-only">Loading</span>
    </span>
    <span class="preloader preloader--bars" role="status">
        <span class="sr-only">Loading</span>
        <span aria-hidden="true"></span>
        <span aria-hidden="true"></span>
        <span aria-hidden="true"></span>
    </span>
    <span class="preloader preloader--circle preloader--lg" style="--color: var(--spruce-base-color-primary, hsl(262deg 71% 49%));" role="status">
        <span class="sr-only">Loading the report</span>
    </span>
    <button class="btn btn--primary" type="button" aria-disabled="true">
        <span class="preloader preloader--circle preloader--sm" aria-hidden="true"></span>
        Saving…
    </button>
</div>

Preloader

Small loading indicators for pending content and buttons: a spinning circle and three bouncing bars, in three sizes and any color.

Variations

  • .preloader--circle: a rotating ring.
  • .preloader--bars: three bars with a sequenced animation.
  • .preloader--sm and .preloader--lg: 1rem and 2.5rem instead of 1.5rem.

Accessibility

  • A standalone preloader has role="status" and a visually hidden text ("Loading the report"), so screen readers announce what is loading. Be specific when you can.
  • Inside a button, the preloader is decorative (aria-hidden="true"), and the button text says what's happening ("Saving…"). Use aria-disabled="true" instead of disabled, so the button stays focusable and its new label is read.
  • With reduced motion, the animation slows down (circle) or turns into a gentle fade (bars) instead of stopping: a frozen loader would look like a broken page.

Customization

Every instance can be customized with custom properties:

  • --size: the width and height (1.5rem).
  • --color: the color (the current text color by default, so it matches its context).
  • --animation-duration: the length of one loop (1s).
  • --border-width: the ring width of the circle (a sixth of the size).

Technical Details

  • The old class names (.preloader--circle, .preloader--bars without .preloader) still work.
  • The ring is drawn with logical border colors, and it rotates with the rotate property.
  • For a whole-page loading state, consider a Skeleton instead: it shows the shape of the content that's coming.

Documentation

Learn about Spruce CSS through our extensive documentation.

Components

Explore our UI library of accessible components built with Spruce CSS.

Blocks and Pages

Full-width page blocks and complete page templates, ready to copy.

Find us on GitHub

Did you find a bug? Have an idea or a question? Please open an issue to help us develop the project.