Skip to content

Progress Bar

A gradient progress bar for boosts, uploads and other long-running tasks.

A gradient progress bar for boosts, uploads and other long-running tasks. Compose it from the exported registry parts so local content and behaviour stay in the application while the visual contract stays token-driven.

Root class newt-progress
import { Progress, ProgressLabel } from "@/registry/bases/newt/ui/progress"

export default function ProgressDemo() {
  return (
    <div className="w-full max-w-sm">
      <ProgressLabel>
        <span>Label</span>
        <span>50%</span>
      </ProgressLabel>
      <Progress value={50} />
    </div>
  )
}
<script setup lang="ts">
import { Progress, ProgressLabel } from "~~/registry/bases/newt/ui/progress"
</script>

<template>
  <div class="w-full max-w-sm">
    <ProgressLabel>
      <span>Label</span>
      <span>50%</span>
    </ProgressLabel>
    <Progress :value="50" />
  </div>
</template>

Installation

pnpm dlx newtui@latest add progress
npx newtui@latest add progress
yarn dlx newtui@latest add progress
bunx newtui@latest add progress

Install the dependencies

pnpm dlx --yes class-variance-authority
npx --yes class-variance-authority
yarn dlx --yes class-variance-authority
bunx --yes class-variance-authority

Copy the source into your project

import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react"

import { cn } from "@/lib/utils"

const progressVariants = cva(
  "h-full rounded-full transition-[width] duration-base ease-newt",
  {
    variants: {
      variant: {
        default: "bg-[linear-gradient(90deg,var(--newt-brand),#8c54ff)]",
        success: "bg-[linear-gradient(90deg,var(--newt-online),#45c178)]",
        danger: "bg-[linear-gradient(90deg,var(--newt-dnd),#f4878a)]",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

export interface ProgressProps
  extends
    React.HTMLAttributes<HTMLDivElement>,
    VariantProps<typeof progressVariants> {
  /** Current value, 0–max */
  value?: number
  max?: number
  indicatorClassName?: string
}

const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
  (
    { className, indicatorClassName, value = 0, max = 100, variant, ...props },
    ref
  ) => {
    const clamped = Math.min(Math.max(value, 0), max)
    const percent = max > 0 ? (clamped / max) * 100 : 0
    return (
      <div
        ref={ref}
        role="progressbar"
        aria-valuemin={0}
        aria-valuemax={max}
        aria-valuenow={clamped}
        className={cn(
          "h-2 w-full overflow-hidden rounded-full bg-newt-bg-base",
          className
        )}
        {...props}
      >
        <div
          className={cn(progressVariants({ variant }), indicatorClassName)}
          style={{ width: `${percent}%` }}
        />
      </div>
    )
  }
)
Progress.displayName = "Progress"

const ProgressLabel = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn(
      "mb-1.5 flex justify-between text-xs text-newt-text-muted",
      className
    )}
    {...props}
  />
))
ProgressLabel.displayName = "ProgressLabel"

export { Progress, ProgressLabel, progressVariants }
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { computed } from "vue"

import { cn } from "@/lib/utils"

import { type ProgressVariants, progressVariants } from "."

const props = withDefaults(
  defineProps<{
    value?: number
    max?: number
    variant?: ProgressVariants["variant"]
    indicatorClass?: HTMLAttributes["class"]
    class?: HTMLAttributes["class"]
  }>(),
  { value: 0, max: 100 }
)

const clamped = computed(() => Math.min(Math.max(props.value, 0), props.max))
const percent = computed(() =>
  props.max > 0 ? (clamped.value / props.max) * 100 : 0
)
</script>

<template>
  <div
    role="progressbar"
    :aria-valuemin="0"
    :aria-valuemax="props.max"
    :aria-valuenow="clamped"
    :class="
      cn('h-2 w-full overflow-hidden rounded-full bg-newt-bg-base', props.class)
    "
  >
    <div
      :class="
        cn(progressVariants({ variant: props.variant }), props.indicatorClass)
      "
      :style="{ width: `${percent}%` }"
    />
  </div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

import { cn } from "@/lib/utils"

const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>

<template>
  <div
    :class="
      cn(
        'mb-1.5 flex justify-between text-xs text-newt-text-muted',
        props.class
      )
    "
  >
    <slot />
  </div>
</template>
import { cva, type VariantProps } from "class-variance-authority"

export { default as Progress } from "./Progress.vue"
export { default as ProgressLabel } from "./ProgressLabel.vue"

export const progressVariants = cva(
  "h-full rounded-full transition-[width] duration-base ease-newt",
  {
    variants: {
      variant: {
        default: "bg-[linear-gradient(90deg,var(--newt-brand),#8c54ff)]",
        success: "bg-[linear-gradient(90deg,var(--newt-online),#45c178)]",
        danger: "bg-[linear-gradient(90deg,var(--newt-dnd),#f4878a)]",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

export type ProgressVariants = VariantProps<typeof progressVariants>

Update the import paths

The files import @/lib/utils and sibling components through the aliases in your components.json. Point them at wherever those live in your project.

Usage

import { Progress } from "@/components/ui/progress"

export function Example() {
  return <Progress />
}
<script setup lang="ts">
import { Progress, ProgressLabel } from "@/components/ui/progress"
</script>

<template>
  <Progress />
</template>

API reference

Progress

PropType
value?number | undefined

Current value, 0–max

Typenumber | undefined
max?number | undefined

Optional max value.

Typenumber | undefined
indicatorClassName?string | undefined

Optional indicatorClassName value.

Typestring | undefined

The registry exports Progress, ProgressLabel. Parts without a table accept the native attributes for the element they render; React uses className, while Vue uses class and fallthrough attributes.

Accessibility

Preserve the native semantics of the rendered elements and pass through ARIA attributes when the surrounding context needs a label or description. Any interactive child needs a visible focus indicator, and meaning must not rely on colour alone.

Tokens

Progress Bar reads its colours, radii and motion from the --newt-* tokens installed with the library. Override a token and every component that reads it follows, including this one — see the token layer. In the plain HTML and CSS distribution the same component is newt-progress.