Scroll & Viewport

Scroll Velocity

Text that drifts and accelerates with the speed and direction of your scroll.

Rows of text that move on their own and react to how fast you are scrolling. Each line scrolls continuously at a base velocity, and the speed and direction of your scroll are folded into that baseline: scroll down and the lines speed up, scroll up and they reverse. The whole thing is driven by a requestAnimationFrame loop, so the motion is smooth rather than stepped.

Alternate lines run in opposite directions, so a stack of texts reads as a stack of counter-moving bands.

Usage

Pass the phrases as texts. Every phrase becomes its own marquee row; even-numbered rows move one way and odd-numbered rows the other. velocity is the base speed, and damping/stiffness shape how the scroll is smoothed into that speed.

Scroll Velocity

Nuxt Motion Block  Nuxt Motion Block  Nuxt Motion Block  Nuxt Motion Block  Nuxt Motion Block  Nuxt Motion Block 
Scroll Down  Scroll Down  Scroll Down  Scroll Down  Scroll Down  Scroll Down 
<template>
  <div class="p-6 bg-elevated rounded-lg">
    <h3 class="text-lg font-bold mb-4 text-highlighted">
      Scroll Velocity
    </h3>
    <MScrollVelocity
      :texts="['Nuxt Motion Block', 'Scroll Down']"
      :velocity="100"
      :damping="50"
      :stiffness="400"
      :num-copies="6"
    />
  </div>
</template>

Examples

Multiple phrases

Every entry in texts is a separate row, the odd ones travelling against the even ones.

Scroll Velocity

Dynamic text effects that respond to scroll speed for engaging parallax experiences

Fast Scroll  Fast Scroll  Fast Scroll  Fast Scroll  Fast Scroll  Fast Scroll 
Medium Speed  Medium Speed  Medium Speed  Medium Speed  Medium Speed  Medium Speed 
Slow Motion  Slow Motion  Slow Motion  Slow Motion  Slow Motion  Slow Motion 
<template>
  <div class="p-6 bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-gray-800 dark:to-gray-900 rounded-xl max-w-2xl mx-auto">
    <div class="text-center mb-6">
      <h3 class="text-2xl font-bold mb-3 text-highlighted">
        Scroll Velocity
      </h3>
      <p class="text-muted">
        Dynamic text effects that respond to scroll speed for engaging parallax experiences
      </p>
    </div>

    <div class="bg-default rounded-lg p-6 shadow">
      <MScrollVelocity
        :texts="['Fast Scroll', 'Medium Speed', 'Slow Motion']"
        :velocity="75"
        class="text-3xl font-bold text-center"
      />
    </div>

    <div class="mt-6 flex justify-center">
      <UButton
        color="primary"
        variant="solid"
        size="sm"
      >
        <UIcon
          name="i-lucide-mouse"
          class="w-4 h-4 mr-2"
        />
        Try Scrolling
      </UButton>
    </div>
  </div>
</template>

Velocity mapping and copies

velocityMapping remaps the smoothed scroll velocity into a speed multiplier, so you can make the effect more or less sensitive. numCopies sets how many times each phrase is repeated inside its row — raise it for very wide screens so the line never runs out of text.

Velocity mapping  Velocity mapping  Velocity mapping  Velocity mapping 
<template>
  <div class="space-y-4">
    <MScrollVelocity
      :texts="['Velocity mapping']"
      :velocity="120"
      :num-copies="4"
      :velocity-mapping="{ input: [0, 500], output: [0, 8] }"
      :damping="40"
      :stiffness="500"
      scroller-class="text-3xl font-semibold"
    />
  </div>
</template>

Styling hooks

parallaxClass, scrollerClass, parallaxStyle and scrollerStyle reach the inner wrapper and the moving row, so you can restyle either without overriding the theme globally.

Styled with the theme hooks  Styled with the theme hooks  Styled with the theme hooks  Styled with the theme hooks  Styled with the theme hooks  Styled with the theme hooks 
<template>
  <div class="space-y-6">
    <MScrollVelocity
      :texts="['Styled with the theme hooks']"
      :velocity="80"
      class="rounded-xl bg-default"
      parallax-class="py-4"
      scroller-class="text-3xl tracking-tight"
      :parallax-style="{ background: 'linear-gradient(90deg, transparent, var(--ui-bg-muted), transparent)' }"
      :scroller-style="{ opacity: 0.85 }"
    />
  </div>
