Menu

Anime Scroll Story Rail

A custom scroll experience with anime.js-powered scroll position indicators and decorative progress elements. Replaces the native scrollbar with an animated, themed alternative.

Preview

Live interactive preview.

Installation

Add this component to your project using the CLI:

terminal
npx -y vui-registry-cli-v1@latest add anime-js-scrollbar

Source Code

anime-js-scrollbar.tsx
export { default as AnimeJsScrollbar } from "./velocity-story-rail";
export { default } from "./velocity-story-rail";

Source Code

velocity-story-rail-parts.tsx
'use client'

import React, { useEffect, useRef, useState } from 'react'
import { motion, useScroll, useTransform, useSpring, useVelocity } from 'framer-motion'

export const SYSTEM_VERSION = 'VELOCITY_RAIL_V2.0'

export const SECTIONS = [
  {
    id: '01',
    title: 'SYMMETRY',
    tag: 'GRID_SYSTEM',
    desc: 'Mathematical precision meets fluid design.',
    para: 'The Velocity Rail uses a sub-pixel grid system so each transition feels intentional. Content locks into place while the background subtly breathes with motion.',
    img: 'https://images.unsplash.com/photo-1519710164239-da123dc03ef4?q=80&w=2070&auto=format&fit=crop',
    metrics: { cpu: '08%', mem: '0.9GB', net: '1.2gbps' },
  },
  {
    id: '02',
    title: 'KINETICS',
    tag: 'MOTION_LOGIC',
    desc: 'Weight, gravity, and momentum preservation.',
    para: 'Instead of jarring jump-cuts, spring-based physics smooth out the experience. Sections glide along the rail with a calm, confident motion profile.',
    img: 'https://images.unsplash.com/photo-1496307042754-b4aa456c4a2d?q=80&w=2070&auto=format&fit=crop',
    metrics: { cpu: '14%', mem: '1.4GB', net: '2.4gbps' },
  },
  {
    id: '03',
    title: 'SURFACE',
    tag: 'OPTIC_GLASS',
    desc: 'Layered depth for immersive interfaces.',
    para: 'Foreground content remains sharp while glass layers, glows, and soft gradients build depth behind it. The result feels cinematic without sacrificing readability.',
    img: 'https://images.unsplash.com/photo-1500534623283-312aade485b7?q=80&w=2070&auto=format&fit=crop',
    metrics: { cpu: '11%', mem: '1.1GB', net: '900mbps' },
  },
]

export const PatternBackground = ({ scrollYProgress }: { scrollYProgress: any }) => {
  const velocity = useVelocity(scrollYProgress)
  const smoothVelocity = useSpring(velocity, { stiffness: 100, damping: 30 })
  const skew = useTransform(smoothVelocity, [-1, 1], [-5, 5])
  const opacity = useTransform(smoothVelocity, [-1, 0, 1], [0.15, 0.04, 0.15])

  return (
    <motion.div
      style={{ skewY: skew, opacity }}
      className="absolute inset-0 z-0 pointer-events-none transition-opacity duration-700"
    >
      <svg width="100%" height="100%" className="absolute inset-0">
        <defs>
          <pattern id="velocity-grid" width="60" height="60" patternUnits="userSpaceOnUse">
            <circle cx="1" cy="1" r="1" fill="currentColor" className="text-foreground/40" />
            <path
              d="M 60 0 L 0 0 0 60"
              fill="none"
              stroke="currentColor"
              strokeWidth="0.5"
              className="text-foreground/10"
            />
          </pattern>
        </defs>
        <rect width="100%" height="100%" fill="url(#velocity-grid)" />
      </svg>
      <div className="absolute top-0 left-1/4 w-[50vw] h-[50vh] bg-foreground/[0.02] blur-[120px] rounded-full" />
      <div className="absolute bottom-0 right-1/4 w-[40vw] h-[40vh] bg-foreground/[0.03] blur-[100px] rounded-full" />
    </motion.div>
  )
}

