Loading Bar
loading-bar ui 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
$ pnpm dlx shadcn@latest add https://uipkge.dev/r/react/loading-bar.json $ npx shadcn@latest add https://uipkge.dev/r/react/loading-bar.json $ yarn dlx shadcn@latest add https://uipkge.dev/r/react/loading-bar.json $ bunx shadcn@latest add https://uipkge.dev/r/react/loading-bar.json 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
fail: () => void
inc: (amount?: number) => void
set: (value: number) => void
} Files installed (3)
-
components/ui/loading-bar/LoadingBar.tsx 7.9 kB
'use client' import * as React from 'react' import { cn } from '@/lib/utils' export interface LoadingBarHandle { start: (from?: number) => void finish: () => void error: () => void fail: () => 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) /** Imperative fail() tints the bar without requiring the error prop. */ const [internalError, setInternalError] = React.useState(false) /** After finish/fail, fade out then reset. */ const [fading, setFading] = React.useState(false) const rafRef = React.useRef<number | null>(null) const hideTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null) /** Bumped on start/finish/fail so in-flight trickle frames abort. */ const generationRef = React.useRef(0) 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 clearTimers = React.useCallback(() => { if (rafRef.current) { cancelAnimationFrame(rafRef.current) rafRef.current = null } if (hideTimerRef.current) { clearTimeout(hideTimerRef.current) hideTimerRef.current = null } }, []) const set = React.useCallback((v: number) => { setInternal(v) onValueChangeRef.current?.(v) }, []) /** Slowly creep the bar toward a soft ceiling so progress feels alive. */ const trickle = React.useCallback((gen: number) => { if (rafRef.current) cancelAnimationFrame(rafRef.current) const step = () => { if (gen !== generationRef.current) return setInternal((prev) => { if (gen !== generationRef.current || prev >= 95) return prev const next = Math.min(95, prev + (95 - prev) * 0.04 + 0.15) onValueChangeRef.current?.(next) if (next < 95 && gen === generationRef.current) { rafRef.current = requestAnimationFrame(step) } return next }) } rafRef.current = requestAnimationFrame(step) }, []) const start = React.useCallback( (from = 20) => { clearTimers() generationRef.current += 1 const gen = generationRef.current setInternalError(false) setFading(false) set(from) trickle(gen) }, [set, trickle, clearTimers], ) const inc = React.useCallback((amount = 10) => { setInternal((prev) => { const next = Math.min(99, prev + amount) onValueChangeRef.current?.(next) return next }) }, []) const finish = React.useCallback(() => { clearTimers() generationRef.current += 1 setInternalError(false) set(100) onFinishRef.current?.() // Hold full bar briefly, then fade + reset so the next start() is clean. hideTimerRef.current = setTimeout(() => { setFading(true) hideTimerRef.current = setTimeout(() => { setInternal(0) onValueChangeRef.current?.(0) setFading(false) hideTimerRef.current = null }, 300) }, 200) }, [set, clearTimers]) const fail = React.useCallback(() => { clearTimers() generationRef.current += 1 setInternalError(true) setInternal(100) onValueChangeRef.current?.(100) onFinishRef.current?.() hideTimerRef.current = setTimeout(() => { setFading(true) hideTimerRef.current = setTimeout(() => { setInternal(0) setInternalError(false) onValueChangeRef.current?.(0) setFading(false) hideTimerRef.current = null }, 300) }, 400) }, [clearTimers]) React.useEffect(() => { return () => { clearTimers() } }, [clearTimers]) 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 isError = error || internalError const barColor = color || (isError ? 'var(--destructive)' : 'var(--primary)') const visible = !hidden && !fading && (indeterminate || internal > 0) return ( <div data-uipkge="" data-slot="loading-bar" data-position={position} data-state={isError ? '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-busy={visible && !isError ? true : undefined} 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