UIPackage

Progress Linear

Vue feedback
Edit on GitHub

Top-of-screen page-loading bar (à la NProgress / nuxt loading-indicator). Auto-advances while a navigation or fetch is in flight, then completes. Drop into the app shell once.

Also available for React ->

Installation

$ npx shadcn-vue@latest add https://uipkge.dev/r/vue/progress-linear.json

Or with the named registry: npx shadcn-vue@latest add @uipkge/progress-linear

Examples

Props

Name Type / Values Default Required
class HTMLAttributes['class'] optional
modelValue number 0 optional
bgColor string optional
buffer number optional
color string optional
height number | string optional
indeterminate boolean false optional
reverse boolean false optional
rounded
'none''sm''default''md''lg''xl''full'
optional
stream boolean false optional
striped boolean false optional
active boolean true optional

Dependencies

Files (3)

  • app/components/ui/progress-linear/ProgressLinear.vue 5.1 kB
    <script setup lang="ts">
    import { computed } from 'vue'
    import type { HTMLAttributes } from 'vue'
    import { cn } from '@/lib/utils'
    import { progressLinearVariants } from './progress-linear.variants'
    
    const props = withDefaults(
      defineProps<{
        class?: HTMLAttributes['class']
        modelValue?: number
        bgColor?: string
        buffer?: number
        color?: string
        height?: number | string
        indeterminate?: boolean
        reverse?: boolean
        rounded?: 'none' | 'sm' | 'default' | 'md' | 'lg' | 'xl' | 'full'
        stream?: boolean
        striped?: boolean
        active?: boolean
      }>(),
      {
        modelValue: 0,
        indeterminate: false,
        reverse: false,
        stream: false,
        striped: false,
        active: true,
      },
    )
    
    const normalizedValue = computed(() => {
      return Math.min(100, Math.max(0, props.modelValue))
    })
    
    const normalizedBuffer = computed(() => {
      return Math.min(100, Math.max(0, props.buffer || 0))
    })
    
    const heightValue = computed(() => {
      if (typeof props.height === 'number') return `${props.height}px`
      if (typeof props.height === 'string') return props.height
      return '4px'
    })
    
    const bgColorValue = computed(() => props.bgColor || 'currentColor')
    
    const progressColorValue = computed(() => props.color || 'currentColor')
    
    const containerClasses = computed(() => cn(progressLinearVariants({ rounded: props.rounded }), props.class))
    </script>
    
    <template>
      <div
        data-uipkge
        data-slot="progress-linear"
        role="progressbar"
        :aria-valuemin="0"
        :aria-valuemax="100"
        :aria-valuenow="indeterminate ? undefined : normalizedValue"
        :class="containerClasses"
        :style="{ height: heightValue }"
      >
        <!-- Background -->
        <div
          class="absolute inset-0 transition-colors duration-300"
          :class="[
            striped
              ? 'bg-[repeating-linear-gradient(45deg,transparent,transparent_8px,rgba(255,255,255,0.1)_8px,rgba(255,255,255,0.1)_16px)]'
              : '',
            !indeterminate && normalizedBuffer > 0 ? 'opacity-30' : 'opacity-100',
          ]"
          :style="{
            backgroundColor: bgColorValue,
            width: normalizedBuffer > 0 ? `${normalizedBuffer}%` : '100%',
          }"
        />
    
        <!-- Buffer (if buffer > 0) -->
        <div
          v-if="!indeterminate && normalizedBuffer > 0 && normalizedBuffer < 100"
          class="absolute inset-0 transition-colors duration-300"
          :class="[
            reverse ? 'right-0 left-auto' : 'right-auto left-0',
            striped
              ? 'bg-[repeating-linear-gradient(45deg,transparent,transparent_8px,rgba(255,255,255,0.15)_8px,rgba(255,255,255,0.15)_16px)]'
              : '',
          ]"
          :style="{
            backgroundColor: bgColorValue,
            width: `${normalizedBuffer}%`,
            opacity: 0.3,
          }"
        />
    
        <!-- Stream lines (when stream is true) -->
        <div
          v-if="stream && !indeterminate && active"
          class="absolute inset-0 overflow-hidden"
          :class="reverse ? 'right-0 left-auto' : 'right-auto left-0'"
        >
          <div
            class="animate-stream absolute inset-0 bg-[repeating-linear-gradient(90deg,transparent,transparent_10px,rgba(255,255,255,0.2)_10px,rgba(255,255,255,0.2)_20px)] bg-[length:40px_40px]"
            :style="{ width: `${normalizedBuffer || 100}%` }"
          />
        </div>
    
        <!-- Progress bar -->
        <div
          class="absolute inset-y-0 transition-colors duration-300"
          :class="[
            reverse ? 'right-0 left-auto' : 'right-auto left-0',
            indeterminate ? 'motion-safe:animate-indeterminate' : '',
            striped && !indeterminate
              ? 'bg-[repeating-linear-gradient(45deg,transparent,transparent_8px,rgba(255,255,255,0.25)_8px,rgba(255,255,255,0.25)_16px)]'
              : '',
          ]"
          :style="{
            width: indeterminate ? '100%' : `${normalizedValue}%`,
            backgroundColor: indeterminate ? undefined : progressColorValue,
          }"
        >
          <!-- Indeterminate animations -->
          <template v-if="indeterminate">
            <div
              class="motion-safe:animate-indeterminate1 absolute inset-y-0 w-full bg-inherit"
              :style="{ backgroundColor: progressColorValue }"
            />
            <div
              class="motion-safe:animate-indeterminate2 absolute inset-y-0 w-full bg-inherit"
              :style="{ backgroundColor: progressColorValue }"
            />
          </template>
        </div>
      </div>
    </template>
    
    <style scoped>
    @media (prefers-reduced-motion: no-preference) {
      .animate-indeterminate {
        animation: indeterminate 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
      }
    
      .animate-indeterminate1 {
        animation: indeterminate1 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
      }
    
      .animate-indeterminate2 {
        animation: indeterminate2 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
      }
    }
    
    .animate-stream {
      animation: stream 1s linear infinite;
    }
    
    @keyframes indeterminate {
      0% {
        transform: translateX(-100%);
      }
      100% {
        transform: translateX(400%);
      }
    }
    
    @keyframes indeterminate1 {
      0% {
        transform: translateX(-100%);
      }
      100% {
        transform: translateX(400%);
      }
    }
    
    @keyframes indeterminate2 {
      0% {
        transform: translateX(-100%);
        opacity: 1;
      }
      100% {
        transform: translateX(400%);
        opacity: 0;
      }
    }
    
    @keyframes stream {
      0% {
        transform: translateX(0);
      }
      100% {
        transform: translateX(40px);
      }
    }
    </style>
  • app/components/ui/progress-linear/progress-linear.variants.ts 1.1 kB
    import type { VariantProps } from 'class-variance-authority'
    import { cva } from 'class-variance-authority'
    
    /**
     * Variant definitions live in their own file (rather than inline in the
     * SFC) so `ProgressLinear.vue` can import them without the cva runtime
     * coupling that breaks SSR when several `<ProgressLinear>` instances
     * render before the module graph fully resolves. Sibling pattern to
     * `card/card.variants.ts`.
     */
    export const progressLinearVariants = cva('relative overflow-hidden w-full', {
      variants: {
        rounded: {
          none: 'rounded-none',
          sm: 'rounded-sm',
          default: 'rounded-full',
          md: 'rounded-md',
          lg: 'rounded-lg',
          xl: 'rounded-xl',
          full: 'rounded-full',
        },
        color: {
          default: 'bg-primary',
          primary: 'bg-primary',
          secondary: 'bg-secondary',
          destructive: 'bg-destructive',
          success: 'bg-[var(--success)]',
          warning: 'bg-[var(--warning)]',
          info: 'bg-[var(--info)]',
          error: 'bg-destructive',
        },
      },
      defaultVariants: {
        rounded: 'default',
        color: 'default',
      },
    })
    
    export type ProgressLinearVariants = VariantProps<typeof progressLinearVariants>
  • app/components/ui/progress-linear/index.ts 0.3 kB
    export { default as ProgressLinear } from './ProgressLinear.vue'
    
    // Re-export variant API from the sibling file (kept separate to avoid the
    // ProgressLinear.vue <-> index.ts circular import that broke dev SSR).
    export { progressLinearVariants, type ProgressLinearVariants } from './progress-linear.variants'

Raw manifest: https://uipkge.dev/r/vue/progress-linear.json