# Text Loop

> A seamless text marquee that flows along a curved SVG path — wave, circle, infinity, arch or line — with an optional crossing band, drag-to-fling and scroll boost.

A phrase repeats along an SVG curve and scrolls without a seam. Hover to ease it to a stop, turn on `draggable` to scrub or throw it (it spins on a circle), and set `scroll-boost` so page scrolling speeds it up or reverses it. `cross` adds a second band running the other way with a soft shadow where they overlap.

<demo-text-loop>



</demo-text-loop>

## Installation

```bash
npx shadcn-vue@latest add "https://nxui.geoql.in/r/text-loop.json"
```

## Usage

```vue
<script setup lang="ts">
  import TextLoop from '~/components/ui/text-loop/TextLoop.vue';
</script>

<template>
  <TextLoop
    text="React ✦ Bits"
    shape="line"
    ribbon-color="#ff5a36"
    color="#120f17"
    :speed="120"
    :ribbon-width="76"
    cross
    :cross-angle="14"
    draggable
  />
</template>
```

The repetition count is measured from the rendered text, so the loop always
fills the curve exactly; two copies of the run, offset by one path length,
make the wrap invisible.

## Props

<table>
<thead>
  <tr>
    <th>
      Prop
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Default
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        text
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        'Vue ✦ nxui'
      </code>
    </td>
    
    <td>
      The phrase repeated along the curve
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        shape
      </code>
    </td>
    
    <td>
      <code>
        'wave' | 'circle' | 'infinity' | 'arch' | 'line'
      </code>
    </td>
    
    <td>
      <code>
        'wave'
      </code>
    </td>
    
    <td>
      Built-in curve the text flows along
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        path
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      Custom SVG path data in a 1200×520 viewBox; overrides <code>
        shape
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        speed
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        90
      </code>
    </td>
    
    <td>
      Travel speed along the path, in units per second
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        direction
      </code>
    </td>
    
    <td>
      <code>
        'forward' | 'reverse'
      </code>
    </td>
    
    <td>
      <code>
        'forward'
      </code>
    </td>
    
    <td>
      Direction the text scrolls
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        separator
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        '✦'
      </code>
    </td>
    
    <td>
      Glyph placed between repetitions
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        curviness
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        90
      </code>
    </td>
    
    <td>
      Wave amplitude, or radius of the closed shapes
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        tilt
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0
      </code>
    </td>
    
    <td>
      Rotates the whole loop, in degrees
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        cross
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        false
      </code>
    </td>
    
    <td>
      Adds a second band crossing the first and running the other way
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        cross-color
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      Crossing band color; defaults to a pale tint of <code>
        ribbon-color
      </code>
      
       (text without a ribbon, dimmed)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        cross-angle
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        16
      </code>
    </td>
    
    <td>
      Angle between the two bands, centered on <code>
        tilt
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        draggable
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        false
      </code>
    </td>
    
    <td>
      Drag to scrub (or spin on a circle); a throw keeps momentum, then eases back
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        scroll-boost
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0
      </code>
    </td>
    
    <td>
      How strongly page scrolling pushes the loop; <code>
        0
      </code>
      
       turns it off
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        font-size
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        46
      </code>
    </td>
    
    <td>
      Font size of the looping text
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        font-weight
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        800
      </code>
    </td>
    
    <td>
      Font weight of the looping text
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        letter-spacing
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        2
      </code>
    </td>
    
    <td>
      Extra tracking between letters
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        uppercase
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        true
      </code>
    </td>
    
    <td>
      Renders the phrase in uppercase
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        outline
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        false
      </code>
    </td>
    
    <td>
      Draws the letters as outlines
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        outline-width
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        1.5
      </code>
    </td>
    
    <td>
      Stroke width of outlined letters
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        color
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      Text fill; white on the ribbon, <code>
        currentColor
      </code>
      
       without it
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ribbon
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        true
      </code>
    </td>
    
    <td>
      Draws a solid band behind the text
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ribbon-color
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        '#5227FF'
      </code>
    </td>
    
    <td>
      Color of the band
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ribbon-width
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        86
      </code>
    </td>
    
    <td>
      Thickness of the band
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        edge-fade
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0
      </code>
    </td>
    
    <td>
      Fades the loop toward the left and right edges, from <code>
        0
      </code>
      
       (hard) to <code>
        1
      </code>
      
       (wide)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        pause-on-hover
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        true
      </code>
    </td>
    
    <td>
      Eases the loop to a stop while hovered, and back up to speed on leave
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        class
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      Additional classes on the root element
    </td>
  </tr>
</tbody>
</table>

## Accessibility

- The SVG has `role="img"` and an `aria-label` set to `text`; the repeated runs are `aria-hidden`.
- With `prefers-reduced-motion: reduce` the loop does not auto-scroll and drags don't fling; dragging still scrubs.
- Animation pauses while the loop is off screen.
- Ported from [React Bits](https://reactbits.dev/text-animations/text-loop) by David Haz.
