UIPackage
Menu

Loading Bar

loading-bar ui
Edit on GitHub

Top-of-viewport NProgress-style progress bar. Drive it imperatively via the useLoadingBar hook (start/finish/error/inc) or with value/onChange. Supports indeterminate mode, custom color and height, top/bottom anchoring, and an optional trailing spinner.

Also available for Vue ->

Installation

$ npx shadcn@latest add https://uipkge.dev/r/react/loading-bar.json
Named registry: npx shadcn@latest add @uipkge-react/loading-bar Installs to: components/ui/loading-bar/

Examples

Props

Name Type / Values Default Required
value

0–100 progress value. Use with value/onChange or drive via the hook.

number optional
color

Bar color. Accepts any CSS color value.

string optional
height

Bar height in px.

number optional
indeterminate

Indeterminate sliding animation (ignores value).

boolean optional
position

Anchor the bar to the top or bottom of the viewport.

'top' | 'bottom' optional
spinner

Show a spinner at the trailing edge of the bar.

boolean optional
error

Error state tints the bar.

boolean optional
hidden

Hide the bar entirely (e.g. when finished).

boolean optional
onValueChange

Fired with the new value on every internal update (v-model equivalent).

(value: number) => void optional
onFinish

Fired when progress reaches 100.

() => void optional

Schema

Type aliases from this item's source — use them to shape the data you pass in.

LoadingBarHandle
interface LoadingBarHandle {
  start: (from?: number) => void
  finish: () => void
  error: () => void
  inc: (amount?: number) => void
  set: (value: number) => void
}

