UIPackage
Menu

Framework

Change language

Boilerplate repo

Progress Linear

progress-linear ui
Boilerplate repo

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
Named registry: npx shadcn-vue@latest add @uipkge/progress-linear Installs to: app/components/ui/progress-linear/

Examples

Loading interactive previews…

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

npm dependencies

Files installed (3)

  • app/components/ui/progress-linear/ProgressLinear.vue 5.5 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 ? 'progress-striped-light' : '',
            !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 ? 'progress-striped-medium' : '',
          ]"
          :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 progress-stream"
            :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 ? 'progress-striped-heavy' : '',
          ]"
          :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>
    /* Gradient utilities */
    .progress-striped-light {
      background-image: repeating-linear-gradient(
        45deg,
        transparent,
        transparent 8px,
        rgba(255, 255, 255, 0.1) 8px,
        rgba(255, 255, 255, 0.1) 16px
      );
    }
    
    .progress-striped-medium {
      background-image: repeating-linear-gradient(
        45deg,
        transparent,
        transparent 8px,
        rgba(255, 255, 255, 0.15) 8px,
        rgba(255, 255, 255, 0.15) 16px
      );
    }
    
    .progress-striped-heavy {
      background-image: repeating-linear-gradient(
        45deg,
        transparent,
        transparent 8px,
        rgba(255, 255, 255, 0.25) 8px,
        rgba(255, 255, 255, 0.25) 16px
      );
    }
    
    .progress-stream {
      background-image: repeating-linear-gradient(
        90deg,
        transparent,
        transparent 10px,
        rgba(255, 255, 255, 0.2) 10px,
        rgba(255, 255, 255, 0.2) 20px
      );
      background-size: 40px 40px;
    }
    
    @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-success',
          warning: 'bg-warning',
          info: 'bg-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