export const VelocityDock = ({ progress, preview = false }: { progress: any, preview?: boolean }) => {
  const [percent, setPercent] = useState(0)
  const velocity = useVelocity(progress)
  const smoothVelocity = useSpring(velocity, { stiffness: 60, damping: 20 })
  const scale = useTransform(smoothVelocity, [-1, 0, 1], [1.02, 1, 1.02])

  useEffect(() => {
    return progress.on('change', (v: number) => setPercent(Math.round(v * 100)))
  }, [progress])

  return (
    <div className={`${preview ? 'absolute' : 'absolute sm:fixed'} bottom-6 sm:bottom-8 left-0 w-full z-[100] flex justify-center px-3 sm:px-6 pointer-events-none`}>
      <motion.div
        style={{ scale }}
        className="w-full max-w-[92vw] sm:max-w-2xl pointer-events-auto"
      >
        <div className="h-12 sm:h-16 rounded-2xl border border-foreground/10 bg-background/60 backdrop-blur-3xl shadow-[0_30px_60px_-15px_rgba(0,0,0,0.5)] flex items-center px-4 sm:px-6 gap-4 sm:gap-8 overflow-hidden relative">
          <motion.div
            animate={{ x: ['-100%', '200%'] }}
            transition={{ duration: 4, repeat: Infinity, ease: 'linear' }}
            className="absolute inset-y-0 w-32 bg-gradient-to-r from-transparent via-foreground/[0.03] to-transparent skew-x-12"
          />
          <div className="flex items-center gap-3 sm:gap-4 shrink-0 border-r border-foreground/5 pr-4 sm:pr-6">
            <div className="flex flex-col">
              <span className="font-mono text-[6px] text-muted-foreground uppercase tracking-[0.3em] mb-1">
                Index_Pulse
              </span>
              <div className="flex items-end gap-[1.5px] h-3">
                {[...Array(10)].map((_, i) => (
                  <motion.div
                    key={i}
                    animate={{ height: [2, 12, 4], opacity: [0.2, 1, 0.2] }}
                    transition={{
                      repeat: Infinity,
                      duration: 1.5,
                      delay: i * 0.1,
                    }}
                    className="w-[1.5px] bg-foreground/60 rounded-full"
                  />
                ))}
              </div>
            </div>
          </div>
          <div className="flex-1 flex flex-col justify-center gap-2">
            <div className="flex justify-between font-mono text-[7px] text-muted-foreground tracking-[0.4em] uppercase">
              <span>0.00</span>
              <span className="text-foreground font-bold italic">READING_STMT_{percent}%</span>
              <span>1.00</span>
            </div>
            <div className="relative h-[2px] w-full bg-foreground/5 rounded-full overflow-hidden">
              <motion.div
                style={{ scaleX: progress }}
                className="absolute inset-0 bg-foreground origin-left shadow-[0_0_10px_rgba(255,255,255,0.3)]"
              />
            </div>
          </div>
          <div className="hidden sm:flex flex-col items-end shrink-0 pl-6 border-l border-foreground/5 font-mono">
            <span className="text-[8px] text-foreground tracking-widest">{SYSTEM_VERSION}</span>
            <span className="text-[6px] text-muted-foreground uppercase">Buffer_Secure</span>
          </div>
        </div>
      </motion.div>
    </div>
  )
}

