Skip to content

2024-11-12

The ridiculously confusing task of styling a decent-looking progress bar

You would think it's easy. I mean, it's just one teeny tiny, simple progress element!

html
<progress></progress>

Let's try to do something basic

Can we change the height, width and accent-color?

css
progress {
  accent-color: tomato;
  height: 2rem;
  width: 100%;
}

It works! I don't like that border-radius though. Let's set it to 0!

css
progress {
  accent-color: tomato;
  border-radius: 0;
  height: 2rem;
  width: 100%;
}

Uh-oh. It just... disappeared? Uhm.. ok let's ignore that then.

It became gray, that's weird. How about doing something about that background color instead then?

css
progress {
  accent-color: tomato;
  background-color: yellow;
  height: 2rem;
  width: 100%;
}

Gray, still?! What did I even do? 😱

Pseudo-element heaven

Turns out the <progress> element is the forgotten middle child in the browser interop program. Pseudo-elements and classes you'll need to remember to get going are:

  • progress:indeterminate
  • progress::-webkit-progress-bar
  • progress::-moz-progress-bar
  • progress::-webkit-progress-value

Armed with this knowledge - how do we change the background-color of... whatever any of that is?

css
progress::-webkit-progress-bar {
  background-color: yellow;
}
progress::-webkit-progress-value {
  background-color: yellow;
}
progress::-moz-progress-bar {
  background-color: yellow;
}

Nothing happened? Hmm, no, guess it's not that.

The finished version

At this point I'm just like... screw it! I tried! (╯°□°)╯︵ ┻━┻

If it's going to be this hard I'll just bypass all that nonsense and do something that's readable and will work regardless of pseudo nonsense!

I ended up reaching for the trusty :after pseudo-element. In this case it also enables me to do some nice animations, so win-win!

css
:where(progress) {
  --_accent-color: var(--primary);
  --_bg-color: var(
    --surface-tonal,
    light-dark(oklch(95% 0.01 255 / 1), oklch(31% 0.01 255 / 1))
  );

  appearance: none;
  background-color: var(--_bg-color);
  border-radius: var(--border-radius);
  border: 0;
  display: inline-block;
  height: var(--size-2);
  overflow: hidden;
  position: relative;
  vertical-align: baseline;
  width: 100%;

  &::-webkit-progress-bar {
    border-radius: var(--border-radius);
    background: none;
  }

  &[value]::-webkit-progress-value {
    background-color: var(--_accent-color);

    @media (prefers-reduced-motion: no-preference) {
      transition: inline-size 0.2s var(--ease-out-4);
    }
  }

  &::-moz-progress-bar {
    background-color: var(--_accent-color);
  }
}

@media (prefers-reduced-motion: no-preference) {
  progress:indeterminate {
    background-color: var(--_bg-color);

    &::after {
      animation: indeterminate 2s linear infinite;
      background-color: var(--_accent-color);
      content: "";
      inset: 0 auto 0 0;
      position: absolute;
      will-change: inset-inline-start, inset-inline-end;
    }

    &[value]::-webkit-progress-value {
      background-color: transparent;
    }

    &::-moz-progress-bar {
      background-color: transparent;
    }
  }
}

[dir="rtl"] {
  @media (prefers-reduced-motion: no-preference) {
    :where(progress):indeterminate {
      animation-direction: reverse;
    }
  }
}

[dir="rtl"] {
  @media (prefers-reduced-motion: no-preference) {
    :where(progress):indeterminate::after {
      animation-direction: reverse;
    }
  }
}

@keyframes indeterminate {
  0% {
    left: -200%;
    right: 100%;
  }
  60% {
    left: 107%;
    right: -8%;
  }
  100% {
    left: 107%;
    right: -8%;
  }
}
html
<progress></progress>

Read more