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.
marquee-demo.mp4
| 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 |
bun add react-native-marquee-animationExpo applications must regenerate and rebuild the native project:
bunx expo prebuild --clean
bunx expo run:ios
# or: bunx expo run:androidimport { 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.
Dragging, release velocity, decay, and the transition back to automatic motion are native-owned:
<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.
Supported content includes normal layer/display-list-backed React Native views:
Text,View,Image, andexpo-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:
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.
| 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 |
- 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
ViewGroupand replays the existing hardware display lists at repeated canvas offsets. A monotonicChoreographerclock 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 useactiveas the authoritative application-level visibility signal. - Only the host is accessible; visually repeated descendants are hidden from VoiceOver and TalkBack.
See architecture, accessibility, testing, and performance.
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.
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.
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.
Maintained by Vivek Indlebele Narasimha Prasad.