export const ContentSection = ({ section }: { section: (typeof SECTIONS)[0] }) => {
  const container = useRef<HTMLDivElement | null>(null)
  const { scrollYProgress } = useScroll({
    target: container,
    offset: ['start end', 'end start'],
  })

  const rotateX = useTransform(scrollYProgress, [0, 0.5, 1], [10, 0, -10])
  const yText = useTransform(scrollYProgress, [0, 1], [50, -50])
  const opacity = useTransform(scrollYProgress, [0, 0.2, 0.8, 1], [0, 1, 1, 0])
  const imgScale = useTransform(scrollYProgress, [0, 0.5, 1], [1.15, 1, 1.15])

  return (
    <motion.section
      ref={container}
      style={{ opacity }}
      className="min-h-full sm:min-h-screen py-12 sm:py-32 flex items-center border-b border-foreground/5 relative overflow-hidden"
    >
      <div className="max-w-7xl mx-auto px-4 sm:px-6 w-full grid grid-cols-1 lg:grid-cols-12 gap-10 sm:gap-16 items-center">
        <div className="lg:col-span-7">
          <motion.div
            style={{ rotateX, perspective: 1500 }}
            className="relative aspect-video rounded-3xl overflow-hidden border border-foreground/10 bg-card group shadow-2xl"
          >
            <motion.img
              style={{ scale: imgScale }}
              src={section.img}
              className="w-full h-full object-cover grayscale brightness-50 contrast-125 group-hover:grayscale-0 group-hover:brightness-100 transition-all duration-[1.5s] ease-out"
            />
            <div className="absolute top-6 left-6 w-12 h-12 border-t border-l border-white/20" />
            <div className="absolute bottom-6 right-6 w-12 h-12 border-b border-r border-white/20" />
          </motion.div>
        </div>
        <div className="lg:col-span-5">
          <motion.div style={{ y: yText }} className="space-y-10">
            <div className="flex items-center gap-4">
              <span className="font-mono text-2xl text-muted-foreground italic font-black">
                ({section.id})
              </span>
              <div className="h-px flex-1 bg-foreground/10" />
              <span className="font-mono text-[9px] tracking-[0.8em] text-muted-foreground uppercase">
                {section.tag}
              </span>
            </div>
            <h2 className="text-4xl sm:text-6xl md:text-8xl font-black tracking-tighter uppercase leading-[0.9]">
              {section.title}
            </h2>
            <div className="space-y-6">
              <p className="text-xl sm:text-2xl font-light leading-tight">{section.desc}</p>
              <p className="text-sm text-muted-foreground leading-relaxed max-w-sm sm:max-w-md">
                {section.para}
              </p>
            </div>
            <div className="pt-8 border-t border-foreground/5 grid grid-cols-3 gap-6">
              {Object.entries(section.metrics).map(([key, val]) => (
                <div key={key}>
                  <div className="font-mono text-[7px] text-muted-foreground uppercase tracking-widest mb-1">
                    {key}
                  </div>
                  <div className="font-mono text-xs font-bold">{val}</div>
                </div>
              ))}
            </div>
          </motion.div>
        </div>
      </div>
    </motion.section>
  )
}

Source Code

velocity-story-rail.tsx
'use client'

import React, { useEffect, useState } from 'react'
import { motion, useScroll, useSpring, useMotionValue } from 'framer-motion'
import Lenis from 'lenis'
import {
  PatternBackground,
  VelocityDock,
  ContentSection,
  SECTIONS,
} from './velocity-story-rail-parts'

