Velocity UI
Loading…
Menu

Loader Collection

A collection of 10 minimal, premium loaders featuring smooth animations for loading states and transitions.

Installation

Add this component to your project using the CLI:

terminal
npx vui-registry-cli-v1 add loader-stack

Source Code

loader-stack.tsx
 'use client'
 
 import React, { useState } from 'react'
 import { motion, AnimatePresence } from 'framer-motion'
 import SpinnerLoader from './spinner-loader'
 import DotsLoader from './dots-loader'
 import BarLoader from './bar-loader'
 import PulseLoader from './pulse-loader'
 import WaveLoader from './wave-loader'
 import RingLoader from './ring-loader'
import OrbitLoader from './orbit-loader'
import BounceLoader from './bounce-loader'
import FadeLoader from './fade-loader'
 
 export default function LoaderStack() {
   const [copied, setCopied] = useState<string | null>(null)
   const copy = (name: string, cmd: string) => {
     navigator.clipboard.writeText(cmd)
     setCopied(name)
     setTimeout(() => setCopied(null), 1200)
   }
   const variants = [
     { name: 'Spinner', Component: SpinnerLoader, cmd: 'npx vui-registry-cli-v1 add loader-spinner' },
     { name: 'Dots', Component: DotsLoader, cmd: 'npx vui-registry-cli-v1 add loader-dots' },
     { name: 'Bar', Component: BarLoader, cmd: 'npx vui-registry-cli-v1 add loader-bar' },
     { name: 'Pulse', Component: PulseLoader, cmd: 'npx vui-registry-cli-v1 add loader-pulse' },
     { name: 'Wave', Component: WaveLoader, cmd: 'npx vui-registry-cli-v1 add loader-wave' },
    { name: 'Ring', Component: RingLoader, cmd: 'npx vui-registry-cli-v1 add loader-ring' },
    { name: 'Orbit', Component: OrbitLoader, cmd: 'npx vui-registry-cli-v1 add loader-orbit' },
    { name: 'Bounce', Component: BounceLoader, cmd: 'npx vui-registry-cli-v1 add loader-bounce' },
    { name: 'Fade', Component: FadeLoader, cmd: 'npx vui-registry-cli-v1 add loader-fade' },
   ]
 
   return (
    <div className="w-full grid gap-6 md:grid-cols-2 p-4">
       {variants.map((v) => (
        <div key={v.name} className="rounded-2xl p-[2px] border border-border bg-card/50 backdrop-blur-sm overflow-hidden">
          <div className="rounded-xl border border-border bg-background/60">
            <div className="flex items-center justify-between px-4 py-3 border-b border-border/50 bg-muted/10 relative">
             <div className="text-xs font-medium text-muted-foreground uppercase tracking-wider">{v.name}</div>
             <button
               onClick={() => copy(v.name, v.cmd)}
                className="text-[10px] font-mono border-b border-border absolute top-2 right-3"
             >
               <AnimatePresence mode="wait">
                 {copied === v.name ? (
                    <motion.span key="copied" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="text-emerald-500">
                      Copied
                   </motion.span>
                 ) : (
                   <motion.span key="copy" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
                     CLI
                   </motion.span>
                 )}
               </AnimatePresence>
             </button>
            </div>
            <div className="p-6 min-h-[140px] grid place-items-center">
              <v.Component />
            </div>
           </div>
        </div>
       ))}
     </div>
   )
 }

Source Code

spinner-loader.tsx
'use client'

import React from 'react'
import { motion } from 'framer-motion'

