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 π€
- Host: GitHub
- URL: https://github.com/maitrungduc1410/react-native-textflow
- Owner: maitrungduc1410
- License: mit
- Created: 2026-05-12T11:08:31.000Z (about 2 months ago)
- Default Branch: master
- Last Pushed: 2026-05-29T02:55:48.000Z (about 1 month ago)
- Last Synced: 2026-05-29T04:23:33.245Z (about 1 month ago)
- Topics: adaptive-text, layout, react-native, reflow
- Language: Kotlin
- Homepage:
- Size: 1.46 MB
- Stars: 3
- Watchers: 0
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
- Agents: AGENTS.md
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)