export default function VelocityStoryRailV2({ 
  disableLenis,
  preview = false 
}: { 
  disableLenis?: boolean,
  preview?: boolean
}) {
  const [mounted, setMounted] = useState(false)
  const { scrollYProgress } = useScroll()
  const smoothProgress = useSpring(scrollYProgress, {
    stiffness: 45,
    damping: 20,
  })
  const mouseX = useMotionValue(-100)
  const mouseY = useMotionValue(-100)

  useEffect(() => {
    setMounted(true)

    let lenis: Lenis | null = null
    let frameId: number | null = null

    if (!disableLenis && !preview) {
      lenis = new Lenis({ lerp: 0.05, smoothWheel: true })
      const raf = (time: number) => {
        lenis?.raf(time)
        frameId = requestAnimationFrame(raf)
      }
      frameId = requestAnimationFrame(raf)
    }

    const handleMouseMove = (e: MouseEvent) => {
      mouseX.set(e.clientX)
      mouseY.set(e.clientY)
    }

    window.addEventListener('mousemove', handleMouseMove)

    return () => {
      if (frameId !== null) {
        cancelAnimationFrame(frameId)
      }
      if (lenis) {
        lenis.destroy()
      }
      window.removeEventListener('mousemove', handleMouseMove)
    }
  }, [disableLenis, preview])

  if (!mounted) return null

  return (
    <div className="bg-background text-foreground antialiased selection:bg-foreground selection:text-background w-full">
      <div className="relative h-full w-full">
        <PatternBackground scrollYProgress={scrollYProgress} />
        <motion.div
          style={{ x: mouseX, y: mouseY }}
          className={`${preview ? 'absolute' : 'fixed'} top-0 left-0 w-5 h-5 border border-foreground/40 rounded-full z-[120] pointer-events-none mix-blend-difference hidden md:flex items-center justify-center`}
        >
          <div className="w-1 h-1 bg-foreground rounded-full" />
        </motion.div>
        <VelocityDock progress={smoothProgress} preview={preview} />
        <nav className={`${preview ? 'absolute' : 'absolute sm:fixed'} top-0 left-0 w-full p-4 sm:p-8 z-[110] flex justify-between items-start mix-blend-difference`}>
          <div className="flex flex-col gap-1.5">
            <div className="flex items-center gap-3">
              <div className="w-10 h-[2px] bg-foreground" />
              <span className="font-mono text-xs font-black tracking-[1em] uppercase">
                VELOCITY
              </span>
            </div>
            <span className="font-mono text-[8px] text-muted-foreground pl-14 tracking-[0.5em] uppercase">
              SOVEREIGN_PATTERN_02
            </span>
          </div>
          <div className="hidden md:flex items-center gap-12 font-mono text-[9px] tracking-[0.8em] text-muted-foreground">
            <span className="hover:text-foreground cursor-pointer transition-colors">PROTOCOL</span>
            <span className="hover:text-foreground cursor-pointer transition-colors">INDEX</span>
            <div className="flex items-center gap-3 text-foreground/40">
              <div className="w-2 h-2 rounded-full bg-foreground animate-pulse" />
              <span>SYNC_OK</span>
            </div>
          </div>
        </nav>
        <main className="relative z-10">
          <section className="min-h-full sm:h-screen flex flex-col justify-center items-center text-center px-4 sm:px-6">
            <motion.div
              initial={{ opacity: 0, y: 30 }}
              animate={{ opacity: 1, y: 0 }}
              transition={{ duration: 1.5, ease: [0.16, 1, 0.3, 1] }}
            >
              <div className="flex items-center justify-center gap-6 mb-12 opacity-40">
                <div className="h-px w-20 bg-foreground" />
                <span className="font-mono text-[10px] tracking-[1.5em] uppercase">
                  Core_Initialize
                </span>
                <div className="h-px w-20 bg-foreground" />
              </div>
              <h1 className="text-[clamp(2.5rem,15vw,9rem)] font-black tracking-tighter leading-[0.75] uppercase italic flex flex-col">
                <span>PRIME</span>
                <span className="text-transparent" style={{ WebkitTextStroke: '1px currentColor' }}>
                  STATIC
                </span>
              </h1>
              <p className="mt-16 max-w-lg mx-auto text-muted-foreground font-mono text-[10px] tracking-[0.4em] uppercase leading-relaxed">
                Premium Narrative Architecture
                <br />
                Built for high-frequency interaction.
              </p>
            </motion.div>
          </section>
          {SECTIONS.map((section) => (
            <ContentSection key={section.id} section={section} />
          ))}
          <footer className="min-h-full sm:h-screen flex flex-col items-center justify-center relative overflow-hidden border-t border-foreground/5">
            <motion.div
              whileInView={{ height: [0, 200] }}
              transition={{ duration: 1.5 }}
              className="w-px bg-gradient-to-b from-foreground via-foreground/20 to-transparent mb-16"
            />
            <h2 className="text-[12vw] font-black italic text-foreground/5 uppercase select-none">
              TERMINAL
            </h2>
            <div className="mt-20flex flex-wrap justify-center gap-12 md:gap-32 font-mono text-[9px] tracking-[0.8em] text-muted-foreground text-center uppercase">
              <div className="hover:text-foreground cursor-pointer transition-colors">
                Archive_Data
              </div>
              <div className="hover:text-foreground cursor-pointer transition-colors">
                Global_Sync
              </div>
              <div className="hover:text-foreground cursor-pointer transition-colors">
                Access_Core
              </div>
            </div>
            <div className="absolute bottom-10 left-0 w-full flex justify-between px-10 font-mono text-[7px] text-muted-foreground tracking-widest uppercase opacity-40">
              <span>Runtime: Stable_AX_9</span>
              <span>© VELOCITY UI OPS 2026</span>
              <span>Ref: End_Of_Line</span>
            </div>
          </footer>
        </main>
      </div>
    </div>
  )
}

Dependencies

  • framer-motion: latest

Props

Component property reference.

NameTypeDefaultDescription
childrenReactNodenullPage content to wrap with the custom scrollbar.
indicatorColorstring'var(--foreground)'Color of the scroll indicator.
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.