</template>

Customize

Adjust texts, velocity, damping, stiffness and numCopies live in the playground below.

Nuxt Motion Block  Nuxt Motion Block  Nuxt Motion Block  Nuxt Motion Block  Nuxt Motion Block  Nuxt Motion Block 
Scroll to move  Scroll to move  Scroll to move  Scroll to move  Scroll to move  Scroll to move 

Customize

Texts2
Nuxt Motion Block
Scroll to move
Velocity100
Damping50
Stiffness400
Copies6

Props

Prop Type Default Description
textsstring[][]The phrases to display. Each becomes its own row; odd rows travel against even rows.
velocitynumber100Base velocity of the animation, in pixels per second. Rows alternate the sign of this value.
dampingnumber50Damping factor for the internal velocity spring. Higher values settle the speed more quickly and reduce wobble.
stiffnessnumber400Stiffness factor for the internal velocity spring. Higher values make the row respond to changes in scroll speed more aggressively.
numCopiesnumber6How many copies of each phrase are rendered inside a row. Raise it for wide viewports so the row never shows a gap.
velocityMapping{ input: [number, number], output: [number, number] }{ input: [0, 1000], output: [0, 5] }Remaps the smoothed scroll velocity into a speed multiplier. The input range is the raw velocity and output the multiplier applied on top of velocity; with the default, a fast scroll can multiply the movement up to 5×.
scrollContainerRefany-Declared for parity with the React primitive and forwarded to each row, but the current implementation reads window.scrollY directly, so the velocity always reacts to page scroll regardless of this value. Treat it as inert.
parallaxClassstring''Classes merged onto the inner parallax wrapper (relative overflow-hidden from the theme), the element that clips each row.
scrollerClassstring''Classes merged onto the moving row. Override this to change the typography the theme applies.
parallaxStyleobject{}Inline styles for the parallax wrapper.
scrollerStyleobject{}Inline styles for the moving row, merged after the translateX transform.
classstring-Merged onto the root <section>.

UI

This component's theme exposes the slots below. Override any of them per instance with the ui prop, or app-wide under ui.scrollVelocity in app.config.ts.

Prop Type Default Description
uiobject-Per-slot classes merged over the theme defaults.
Slot Props Description
root-
parallax-relative overflow-hidden
scroller-flex whitespace-nowrap text-center font-sans text-4xl font-bold tracking-[-0.02em] drop-shadow md:text-[5rem] md:leading-[5rem]
text-flex-shrink-0

Accessibility

The rows are plain, animated spans inside a <section>. There are no ARIA attributes, roles or live regions, and because the text is duplicated numCopies times, the same phrase appears many times in the accessibility tree. A screen reader reads every copy.

There is no prefers-reduced-motion handling: the requestAnimationFrame loop runs for every user, and because a row moves on its own even without scrolling, a motion-sensitive user has no way to stop it from props alone. Keep these bands decorative and out of the primary reading path, and if the content is meaningful, provide it as static text elsewhere.

Performance

  • Each row runs a useRafFn loop that updates a single baseX counter and writes a translateX transform. The work per frame is arithmetic, not measurement.
  • The only measurement is the width of the first copy (offsetWidth), read once on mount and on window resize — never per frame.
  • useRafFn pauses automatically when the tab is hidden, so a backgrounded page costs nothing.
  • The rows animate transform only, so the compositor can run them off the main thread and no frame triggers layout.
  • The velocity spring is a hand-rolled simulation (stiffness / 1000 and damping / 1000 per frame) rather than a Motion spring, which keeps the whole component dependency-free at runtime.

How the velocity works

Each frame the row reads window.scrollY, subtracts the previous value and multiplies by 60 to get a per-second figure. That raw velocity is smoothed by the spring to produce smoothVelocity, which velocityMapping then rescales into a multiplier. The multiplier is added to the base movement, and its sign decides the direction the row travels:

moveBy  = direction × velocity × (delta / 1000)
moveBy += direction × moveBy × velocityFactor

So a positive velocityFactor speeds the row up in its current direction, and a negative one flips directionFactor and sends it the other way. velocity is the floor — the row keeps drifting at that speed even when the page is still.

Because the position is wrapped between -copyWidth and 0, the row can scroll forever without its baseX value growing unbounded.

Copyright © 2026