export default function EliteSpinner() {
  return (
    <div className="flex flex-col items-center justify-center py-20">
      <div className="relative w-12 h-12">
        
        {/* 1. THE TRACK: The background "rail" for the spinner */}
        <div className="absolute inset-0 rounded-full border-[3px] border-zinc-100 dark:border-zinc-800/50" />

        {/* 2. THE SPINNER: High-contrast, weighted arcs */}
        <motion.svg
          viewBox="0 0 50 50"
          className="absolute inset-0 w-full h-full"
          animate={{ rotate: 360 }}
          transition={{
            duration: 1,
            repeat: Infinity,
            ease: "linear"
          }}
        >
          {/* Primary Arc (The Head) */}
          <motion.circle
            cx="25"
            cy="25"
            r="21"
            fill="none"
            stroke="currentColor" // Adapts to text color
            strokeWidth="3.5"
            strokeLinecap="round"
            strokeDasharray="30 150" // Precise arc length
            className="text-zinc-900 dark:text-zinc-100"
          />

          {/* Secondary Arc (The Tail - for visual weight) */}
          <motion.circle
            cx="25"
            cy="25"
            r="21"
            fill="none"
            stroke="currentColor"
            strokeWidth="3.5"
            strokeLinecap="round"
            strokeDasharray="1 150"
            className="text-zinc-400 dark:text-zinc-600"
            style={{ rotate: 120 }} // Positioned behind the head
          />
        </motion.svg>

        {/* 3. CENTER GLOW: Subtle light source in the middle */}
        <div className="absolute inset-0 flex items-center justify-center">
             <div className="w-1 h-1 rounded-full bg-zinc-400 dark:bg-zinc-500 opacity-20" />
        </div>
      </div>

      {/* 4. REFINED STATUS: Minimalist, spaced typography */}
      <motion.p 
        initial={{ opacity: 0.4 }}
        animate={{ opacity: [0.4, 1, 0.4] }}
        transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
        className="mt-6 text-[11px] font-medium tracking-[0.3em] uppercase text-zinc-500 dark:text-zinc-400"
      >
        Processing
      </motion.p>
    </div>
  )
}

Source Code

dots-loader.tsx
 'use client'
 
 import React from 'react'
 import { motion } from 'framer-motion'
 
 export default function DotsLoader() {
   return (
     <div className="flex items-center gap-2">
       {[0, 1, 2].map((i) => (
         <motion.div
           key={i}
           className="w-2.5 h-2.5 rounded-full bg-foreground/70"
           animate={{ y: [0, -6, 0], opacity: [0.6, 1, 0.6] }}
           transition={{ repeat: Infinity, duration: 0.8, delay: i * 0.1 }}
         />
       ))}
     </div>
   )
 }

Source Code

bar-loader.tsx
'use client'

import React from 'react'
import { motion } from 'framer-motion'

export default function LuxuryBeamLoader() {
  return (
    <div className="flex flex-col items-start gap-3 py-10 px-6">
      <div className="relative w-72 group">
        
        {/* 1. THE RAIL: Ultra-thin, low-contrast foundation */}
        <div className="h-[1px] w-full bg-zinc-200 dark:bg-zinc-800 rounded-full" />

        {/* 2. THE EMITTER: A localized glow that moves with the bar */}
        <motion.div
          initial={{ x: "0%" }}
          animate={{ x: "100%" }}
          transition={{
            repeat: Infinity,
            duration: 2,
            ease: [0.4, 0, 0.2, 1],
          }}
          className="absolute top-0 left-0 w-24 h-[1px] overflow-visible"
        >
          {/* Central Bright Beam */}
          <div className="w-full h-full bg-gradient-to-r from-transparent via-zinc-950 dark:via-white to-transparent" />
          
          {/* Light Leak / Bloom effect */}
          <div className="absolute inset-0 w-full h-full bg-zinc-950 dark:bg-white blur-[4px] opacity-30" />
        </motion.div>

        {/* 3. THE "CHASE" STREAK: A secondary, faster pulse for complexity */}
        <motion.div
          initial={{ x: "-20%" }}
          animate={{ x: "120%" }}
          transition={{
            repeat: Infinity,
            duration: 1.5,
            ease: "easeInOut",
            delay: 0.1,
          }}
          className="absolute top-0 left-0 w-16 h-[1px]"
        >
          <div className="w-full h-full bg-gradient-to-r from-transparent via-zinc-400 dark:via-zinc-500 to-transparent opacity-40" />
        </motion.div>
      </div>

      {/* 4. TECHNICAL DATA: Minimalist layout */}
      <div className="w-72 flex justify-between items-baseline">
        <div className="flex flex-col">
          <span className="text-[9px] font-bold uppercase tracking-[0.3em] text-zinc-900 dark:text-zinc-100">
            System Hash
          </span>
          <span className="text-[8px] font-mono text-zinc-400 dark:text-zinc-600">
            0x71C...A42
          </span>
        </div>
        
        <div className="text-right">
          <motion.span 
            animate={{ opacity: [0.4, 1, 0.4] }}
            transition={{ duration: 2, repeat: Infinity }}
            className="text-[10px] font-medium text-zinc-500"
          >
            Processing...
          </motion.span>
        </div>
      </div>
    </div>
  )
}

