Display
ProgressBar
Linear progress indicator
Preview
size
value
animated
Code
import css from "./ProgressBar.module.css";
type Size = "xs" | "sm" | "md" | "lg";
interface ResponsiveSize {
base: Size;
mobile?: Size;
tablet?: Size;
desktop?: Size;
wide?: Size;
}
interface ProgressBarProps {
size: Size | ResponsiveSize;
value: number;
// Named after the attribute it becomes: the bar draws no text of its own.
"aria-label": string;
animated?: boolean;
}
function getSizeClasses(size: Size | ResponsiveSize): string {
if (typeof size === "string") {
return css[size];
}
const classes = [css[`${size.base}_base`]];
if (size.mobile) classes.push(css[`${size.mobile}_mobile`]);
if (size.tablet) classes.push(css[`${size.tablet}_tablet`]);
if (size.desktop) classes.push(css[`${size.desktop}_desktop`]);
if (size.wide) classes.push(css[`${size.wide}_wide`]);
return classes.join(" ");
}
export default function ProgressBar({
size,
value,
"aria-label": ariaLabel,
animated,
}: ProgressBarProps) {
return (
<div
className={`
${css.progressbar}
${getSizeClasses(size)}
`}
role="progressbar"
aria-label={ariaLabel}
aria-valuenow={value}
aria-valuemin={0}
aria-valuemax={100}
>
<div
className={`${css.progressbar_fill} ${animated ? css.animated : ""}`}
style={
animated
? ({ "--progress-value": `${value}%` } as React.CSSProperties)
: { width: `${value}%` }
}
></div>
</div>
);
}
.progressbar {
background: light-dark(
color-mix(in srgb, var(--color-text-secondary) 10%, transparent),
color-mix(in srgb, #fff 15%, transparent)
);
box-shadow: inset 0 2px 4px
light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.9));
border-radius: var(--radius-pill);
&.xs {
height: 4px;
}
&.sm {
height: 6px;
}
&.md {
height: 8px;
}
&.lg {
height: 10px;
}
&.xs_base {
height: 4px;
}
&.sm_base {
height: 6px;
}
&.md_base {
height: 8px;
}
&.lg_base {
height: 10px;
}
@media (min-width: 480px) {
&.xs_mobile {
height: 4px;
}
&.sm_mobile {
height: 6px;
}
&.md_mobile {
height: 8px;
}
&.lg_mobile {
height: 10px;
}
}
@media (min-width: 768px) {
&.xs_tablet {
height: 4px;
}
&.sm_tablet {
height: 6px;
}
&.md_tablet {
height: 8px;
}
&.lg_tablet {
height: 10px;
}
}
@media (min-width: 1024px) {
&.xs_desktop {
height: 4px;
}
&.sm_desktop {
height: 6px;
}
&.md_desktop {
height: 8px;
}
&.lg_desktop {
height: 10px;
}
}
@media (min-width: 1440px) {
&.xs_wide {
height: 4px;
}
&.sm_wide {
height: 6px;
}
&.md_wide {
height: 8px;
}
&.lg_wide {
height: 10px;
}
}
.progressbar_fill {
height: 100%;
border-radius: var(--radius-pill);
background: linear-gradient(
to right,
light-dark(var(--color-accent-secondary), var(--color-accent-secondary)),
light-dark(var(--color-accent), var(--color-accent))
);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2);
&.animated {
width: var(--progress-value);
transition: width 0.4s ease-out;
animation: fill-bar 1s ease-out;
@media (prefers-reduced-motion: reduce) {
transition: none;
animation: none;
}
}
}
}
@keyframes fill-bar {
from {
width: 0%;
}
to {
width: var(--progress-value);
}
}
<script setup lang="ts">
import { computed } from "vue";
type Size = "xs" | "sm" | "md" | "lg";
interface ResponsiveSize {
base: Size;
mobile?: Size;
tablet?: Size;
desktop?: Size;
wide?: Size;
}
interface ProgressBarProps {
size: Size | ResponsiveSize;
value: number;
// Named after the attribute it becomes: the bar draws no text of its own.
ariaLabel: string;
animated?: boolean;
}
const { size, value, ariaLabel, animated } =
defineProps<ProgressBarProps>();
const sizeClasses = computed(() => {
if (typeof size === "string") {
return size;
}
const classes = [`${size.base}_base`];
if (size.mobile) classes.push(`${size.mobile}_mobile`);
if (size.tablet) classes.push(`${size.tablet}_tablet`);
if (size.desktop) classes.push(`${size.desktop}_desktop`);
if (size.wide) classes.push(`${size.wide}_wide`);
return classes.join(" ");
});
const fillStyle = computed(() =>
animated ? { "--progress-value": `${value}%` } : { width: `${value}%` },
);
</script>
<template>
<div
class="progressbar"
:class="sizeClasses"
role="progressbar"
:aria-label="ariaLabel"
:aria-valuenow="value"
:aria-valuemin="0"
:aria-valuemax="100"
>
<div class="progressbar_fill" :class="{ animated }" :style="fillStyle"></div>
</div>
</template>
<style scoped>
.progressbar {
background: light-dark(
color-mix(in srgb, var(--color-text-secondary) 10%, transparent),
color-mix(in srgb, #fff 15%, transparent)
);
box-shadow: inset 0 2px 4px
light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.9));
border-radius: var(--radius-pill);
&.xs {
height: 4px;
}
&.sm {
height: 6px;
}
&.md {
height: 8px;
}
&.lg {
height: 10px;
}
&.xs_base {
height: 4px;
}
&.sm_base {
height: 6px;
}
&.md_base {
height: 8px;
}
&.lg_base {
height: 10px;
}
@media (min-width: 480px) {
&.xs_mobile {
height: 4px;
}
&.sm_mobile {
height: 6px;
}
&.md_mobile {
height: 8px;
}
&.lg_mobile {
height: 10px;
}
}
@media (min-width: 768px) {
&.xs_tablet {
height: 4px;
}
&.sm_tablet {
height: 6px;
}
&.md_tablet {
height: 8px;
}
&.lg_tablet {
height: 10px;
}
}
@media (min-width: 1024px) {
&.xs_desktop {
height: 4px;
}
&.sm_desktop {
height: 6px;
}
&.md_desktop {
height: 8px;
}
&.lg_desktop {
height: 10px;
}
}
@media (min-width: 1440px) {
&.xs_wide {
height: 4px;
}
&.sm_wide {
height: 6px;
}
&.md_wide {
height: 8px;
}
&.lg_wide {
height: 10px;
}
}
.progressbar_fill {
height: 100%;
border-radius: var(--radius-pill);
background: linear-gradient(
to right,
light-dark(var(--color-accent-secondary), var(--color-accent-secondary)),
light-dark(var(--color-accent), var(--color-accent))
);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2);
&.animated {
width: var(--progress-value);
transition: width 0.4s ease-out;
animation: fill-bar 1s ease-out;
@media (prefers-reduced-motion: reduce) {
transition: none;
animation: none;
}
}
}
}
@keyframes fill-bar {
from {
width: 0%;
}
to {
width: var(--progress-value);
}
}
</style>
Props
| Prop | Type | Required | Description |
|---|---|---|---|
| size | "xs" | "sm" | "md" | "lg" | ResponsiveSize | required | Bar thickness. Fixed or responsive. |
| value | number | required | Progress as a percentage (0–100). |
| aria-label | string | required | Name of the bar for screen readers; the bar draws no text. |
| animated | boolean | — | Animates the fill and every value change. |