Files installed (3)

  • components/ui/loading-bar/LoadingBar.tsx 6.2 kB
    'use client'
    
    import * as React from 'react'
    import { cn } from '@/lib/utils'
    
    export interface LoadingBarHandle {
      start: (from?: number) => void
      finish: () => void
      error: () => void
      inc: (amount?: number) => void
      set: (value: number) => void
    }
    
    export interface LoadingBarProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
      /** 0–100 progress value. Use with value/onChange or drive via the hook. */
      value?: number
      /** Bar color. Accepts any CSS color value. */
      color?: string
      /** Bar height in px. */
      height?: number
      /** Indeterminate sliding animation (ignores value). */
      indeterminate?: boolean
      /** Anchor the bar to the top or bottom of the viewport. */
      position?: 'top' | 'bottom'
      /** Show a spinner at the trailing edge of the bar. */
      spinner?: boolean
      /** Error state tints the bar. */
      error?: boolean
      /** Hide the bar entirely (e.g. when finished). */
      hidden?: boolean
      /** Fired with the new value on every internal update (v-model equivalent). */
      onValueChange?: (value: number) => void
      /** Fired when progress reaches 100. */
      onFinish?: () => void
    }
    
    const LoadingBar = React.forwardRef<LoadingBarHandle, LoadingBarProps>(
      (
        {
          value = 0,
          color = '',
          height = 3,
          indeterminate = false,
          position = 'top',
          spinner = false,
          error = false,
          hidden = false,
          className,
          onValueChange,
          onFinish,
          ...props
        },
        ref,
      ) => {
        const [internal, setInternal] = React.useState(value)
        const rafRef = React.useRef<number | null>(null)
        const onValueChangeRef = React.useRef(onValueChange)
        const onFinishRef = React.useRef(onFinish)
        onValueChangeRef.current = onValueChange
        onFinishRef.current = onFinish
    
        // Keep internal in sync when the controlled value prop changes.
        React.useEffect(() => {
          setInternal(value)
        }, [value])
    
        const set = React.useCallback((v: number) => {
          setInternal(v)
          onValueChangeRef.current?.(v)
          if (v >= 100) onFinishRef.current?.()
        }, [])
    
        const tick = React.useCallback(
          (target: number) => {
            if (rafRef.current) cancelAnimationFrame(rafRef.current)
            const step = () => {
              setInternal((prev) => {
                if (prev >= 100 || prev >= target) return prev
                const next = Math.min(target, prev + (100 - prev) * 0.02 + 0.5)
                onValueChangeRef.current?.(next)
                if (next >= 100) onFinishRef.current?.()
                if (next < target) rafRef.current = requestAnimationFrame(step)
                return next
              })
            }
            rafRef.current = requestAnimationFrame(step)
          },
          [],
        )
    
        const start = React.useCallback(
          (from = 20) => {
            set(from)
            tick(from)
          },
          [set, tick],
        )
    
        const inc = React.useCallback((amount = 10) => {
          setInternal((prev) => {
            const next = Math.min(99, prev + amount)
            onValueChangeRef.current?.(next)
            return next
          })
        }, [])
    
        const finish = React.useCallback(() => {
          if (rafRef.current) cancelAnimationFrame(rafRef.current)
          set(100)
        }, [set])
    
        const fail = React.useCallback(() => {
          if (rafRef.current) cancelAnimationFrame(rafRef.current)
          setInternal(100)
          // error prop drives the color; emit finish so callers can hide.
          onValueChangeRef.current?.(100)
          onFinishRef.current?.()
        }, [])
    
        React.useEffect(() => {
          return () => {
            if (rafRef.current) cancelAnimationFrame(rafRef.current)
          }
        }, [])
    
        React.useImperativeHandle(ref, () => ({ start, finish, error: fail, fail, inc, set }), [
          start,
          finish,
          fail,
          inc,
          set,
        ])
    
        const pct = Math.min(100, Math.max(0, internal))
        const barColor = color || (error ? 'var(--destructive)' : 'var(--primary)')
        const visible = !hidden && (indeterminate || internal > 0)
    
        return (
          <div
            data-uipkge=""
            data-slot="loading-bar"
            data-position={position}
            data-state={error ? 'error' : indeterminate ? 'indeterminate' : 'determinate'}
            className={cn(
              'pointer-events-none fixed left-0 z-[9999] w-full transition-opacity duration-300',
              position === 'top' ? 'top-0' : 'bottom-0',
              visible ? 'opacity-100' : 'opacity-0',
              className,
            )}
            style={{ height: `${height}px`, ...(props.style ?? {}) }}
            role="progressbar"
            aria-valuemin={0}
            aria-valuemax={100}
            aria-valuenow={indeterminate ? undefined : pct}
            aria-hidden={!visible}
            {...props}
          >
            {/* Track */}
            <div className="absolute inset-0 bg-transparent" />
    
            {indeterminate ? (
              // Indeterminate sliding bar
              <div
                data-slot="loading-bar-indeterminate"
                className="loading-bar-indeterminate absolute inset-y-0 w-1/3"
                style={{ backgroundColor: barColor }}
              >
                {spinner && (
                  <div
                    data-slot="loading-bar-spinner"
                    className="absolute top-1/2 right-0 size-3 translate-x-1/2 -translate-y-1/2 animate-spin rounded-full border-2 border-current border-t-transparent"
                    style={{ color: barColor }}
                  />
                )}
              </div>
            ) : (
              // Determinate bar
              <div
                data-slot="loading-bar-fill"
                className="absolute inset-y-0 left-0 transition-[width] duration-200 ease-out"
                style={{ width: `${pct}%`, backgroundColor: barColor }}
              >
                {spinner && (
                  <div
                    data-slot="loading-bar-spinner"
                    className="absolute top-1/2 right-0 size-3 translate-x-1/2 -translate-y-1/2 animate-spin rounded-full border-2 border-current border-t-transparent"
                    style={{ color: barColor }}
                  />
                )}
              </div>
            )}
    
            <style>{`
    @media (prefers-reduced-motion: no-preference) {
      .loading-bar-indeterminate {
        animation: loading-bar-slide 1.2s ease-in-out infinite;
      }
    }
    @keyframes loading-bar-slide {
      0% { left: -33%; }
      100% { left: 100%; }
    }
    `}</style>
          </div>
        )
      },
    )
    LoadingBar.displayName = 'LoadingBar'
    
    export { LoadingBar }
  • components/ui/loading-bar/useLoadingBar.ts 1.2 kB
    'use client'
    
    import * as React from 'react'
    import type { LoadingBarHandle } from './LoadingBar'
    
    export type { LoadingBarHandle }
    
    /**
     * Hook that drives a <LoadingBar> instance via a ref callback.
     *
     * Usage:
     *   const bar = useLoadingBar()
     *   <LoadingBar ref={bar.setRef} />
     *   bar.start()
     *   await fetch(...)
     *   bar.finish()
     */
    export function useLoadingBar() {
      const handleRef = React.useRef<LoadingBarHandle | null>(null)
      const [loading, setLoading] = React.useState(false)
      const [isError, setIsError] = React.useState(false)
    
      const setRef = React.useCallback((el: LoadingBarHandle | null) => {
        handleRef.current = el
      }, [])
    
      function start(from = 20) {
        setIsError(false)
        setLoading(true)
        handleRef.current?.start(from)
      }
    
      function finish() {
        setLoading(false)
        handleRef.current?.finish()
      }
    
      function error() {
        setIsError(true)
        setLoading(false)
        handleRef.current?.error()
      }
    
      function inc(amount = 10) {
        handleRef.current?.inc(amount)
      }
    
      function set(value: number) {
        handleRef.current?.set(value)
      }
    
      return {
        setRef,
        loading,
        isError,
        start,
        finish,
        error,
        inc,
        set,
      }
    }
  • components/ui/loading-bar/index.ts 0.1 kB
    export { LoadingBar, type LoadingBarHandle, type LoadingBarProps } from './LoadingBar'
    export { useLoadingBar } from './useLoadingBar'

Raw manifest: https://uipkge.dev/r/react/loading-bar.json