Source Code

pulse-loader.tsx
 'use client'
 
 import React from 'react'
 import { motion } from 'framer-motion'
 
 export default function PulseLoader() {
   return (
     <motion.div
       className="w-6 h-6 rounded-full bg-foreground/60"
       animate={{ scale: [1, 1.3, 1], opacity: [0.8, 1, 0.8] }}
       transition={{ repeat: Infinity, duration: 0.9, ease: 'easeInOut' }}
     />
   )
 }

Source Code

wave-loader.tsx
 'use client'
 
 import React from 'react'
 import { motion } from 'framer-motion'
 
 export default function WaveLoader() {
   return (
     <div className="flex items-end gap-1 h-6">
       {[0, 1, 2, 3, 4].map((i) => (
         <motion.div
           key={i}
           className="w-2 bg-foreground/70 rounded"
           animate={{ height: [8, 24, 8] }}
           transition={{ repeat: Infinity, duration: 1.2, delay: i * 0.08 }}
         />
       ))}
     </div>
   )
 }

Source Code

ring-loader.tsx
 'use client'
 
 import React from 'react'
 import { motion } from 'framer-motion'
 
 export default function RingLoader() {
   return (
     <div className="relative w-12 h-12">
       <motion.div
         className="absolute inset-0 rounded-full border-2 border-foreground/20"
       />
       <motion.div
         className="absolute inset-0 rounded-full border-2 border-transparent border-t-foreground"
         animate={{ rotate: 360 }}
         transition={{ repeat: Infinity, duration: 1.2, ease: 'linear' }}
       />
     </div>
   )
 }

Source Code

orbit-loader.tsx
 'use client'
 
 import React from 'react'
 import { motion } from 'framer-motion'
 
 export default function OrbitLoader() {
   return (
     <div className="relative w-12 h-12">
       <motion.div
         className="absolute inset-0 rounded-full border border-foreground/20"
       />
       <motion.div
         className="absolute left-1/2 top-0 w-1.5 h-1.5 -ml-0.75 rounded-full bg-foreground"
         animate={{ rotate: 360 }}
         style={{ transformOrigin: '0 12px' }}
         transition={{ repeat: Infinity, duration: 1.4, ease: 'linear' }}
       />
     </div>
   )
 }

Source Code

bounce-loader.tsx
'use client'

import React from 'react'
import { motion } from 'framer-motion'

