Loading Bar
loading-bar ui Top-of-viewport NProgress-style progress bar. Drive it imperatively via the useLoadingBar composable (start/finish/error/inc) or with v-model. Supports indeterminate mode, custom color and height, top/bottom anchoring, and an optional trailing spinner.
Also available for React ->Installation
$ pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/loading-bar.json $ npx shadcn-vue@latest add https://uipkge.dev/r/vue/loading-bar.json $ yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/loading-bar.json $ bunx shadcn-vue@latest add https://uipkge.dev/r/vue/loading-bar.json npx shadcn-vue@latest add @uipkge/loading-bar Installs to: app/components/ui/loading-bar/ Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
modelValue 0–100 progress value. Use with v-model or drive via the composable. | number | 0 | optional |
color Bar color. Accepts any CSS color value. | string | '' | optional |
height Bar height in px. | number | 3 | optional |
indeterminate Indeterminate sliding animation (ignores modelValue). | boolean | false | optional |
position Anchor the bar to the top or bottom of the viewport. | 'top''bottom' | 'top' | optional |
spinner Show a spinner at the trailing edge of the bar. | boolean | false | optional |
error Error state tints the bar. | boolean | false | optional |
hidden Hide the bar entirely (e.g. when finished). | boolean | false | optional |
class | HTMLAttributes['class'] | — | 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)
-
app/components/ui/loading-bar/LoadingBar.vue 6.1 kB
<script setup lang="ts"> import { computed, onBeforeUnmount, ref, watch } from 'vue' import type { HTMLAttributes } from 'vue' import { cn } from '@/lib/utils' const props = withDefaults( defineProps<{ /** 0–100 progress value. Use with v-model or drive via the composable. */ modelValue?: number /** Bar color. Accepts any CSS color value. */ color?: string /** Bar height in px. */ height?: number /** Indeterminate sliding animation (ignores modelValue). */ 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 class?: HTMLAttributes['class'] }>(), { modelValue: 0, color: '', height: 3, indeterminate: false, position: 'top', spinner: false, error: false, hidden: false, }, ) const emit = defineEmits<{ (e: 'update:modelValue', value: number): void (e: 'finish'): void }>() const internal = ref(props.modelValue) /** Imperative fail() tints the bar without requiring the error prop. */ const internalError = ref(false) /** After finish/fail, fade out then reset. */ const fading = ref(false) let raf: number | null = null let hideTimer: ReturnType<typeof setTimeout> | null = null /** Bumped on start/finish/fail so in-flight trickle frames abort. */ let generation = 0 watch( () => props.modelValue, (v) => { internal.value = v }, ) const pct = computed(() => Math.min(100, Math.max(0, internal.value))) const isError = computed(() => props.error || internalError.value) const barColor = computed(() => props.color || (isError.value ? 'var(--destructive)' : 'var(--primary)')) const visible = computed(() => !props.hidden && !fading.value && (props.indeterminate || internal.value > 0)) function clearTimers() { if (raf) { cancelAnimationFrame(raf) raf = null } if (hideTimer) { clearTimeout(hideTimer) hideTimer = null } } function set(v: number) { internal.value = v emit('update:modelValue', v) } // Imperative API exposed for the composable / parent to drive the bar. function start(from = 20) { clearTimers() generation += 1 const gen = generation internalError.value = false fading.value = false set(from) // Trickle toward ~95 while the consumer is still working (classic NProgress). trickle(gen) } /** Slowly creep the bar toward a soft ceiling so progress feels alive. */ function trickle(gen: number) { if (raf) cancelAnimationFrame(raf) const step = () => { if (gen !== generation || internalError.value || fading.value) return if (internal.value >= 95) return // Asymptotic crawl — slows as it approaches the ceiling. const next = Math.min(95, internal.value + (95 - internal.value) * 0.04 + 0.15) set(next) if (next < 95 && gen === generation) raf = requestAnimationFrame(step) } raf = requestAnimationFrame(step) } function inc(amount = 10) { if (fading.value) return set(Math.min(99, internal.value + amount)) } function finish() { clearTimers() generation += 1 internalError.value = false set(100) emit('finish') // Hold full bar briefly, then fade + reset so the next start() is clean. hideTimer = setTimeout(() => { fading.value = true hideTimer = setTimeout(() => { internal.value = 0 emit('update:modelValue', 0) fading.value = false hideTimer = null }, 300) }, 200) } function fail() { clearTimers() generation += 1 internalError.value = true internal.value = 100 emit('update:modelValue', 100) emit('finish') hideTimer = setTimeout(() => { fading.value = true hideTimer = setTimeout(() => { internal.value = 0 internalError.value = false emit('update:modelValue', 0) fading.value = false hideTimer = null }, 300) }, 400) } onBeforeUnmount(() => { clearTimers() }) defineExpose({ start, finish, fail, error: fail, inc, set }) </script> <template> <div data-uipkge data-slot="loading-bar" :data-position="position" :data-state="isError ? 'error' : indeterminate ? 'indeterminate' : 'determinate'" :class=" 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', props.class, ) " :style="{ height: `${height}px` }" role="progressbar" :aria-valuemin="0" :aria-valuemax="100" :aria-valuenow="indeterminate ? undefined : pct" :aria-busy="visible && !isError ? true : undefined" :aria-hidden="!visible" > <!-- Track --> <div class="absolute inset-0 bg-transparent" /> <!-- Determinate bar --> <div v-if="!indeterminate" data-slot="loading-bar-fill" class="absolute inset-y-0 left-0 transition-[width] duration-200 ease-out" :style="{ width: `${pct}%`, backgroundColor: barColor }" > <div v-if="spinner" data-slot="loading-bar-spinner" class="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> <!-- Indeterminate sliding bar --> <div v-else data-slot="loading-bar-indeterminate" class="loading-bar-indeterminate absolute inset-y-0 w-1/3" :style="{ backgroundColor: barColor }" > <div v-if="spinner" data-slot="loading-bar-spinner" class="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> </div> </template> <style scoped> @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> -
app/components/ui/loading-bar/useLoadingBar.ts 1.7 kB
import { ref, shallowRef } from 'vue' import type { ComponentPublicInstance } from 'vue' export interface LoadingBarHandle { start: (from?: number) => void finish: () => void error: () => void inc: (amount?: number) => void set: (value: number) => void } /** * Composable that drives a <LoadingBar> instance via a template ref. * * Usage: * const bar = useLoadingBar() * <LoadingBar :ref="bar.setRef" /> * bar.start() * await fetch(...) * bar.finish() */ export function useLoadingBar() { const refEl = shallowRef<ComponentPublicInstance | null>(null) const loading = ref(false) const isError = ref(false) function setRef(el: Element | ComponentPublicInstance | null) { refEl.value = el as ComponentPublicInstance | null } function getHandle(): LoadingBarHandle | null { return (refEl.value as unknown as LoadingBarHandle) ?? null } function start(from = 20) { isError.value = false loading.value = true const h = getHandle() if (h && typeof h.start === 'function') h.start(from) } function finish() { loading.value = false const h = getHandle() if (h && typeof h.finish === 'function') h.finish() } function error() { isError.value = true loading.value = false const h = getHandle() if (h && typeof h.error === 'function') h.error() } function inc(amount = 10) { const h = getHandle() if (h && typeof h.inc === 'function') h.inc(amount) } function set(value: number) { const h = getHandle() if (h && typeof h.set === 'function') h.set(value) } return { setRef, loading, isError, start, finish, error, inc, set, } } -
app/components/ui/loading-bar/index.ts 0.1 kB
export { default as LoadingBar } from './LoadingBar.vue' export { useLoadingBar, type LoadingBarHandle } from './useLoadingBar'
Raw manifest: https://uipkge.dev/r/vue/loading-bar.json