Scrolling Number

A wheel of numbers that spins to its value and settles under a highlight.

<script setup lang="ts">
import type { ScrollingNumberDirection } from '@animate-ui/vue/primitives/texts/scrolling-number'
import { RefreshIcon } from '@hugeicons/core-free-icons'
import { HugeiconsIcon } from '@hugeicons/vue'
import {
  ScrollingNumber,
  ScrollingNumberContainer,
  ScrollingNumberHighlight,
  ScrollingNumberItems,
} from '@animate-ui/vue/primitives/texts/scrolling-number'
import { cn } from 'cn'
import { computed, ref } from 'vue'
import { Button } from '@/components/ui/button'

const props = withDefaults(
  defineProps<{ direction?: ScrollingNumberDirection, delay?: number | string }>(),
  { direction: 'btt', delay: 1000 },
)

const run = ref(0)
const isVertical = computed(() => props.direction === 'btt' || props.direction === 'ttb')
</script>

<template>
  <div class="flex flex-col items-center gap-6">
    <ScrollingNumberContainer
      :key="`${run}-${direction}-${delay}`"
      :direction="direction"
      :number="1000"
      :step="100"
      :items-size="isVertical ? 40 : 75"
      :class="cn('text-lg font-medium tabular-nums', isVertical ? 'w-18' : 'h-10')"
    >
      <ScrollingNumber :delay="Number(delay)">
        <ScrollingNumberItems class="text-muted-foreground flex items-center justify-center" />
      </ScrollingNumber>
      <ScrollingNumberHighlight class="bg-muted size-full rounded-md" />
    </ScrollingNumberContainer>
    <Button variant="outline" size="sm" @click="run++">
      <HugeiconsIcon :icon="RefreshIcon" :stroke-width="2" data-icon="inline-start" />
      Replay
    </Button>
  </div>
</template>

Installation

npm install @animate-ui/vue motion-v
pnpm add @animate-ui/vue motion-v
yarn add @animate-ui/vue motion-v
bun add @animate-ui/vue motion-v

Usage

<script setup lang="ts">
import {
  ScrollingNumber,
  ScrollingNumberContainer,
  ScrollingNumberHighlight,
  ScrollingNumberItems,
} from '@animate-ui/vue/primitives/texts/scrolling-number';
</script>

<template>
  <ScrollingNumberContainer :number="1000" :step="100" :items-size="40">
    <ScrollingNumber>
      <ScrollingNumberItems />
    </ScrollingNumber>
    <ScrollingNumberHighlight />
  </ScrollingNumberContainer>
</template>

Animation

  • After delay, the strip spins through every step up to number on a spring (stiffness: 90, damping: 30): fast through the range, then it settles without overshoot.
  • A new number springs on from where the strip is.
  • The strip is written by a motion value, without a Vue render per frame. @number-change reports the value under the highlight as it passes.
  • <AnimateUIProvider :tempo> paces the default spring.
  • Under reduced motion, the wheel lands on the number right away.
  • The strip is hidden from screen readers, which read the number once from the container.

API Reference

ScrollingNumberContainer

PropTypeDefault
number
number-
step
number-
items-size?
number30
side-items-count?
number2
direction?
'btt' | 'ttb' | 'ltr' | 'rtl''btt'
@number-change?
(value: number) => void-
in-view?
booleanfalse
in-view-margin?
string'0px'
in-view-once?
booleantrue
...props?
HTMLAttributes-

ScrollingNumber

PropTypeDefault
delay?
number0
@completed?
() => void-
transition?
Transition{ stiffness: 90, damping: 30 }
...props?
HTMLMotionProps<'div'>-

ScrollingNumberItems

PropTypeDefault
...props?
HTMLAttributes-

ScrollingNumberHighlight

PropTypeDefault
...props?
HTMLAttributes-

useScrollingNumber

Returns the container's state inside it (number, step, range, direction, isInView…).

Built by Skyleen. The source code is available on GitHub.

Last updated: 10/23/2026