export default function VelocityShardLoader() {
  return (
    <div className="flex flex-col items-center justify-center py-20 gap-10">
      <div className="relative w-20 h-20 flex items-center justify-center">
        
        {/* 1. THE VELOCITY RING: Gauge backdrops */}
        <div className="absolute inset-0 rounded-full border-[1px] border-zinc-200 dark:border-zinc-800" />
        <div className="absolute inset-2 rounded-full border-[0.5px] border-zinc-100 dark:border-zinc-900" />

        {/* 2. THE PRECISION SHARDS: High-speed staggering */}
        {[...Array(12)].map((_, i) => (
          <motion.div
            key={i}
            className="absolute inset-0 flex items-start justify-center pt-1"
            style={{ rotate: `${i * 30}deg` }}
          >
            <motion.div
              className="w-[2.5px] rounded-full bg-zinc-950 dark:bg-white"
              animate={{ 
                height: [4, 16, 4],
                opacity: [0.1, 1, 0.1],
                y: [0, 2, 0]
              }}
              transition={{ 
                repeat: Infinity, 
                duration: 1, 
                delay: i * 0.08, 
                ease: [0.4, 0, 0.2, 1] 
              }}
            />
          </motion.div>
        ))}

        {/* 3. THE CLEAN CORE: Minimalist central hub */}
        <div className="relative w-6 h-6 border-[0.5px] border-zinc-200 dark:border-zinc-800 rounded-full flex items-center justify-center bg-transparent">
          <motion.div 
            animate={{ scale: [1, 1.2, 1], opacity: [0.5, 1, 0.5] }}
            transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
            className="w-1.5 h-1.5 bg-zinc-950 dark:bg-white rounded-full shadow-[0_0_8px_rgba(255,255,255,0.3)]" 
          />
        </div>
      </div>

      {/* 4. BRANDING: Velocity UI Layout */}
      <div className="flex flex-col items-center gap-1.5">
        <div className="flex items-center gap-3">
          <span className="text-[11px] font-black uppercase tracking-[0.4em] text-zinc-950 dark:text-white">
            Velocity<span className="text-zinc-400 font-light ml-1">UI</span>
          </span>
        </div>
        
        {/* Dynamic Status Bar */}
        <div className="flex items-center gap-2">
          <div className="h-[2px] w-12 bg-zinc-100 dark:bg-zinc-900 overflow-hidden relative rounded-full">
            <motion.div 
              animate={{ x: ["-100%", "100%"] }}
              transition={{ duration: 1.2, repeat: Infinity, ease: "easeInOut" }}
              className="absolute inset-0 bg-zinc-950 dark:bg-white w-1/2 rounded-full"
            />
          </div>
          <span className="text-[8px] font-mono text-zinc-500 uppercase tracking-tighter">
            System.Active
          </span>
        </div>
      </div>
    </div>
  )
}

Source Code

fade-loader.tsx
'use client'

import React from 'react'
import { motion } from 'framer-motion'

export default function FadeLoader() {
  const bars = [0, 1, 2, 3]

  return (
    <div className="flex items-center gap-1.5" aria-label="Loading content">
      {bars.map((i) => (
        <motion.div
          key={i}
          className="w-1.5 h-5 rounded-full bg-current opacity-20"
          animate={{ 
            opacity: [0.2, 1, 0.2],
            scaleY: [1, 1.3, 1], // Adds a "pulse" feel
          }}
          transition={{ 
            repeat: Infinity, 
            duration: 1, 
            delay: i * 0.15, // Better staggered rhythm
            ease: "easeInOut" 
          }}
        />
      ))}
    </div>
  )
}

Dependencies

  • framer-motion: latest
  • clsx: latest
  • tailwind-merge: latest

Props

Component property reference.

NameTypeDefaultDescription
sizenumber40Size of the loader in pixels.
colorstring'currentColor'CSS color for the loader elements.
speednumber1Animation speed multiplier.
Context Worth Keeping In Orbit

Most components here are inspired by outstanding libraries and creators in the ecosystem. I don’t claim to be the original author — this is my space for learning, rebuilding, and understanding great work at a deeper level.

I’m still a student of the craft, constantly studying the best and translating what I learn through my own perspective. Every piece reflects curiosity, respect for the community, and small creative touches that feel true to me.

I’ve done my best to credit inspirations properly. If anything is missing or inaccurate, I truly appreciate a message so it can be corrected with care.