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 Named registry:
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 4.5 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) let raf: number | null = null watch( () => props.modelValue, (v) => { internal.value = v }, ) const pct = computed(() => Math.min(100, Math.max(0, internal.value))) const barColor = computed(() => props.color || (props.error ? 'var(--destructive)' : 'var(--primary)')) const visible = computed(() => !props.hidden && (props.indeterminate || internal.value > 0)) function set(v: number) { internal.value = v emit('update:modelValue', v) if (v >= 100) emit('finish') } // Imperative API exposed for the composable / parent to drive the bar. function start(from = 20) { set(from) tick(from) } function tick(target: number) { if (raf) cancelAnimationFrame(raf) const step = () => { if (internal.value >= 100 || internal.value >= target) return const next = Math.min(target, internal.value + (100 - internal.value) * 0.02 + 0.5) set(next) if (next < target) raf = requestAnimationFrame(step) } raf = requestAnimationFrame(step) } function inc(amount = 10) { set(Math.min(99, internal.value + amount)) } function finish() { if (raf) cancelAnimationFrame(raf) set(100) } function fail() { if (raf) cancelAnimationFrame(raf) internal.value = 100 // error prop drives the color; emit finish so callers can hide. emit('update:modelValue', 100) emit('finish') } onBeforeUnmount(() => { if (raf) cancelAnimationFrame(raf) }) defineExpose({ start, finish, fail, error: fail, inc, set }) </script> <template> <div data-uipkge data-slot="loading-bar" :data-position="position" :data-state="error ? '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-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