An open API service indexing awesome lists of open source software.

https://github.com/maitrungduc1410/react-native-textflow

Native fluid text reflow for React Native πŸ”€
https://github.com/maitrungduc1410/react-native-textflow

adaptive-text layout react-native reflow

Last synced: 27 days ago
JSON representation

Native fluid text reflow for React Native πŸ”€

Awesome Lists containing this project

README

          

# react-native-textflow

> Native SwiftUI / Jetpack Compose-style **fluid text reflow** for React Native.
> Words spring between lines as their container resizes β€” driven by the OS's own
> motion physics, not a JavaScript animation loop.

```tsx

When there is no more space for some words, those words smoothly fly to the next line.

```

## Demo

| Android | iOS |
| --- | --- |
| | |

## Why this exists

In SwiftUI and Jetpack Compose, you can render a piece of text whose words
*animate to their new positions* whenever the available width changes. It's
the polish you see in modern Apple/Google apps β€” and it's missing from React
Native, because `` is a single, indivisible glyph run.

`react-native-textflow` ships that exact effect as a single component.
The JavaScript layer is deliberately thin β€” it forwards a string + style +
animation config through Fabric codegen and lets the OS own everything else.
That's why the look is indistinguishable from a native SwiftUI / Material 3
app.

Works with vanilla **React Native CLI** *and* with **Expo** (via Dev Client +
Prebuild β€” Expo Go is not supported because the library ships custom native
code). See [Installation β†’ Expo](#expo) for the one-line `app.json` snippet.

## How it works

| | iOS | Android |
| --- | --- | --- |
| Host view | `UIView` wrapping a `UIHostingController` | `FrameLayout` wrapping a `ComposeView` |
| Layout engine | SwiftUI custom `Layout` protocol (`AdaptiveTextFlowLayout`) | Jetpack Compose custom `Layout` (`AdaptiveFlowLayout`) inside `LookaheadScope` |
| Per-word motion | `.animation(_, value:)` with `interpolatingSpring` | `Modifier.animateTokenPlacement` (placement-only) |
| Yoga measurer | CoreText `boundingRectWithSize` via Obj-C++ | Compose `TextMeasurer` via JNI to Kotlin |
| Tokenizer | `String.components(separatedBy:)` / grapheme cluster iteration | `BreakIterator` |
| Min OS | iOS 16 | Android 7 (API 24), Compose BOM 2026.05+ |

Both platforms share a custom Fabric C++ shadow node (`AdaptiveTextShadowNode`)
that owns a Yoga measure callback. The C++ side delegates measurement to a
per-platform implementation that wraps text using the **same wrap algorithm**
as the renderer β€” byte-for-byte β€” so Yoga's predicted height and the
renderer's actual layout always agree. Without that lockstep, Fabric's strict
EXACTLY measure spec on `AdaptiveTextView` would clip the bottom line whenever
the two sides disagreed by even one wrap decision.

## Installation

```sh
npm install react-native-textflow
# or
yarn add react-native-textflow
```

Requires:

- React Native **0.85+** (Fabric / New Architecture)
- iOS **16.0+**
- Android **API 24+**, Jetpack Compose **BOM 2026.05+** (Compose 1.10+)

### Bare React Native (CLI)

After installing the package, link the iOS pods:

```sh
cd ios && pod install
```

Android is autolinked through `react-native.config.js` β€” Gradle picks up the
custom CMakeLists and the Compose dependencies automatically on the next
build.

### Expo

`react-native-textflow` works with any Expo project that uses **Expo Dev
Client** + **Prebuild** (a.k.a. Continuous Native Generation). It does **not**
work with Expo Go β€” Expo Go ships a fixed set of native modules and cannot
load custom Fabric components.

If your project is still on Expo Go, run `npx expo install expo-dev-client`
first to switch to a custom dev build; the rest of your managed-Expo
ergonomics (EAS Build, OTA updates, config plugins, ...) are unaffected.

Install the package and a small build-properties helper:

```sh
npx expo install react-native-textflow expo-build-properties
```

Then add this to your `app.json` / `app.config.js` so iOS pods resolve at
the 16.0 deployment target the library needs:

```json
{
"expo": {
"plugins": [
[
"expo-build-properties",
{
"ios": {
"deploymentTarget": "16.0"
}
}
]
]
}
}
```

Finally regenerate the native projects and build:

```sh
npx expo prebuild --clean
npx expo run:ios # or: npx expo run:android
# or, for cloud builds: eas build --profile development
```

That's it β€” autolinking + the podspec + `react-native.config.js` already do
the rest. No additional Expo config plugin from this library is required.

## Usage

```tsx
import { AdaptiveText } from 'react-native-textflow';

Hello, adaptive world.

```

### With a custom animation

```tsx

Tweak damping and stiffness β€” feel the curve.

```

### Inside any RN container

`` plays nicely with ``, ``, ``,
``, and `` out of the box. See the example app for
eleven ready-made demos.

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` / `text` | `string` | `''` | The text to render. Children win over `text`. |
| `style` | `StyleProp` | β€” | RN style. Text-style keys (`fontSize`, `color`, `fontFamily`, `fontWeight`, `fontStyle`, `letterSpacing`, `lineHeight`, `textAlign`) are forwarded to the native flow layout; everything else (size, padding, background, etc.) styles the wrapper. |
| `splitBy` | `'word' \| 'grapheme'` | `'word'` | Tokenization granularity. Use `'grapheme'` for CJK or per-character flow effects. |
| `animation` | `AdaptiveAnimationConfig` | spring (18, 220, 1) | Motion curve used when the layout reflows. |
| `wordSpacing` | `number` | 6 | Horizontal spacing between tokens, in points. |
| `lineSpacing` | `number` | 4 | Vertical spacing between lines, in points. |
| `textColor` | `ColorValue` | text style `color` | Override colour applied only to the text glyphs. |
| ...rest | `ViewProps` | β€” | Standard view props (`testID`, `accessibility*`, layout, etc.). |

### Animation config

```ts
type AdaptiveAnimationConfig =
| { type: 'spring'; damping?: number; stiffness?: number; mass?: number }
| { type: 'timing'; duration?: number; easing?: 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' }
| { type: 'none' };
```

### `textAlign` and RTL

`'start'` / `'end'` flip with the active layout direction (RTL β†’ reversed).
`'left'` / `'right'` are absolute. Toggling `textAlign` smoothly animates
every token to its new line position on both platforms.

## Accessibility

- The container exposes a single `accessibilityLabel` defaulting to the
rendered string. Per-word native views are hidden from the accessibility
tree, so VoiceOver and TalkBack read the phrase as one logical unit.
- Both iOS and Android respect Dynamic Type / Font Scale; pass an absolute
`fontSize` and the OS will scale it relative to `.body` text style.
- RTL strings (Arabic, Hebrew, etc.) flow correctly when `textAlign` is
`'start'` or `'end'`. Mirror behaviour follows `I18nManager`.

## Performance notes

- Tokenization runs once per `text` / `splitBy` change and is memoised.
- Per-token widths are cached process-wide keyed by `(font config, token text)`,
so a drag-resize that pushes a new width per touch frame collapses N
`TextMeasurer.measure(...)` / `boundingRectWithSize:` calls to N hash-map
lookups after the first measurement of each token.
- Per-token re-renders are skipped via Compose's smart skipping and SwiftUI's
view-identity tracking β€” only tokens whose props actually change are
recomposed.
- The library does **not** depend on Reanimated. All motion is OS-native.
- Animation is **placement-only**: each token's intrinsic size never
interpolates, so adjacent tokens can never overdraw each other's trailing
glyphs (the "missing trailing letter" bug class).

## Example app

The repository ships a comprehensive example app showing ``
inside every common React Native container:

```sh
yarn install
yarn example ios # or: yarn example android
```

Screens:

| # | Screen | What it demonstrates |
| --- | --- | --- |
| 1 | Resizable container | Drag-handle resize; words spring per touch frame. |
| 2 | Animated width | `Animated.timing` driving the wrapper width. Reflow follows. |
| 3 | Inside a `` | Reflow inside RN's stock modal. |
| 4 | Inside a `` | Long paragraphs + sticky header; verifies measurement under prop toggles. |
| 5 | Inside a `` | Each row is ``. Verifies measurement under virtualization. |
| 6 | Dynamic content | Add / remove / shuffle words; survivors slide, new ones fade in. |
| 7 | Style morphing | Sliders for fontSize / letterSpacing / lineHeight + chips for weight / italic / color. |
| 8 | RTL | Arabic & Hebrew samples Γ— full `textAlign` matrix (animates on both platforms). |
| 9 | Animation config | Live-tune spring vs timing vs none. |
| 10 | Showcase card | Avatar + adaptive bio that grows on tap (the README screenshot). |
| 11 | Dark / Light theme | Card chrome interpolates, AdaptiveText snaps to new color (color is a non-layout prop). |

## Testing

The library ships a **six-layer test suite** that guards everything from
the JS faΓ§ade down to the user-observable Compose / SwiftUI motion. The
quick commands:

```sh
yarn lint && yarn typecheck # static checks
yarn test # Jest (JS unit)
cd example/android && ./gradlew :react-native-textflow:testDebugUnitTest # Android JVM unit
yarn maestro:android # E2E (Android)
yarn maestro:ios # E2E (iOS)
```

iOS XCTest runs via Xcode (open `example/ios/AdaptiveTextExample.xcworkspace`
and pick the `AdaptiveTextExampleTests` scheme) or via `xcodebuild`; the
Android Compose UI test runs via `connectedDebugAndroidTest` against a
booted emulator. CI runs every layer on `master` β€” cheap layers on every
PR, simulator/emulator-bound layers gated by `paths-filter`.

Full instructions including prerequisites, what each layer covers, and a
Maestro debugging recipe live in [TEST_GUIDE.md](TEST_GUIDE.md).

## Architecture in one diagram

```
JS:
β”‚
β–Ό Fabric codegen
C++: AdaptiveTextShadowNode (LeafYogaNode + MeasurableYogaNode)
β”‚ β”‚
β”‚ └─ measureContent() β†’ adaptive_text::measure(props, ctx, constraints)
β”‚ β”‚
β”‚ β”œβ”€ iOS: AdaptiveTextMeasurer.mm (CoreText)
β”‚ └─ Android: AdaptiveTextMeasurer.cpp
β”‚ └─ JNI β†’ AdaptiveTextNativeMeasurer.kt
β”‚ └─ Compose TextMeasurer
β”‚
β–Ό Fabric mount
Native view: AdaptiveTextView (UIView / FrameLayout)
β”‚
β–Ό
Renderer:
β€’ iOS: UIHostingController β†’ AdaptiveTextContent (SwiftUI)
└─ AdaptiveTextFlowLayout (custom Layout)
β€’ Android: ComposeView β†’ AdaptiveTextFlow (Compose)
└─ AdaptiveFlowLayout (custom Layout) inside LookaheadScope
└─ per-token Text with Modifier.animateTokenPlacement
```

Two invariants keep the system honest:

1. **Measurer ↔ renderer wrap parity.** The Yoga measurer and the renderer
use the same shaper (Compose `TextMeasurer` / `boundingRectWithSize:`)
and the same wrap algorithm. A 6 dp horizontal safety margin further
compensates for subpixel disagreement between Compose's
`ParagraphIntrinsics` and `MultiParagraphIntrinsics` paths.
2. **`ComposeView.layoutParams.height = MATCH_PARENT`.** Pins the
ComposeView's outer Android size to whatever Yoga gave AdaptiveTextView,
so a one-frame-stale Compose tree can never report a smaller size that
leaves recomposed content clipped at the bottom. (See the long-form
comment in `AdaptiveTextView.kt` for the trace.)

## Roadmap

Deliberately out of scope for v1, planned for future versions:

- Per-word `onPress` / `onLongPress`
- Magazine-style exclusion paths (text wrapping around shapes)
- Shared-element transitions across screens
- Web platform fallback (Reanimated `LinearTransition`)
- Smoothly animated text colors (via `Animated.createAnimatedComponent` or Reanimated `useAnimatedProps`)

## Contributing

- [Development workflow](CONTRIBUTING.md#development-workflow)
- [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
- [Code of conduct](CODE_OF_CONDUCT.md)

See [AGENTS.md](AGENTS.md) for an architecture map and the design
decisions behind every non-obvious choice in this codebase.

## License

MIT

---

Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)