# React Native Marquee

[![npm version](https://img.shields.io/npm/v/react-native-marquee-animation.svg)](https://www.npmjs.com/package/react-native-marquee-animation)
[![npm downloads](https://img.shields.io/npm/dm/react-native-marquee-animation.svg)](https://www.npmjs.com/package/react-native-marquee-animation)
[![CI](https://github.com/invivek26/react-native-marquee-animation/actions/workflows/ci.yml/badge.svg)](https://github.com/invivek26/react-native-marquee-animation/actions/workflows/ci.yml)
[![license](https://img.shields.io/npm/l/react-native-marquee-animation.svg)](LICENSE)

A production-grade generic marquee for React Native's New Architecture. Render
ordinary React Native text, images, icons, badges, charts, and composed views
once; native iOS and Android code repeats the visual strip and owns all motion.

There is no per-frame JavaScript, duplicated React state, or list
virtualization.

Explore the interactive web interpretation at [viveks.app/open-source](https://viveks.app/open-source).

## Demo

https://github.com/user-attachments/assets/9f4e1655-0f2c-468f-b0e4-25af40de5631

## Compatibility

| Requirement  | Supported                                      |
| ------------ | ---------------------------------------------- |
| React Native | 0.85.x to 0.88.x                               |
| React        | 19.2.x and 19.3.x                              |
| Expo         | SDK 56 to 58 development and production builds |
| iOS          | 17 and newer                                   |
| Android      | API 24 and newer                               |
| Architecture | New Architecture (Fabric) only                 |
| Expo Go      | No                                             |

## Installation

```sh
bun add react-native-marquee-animation
```

Expo applications must regenerate and rebuild the native project:

```sh
bunx expo prebuild --clean
bunx expo run:ios
# or: bunx expo run:android
```

## Quick start

```tsx
import { Image, StyleSheet, Text, View } from 'react-native';
import { Marquee } from 'react-native-marquee-animation';

export const NewsStrip = () => (
  <Marquee
    accessibilityLabel="Latest headlines"
    contentContainerStyle={styles.row}
    spacing={32}
    speed={25}
    style={styles.marquee}
  >
    <View style={styles.card}>
      <Image source={require('./logo.png')} style={styles.image} />
      <Text>Images, text, badges, icons, and custom views</Text>
    </View>
    <View style={styles.card}>
      <Text>React state is mounted exactly once</Text>
    </View>
  </Marquee>
);

const styles = StyleSheet.create({
  card: { alignItems: 'center', flexDirection: 'row', gap: 8 },
  image: { height: 24, width: 24 },
  marquee: { height: 48 },
  row: { alignItems: 'center', gap: 24 },
});
```

The package wraps `children` in one non-collapsible horizontal content view,
measures that view only when layout changes, and passes one width update to
native code. Native motion remains uninterrupted when child pixels change
without changing width.

## Gesture physics

Dragging, release velocity, decay, and the transition back to automatic motion
are native-owned:

```tsx
<Marquee
  accessibilityLabel="Live activity"
  interaction={{
    maxFlingVelocity: 3200,
    deceleration: 0.9985,
    pauseOnPress: true,
    resumeDelay: 0,
  }}
>
  {content}
</Marquee>
```

After a fling decelerates, both platforms blend velocity into the configured
automatic direction over 350 ms instead of stopping or snapping.
`pauseOnPress: true` pauses immediately on touch-down. `resumeDelay` controls
when automatic motion resumes after release. On iOS, drag, fling, and the blend
back to automatic motion are synchronized to the active screen's refresh rate.

## Content contract

Supported content includes normal layer/display-list-backed React Native views:

- `Text`, `View`, `Image`, and `expo-image`;
- SVG, icons, badges, gradients, and composed layouts;
- custom charts that render into the ordinary React Native view hierarchy;
- child state, asynchronous image loading, and dynamic value changes.

Marquee children receive no touches. The host owns horizontal pan, hold, and
fling gestures, and repeated pixels are not additional React views. Nested
`Pressable`, `TextInput`, map, video, camera, `SurfaceView`, `TextureView`,
Metal, and other external rendering surfaces are outside the contract.

To make items tappable, pass `onContentPress`. Native code maps a tap on any
visual copy back to `x` in the content container's own coordinates,
`[0, contentWidth)`, so the same pixel reports the same `x` on both platforms
regardless of copy, direction, phase, drag offset, or static alignment. Taps in
the `spacing` gap or outside static content report nothing. Hit-test your own
items with their `onLayout` ranges:

```tsx
const ranges = useRef(new Map<string, { start: number; end: number }>());

<Marquee
  accessibilityLabel="Watchlist"
  onContentPress={({ x }) => {
    for (const [id, range] of ranges.current) {
      if (x >= range.start && x < range.end) return openChart(id);
    }
  }}
>
  {stocks.map((stock) => (
    <StockPill
      key={stock.id}
      onLayout={({ nativeEvent: { layout } }) =>
        ranges.current.set(stock.id, {
          start: layout.x,
          end: layout.x + layout.width,
        })
      }
      stock={stock}
    />
  ))}
</Marquee>;
```

A tap is a touch down and up that moves less than the platform touch slop
without starting a drag; drags, flings, cancellations, and parent scrolls never
fire. The position is sampled at touch-down, so moving content
(`pauseOnPress: false`) reports what was under the finger when it landed.
Tapping does not interrupt automatic motion. Without `onContentPress`, native
touch handling is unchanged and touches outside the drag area still reach
native parents. The strip remains one accessible
element; provide an accessible alternative if individual items matter to
assistive technology users.

## Main props

| Prop                    | Default  | Meaning                                      |
| ----------------------- | -------- | -------------------------------------------- |
| `accessibilityLabel`    | required | One semantic description of the strip        |
| `speed`                 | `25`     | Logical pixels per second                    |
| `direction`             | `left`   | Automatic motion direction                   |
| `spacing`               | `0`      | Empty distance between visual copies         |
| `active`                | `true`   | Application-level pause control              |
| `shortContentMode`      | `static` | Static content or repeated short content     |
| `contentAlignment`      | `start`  | Alignment while short content is stationary  |
| `contentContainerStyle` | —        | Style for the one horizontal content wrapper |
| `onContentPress`        | —        | Tap position `{ x }` in content coordinates  |
| `reduceMotion`          | `system` | `system`, `always`, or `never`               |

## Native architecture

- iOS mounts one Fabric child container as the only subview of a
  `CAReplicatorLayer`-backed Swift view. Core Animation translates it without a
  display callback during automatic motion.
- Android mounts one Fabric child container in a custom `ViewGroup` and replays
  the existing hardware display lists at repeated canvas offsets. A monotonic
  `Choreographer` clock changes only the translation phase. Missed callbacks
  advance by at most roughly one current display interval instead of catching
  up in one visible jump; API 35+ requests a high refresh rate only while active.
- Same-width visual updates do not restart motion. Width changes preserve the
  physical offset and velocity while native code recomputes the loop period.
- Content that fits, `active={false}`, detached views, backgrounded apps, and
  Reduced Motion schedule no continuous frame work. Window-attached views use
  `active` as the authoritative application-level visibility signal.
- Only the host is accessible; visually repeated descendants are hidden from
  VoiceOver and TalkBack.

See [architecture](docs/architecture.md), [accessibility](docs/accessibility.md),
[testing](docs/testing.md), and [performance](docs/performance.md).

## Fabric and Codegen

`src/RNMarqueeViewNativeComponent.ts` is the source of truth. The native
component is `RNMarqueeView`, the Codegen library is `RNMarqueeViewSpec`, and
the iOS pod/module is `RNMarquee`. Generated bindings are consumer-owned and
are not published.

## Development

The repository uses Bun, React Native Builder Bob, an Expo SDK 56 consumer,
Swift and Kotlin tests, Release builds, packed-tarball validation, and Maestro.
Start with [CONTRIBUTING.md](CONTRIBUTING.md).

### Example experience

The Expo example follows the same showcase-first product system as the sibling
number-animation package. Release builds open a self-running green-and-cream
showcase with product, music, event, finance, image, badge, a Bhagavad Gita
verse in Kannada script, and gesture examples. Development builds expose an
`Open reliability lab` button; benchmark deep links open that lab directly in
either build.

Use `bun run capture:ios` or `bun run capture:android` from `example/` for clean
Release presentation captures. See [example/CAPTURE.md](example/CAPTURE.md).

## License

[MIT](LICENSE)

Maintained by [Vivek Indlebele Narasimha Prasad](https://viveks.app).
