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
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