{"id":50746584,"url":"https://github.com/maitrungduc1410/react-native-textflow","last_synced_at":"2026-06-10T21:32:34.324Z","repository":{"id":357355622,"uuid":"1236559112","full_name":"maitrungduc1410/react-native-textflow","owner":"maitrungduc1410","description":"Native fluid text reflow for React Native 🔤","archived":false,"fork":false,"pushed_at":"2026-05-29T02:55:48.000Z","size":1531,"stargazers_count":3,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-05-29T04:23:33.245Z","etag":null,"topics":["adaptive-text","layout","react-native","reflow"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/maitrungduc1410.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-05-12T11:08:31.000Z","updated_at":"2026-05-29T02:55:51.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/maitrungduc1410/react-native-textflow","commit_stats":null,"previous_names":["maitrungduc1410/react-native-textflow"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/maitrungduc1410/react-native-textflow","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maitrungduc1410%2Freact-native-textflow","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maitrungduc1410%2Freact-native-textflow/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maitrungduc1410%2Freact-native-textflow/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maitrungduc1410%2Freact-native-textflow/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/maitrungduc1410","download_url":"https://codeload.github.com/maitrungduc1410/react-native-textflow/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maitrungduc1410%2Freact-native-textflow/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34172196,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-10T02:00:07.152Z","response_time":89,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["adaptive-text","layout","react-native","reflow"],"created_at":"2026-06-10T21:32:34.174Z","updated_at":"2026-06-10T21:32:34.306Z","avatar_url":"https://github.com/maitrungduc1410.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# react-native-textflow\n\n\u003e Native SwiftUI / Jetpack Compose-style **fluid text reflow** for React Native.\n\u003e Words spring between lines as their container resizes — driven by the OS's own\n\u003e motion physics, not a JavaScript animation loop.\n\n```tsx\n\u003cAdaptiveText\n  style={{ fontSize: 22, fontWeight: '600', color: '#0e1116' }}\n  animation={{ type: 'spring', damping: 18, stiffness: 220 }}\n\u003e\n  When there is no more space for some words, those words smoothly fly to the next line.\n\u003c/AdaptiveText\u003e\n```\n\n## Demo\n\n| Android | iOS |\n| --- | --- |\n| \u003cvideo src=\"https://github.com/user-attachments/assets/e880e96e-1c53-4d9b-8f26-fe85118b3938\" controls loop muted\u003e\u003c/video\u003e | \u003cvideo src=\"https://github.com/user-attachments/assets/650e1eb7-f772-4ccf-a95f-2f923dd30fe7\" controls loop muted\u003e\u003c/video\u003e |\n\n## Why this exists\n\nIn SwiftUI and Jetpack Compose, you can render a piece of text whose words\n*animate to their new positions* whenever the available width changes. It's\nthe polish you see in modern Apple/Google apps — and it's missing from React\nNative, because `\u003cText\u003e` is a single, indivisible glyph run.\n\n`react-native-textflow` ships that exact effect as a single component.\nThe JavaScript layer is deliberately thin — it forwards a string + style +\nanimation config through Fabric codegen and lets the OS own everything else.\nThat's why the look is indistinguishable from a native SwiftUI / Material 3\napp.\n\nWorks with vanilla **React Native CLI** *and* with **Expo** (via Dev Client +\nPrebuild — Expo Go is not supported because the library ships custom native\ncode). See [Installation → Expo](#expo) for the one-line `app.json` snippet.\n\n## How it works\n\n| | iOS | Android |\n| --- | --- | --- |\n| Host view | `UIView` wrapping a `UIHostingController` | `FrameLayout` wrapping a `ComposeView` |\n| Layout engine | SwiftUI custom `Layout` protocol (`AdaptiveTextFlowLayout`) | Jetpack Compose custom `Layout` (`AdaptiveFlowLayout`) inside `LookaheadScope` |\n| Per-word motion | `.animation(_, value:)` with `interpolatingSpring` | `Modifier.animateTokenPlacement` (placement-only) |\n| Yoga measurer | CoreText `boundingRectWithSize` via Obj-C++ | Compose `TextMeasurer` via JNI to Kotlin |\n| Tokenizer | `String.components(separatedBy:)` / grapheme cluster iteration | `BreakIterator` |\n| Min OS | iOS 16 | Android 7 (API 24), Compose BOM 2026.05+ |\n\nBoth platforms share a custom Fabric C++ shadow node (`AdaptiveTextShadowNode`)\nthat owns a Yoga measure callback. The C++ side delegates measurement to a\nper-platform implementation that wraps text using the **same wrap algorithm**\nas the renderer — byte-for-byte — so Yoga's predicted height and the\nrenderer's actual layout always agree. Without that lockstep, Fabric's strict\nEXACTLY measure spec on `AdaptiveTextView` would clip the bottom line whenever\nthe two sides disagreed by even one wrap decision.\n\n## Installation\n\n```sh\nnpm install react-native-textflow\n# or\nyarn add react-native-textflow\n```\n\nRequires:\n\n- React Native **0.85+** (Fabric / New Architecture)\n- iOS **16.0+**\n- Android **API 24+**, Jetpack Compose **BOM 2026.05+** (Compose 1.10+)\n\n### Bare React Native (CLI)\n\nAfter installing the package, link the iOS pods:\n\n```sh\ncd ios \u0026\u0026 pod install\n```\n\nAndroid is autolinked through `react-native.config.js` — Gradle picks up the\ncustom CMakeLists and the Compose dependencies automatically on the next\nbuild.\n\n### Expo\n\n`react-native-textflow` works with any Expo project that uses **Expo Dev\nClient** + **Prebuild** (a.k.a. Continuous Native Generation). It does **not**\nwork with Expo Go — Expo Go ships a fixed set of native modules and cannot\nload custom Fabric components.\n\nIf your project is still on Expo Go, run `npx expo install expo-dev-client`\nfirst to switch to a custom dev build; the rest of your managed-Expo\nergonomics (EAS Build, OTA updates, config plugins, ...) are unaffected.\n\nInstall the package and a small build-properties helper:\n\n```sh\nnpx expo install react-native-textflow expo-build-properties\n```\n\nThen add this to your `app.json` / `app.config.js` so iOS pods resolve at\nthe 16.0 deployment target the library needs:\n\n```json\n{\n  \"expo\": {\n    \"plugins\": [\n      [\n        \"expo-build-properties\",\n        {\n          \"ios\": {\n            \"deploymentTarget\": \"16.0\"\n          }\n        }\n      ]\n    ]\n  }\n}\n```\n\nFinally regenerate the native projects and build:\n\n```sh\nnpx expo prebuild --clean\nnpx expo run:ios     # or: npx expo run:android\n# or, for cloud builds: eas build --profile development\n```\n\nThat's it — autolinking + the podspec + `react-native.config.js` already do\nthe rest. No additional Expo config plugin from this library is required.\n\n## Usage\n\n```tsx\nimport { AdaptiveText } from 'react-native-textflow';\n\n\u003cAdaptiveText style={{ fontSize: 18, color: '#111' }}\u003e\n  Hello, adaptive world.\n\u003c/AdaptiveText\u003e\n```\n\n### With a custom animation\n\n```tsx\n\u003cAdaptiveText\n  animation={{ type: 'spring', damping: 12, stiffness: 280, mass: 0.8 }}\n  style={{ fontSize: 24, fontWeight: '700' }}\n\u003e\n  Tweak damping and stiffness — feel the curve.\n\u003c/AdaptiveText\u003e\n```\n\n### Inside any RN container\n\n`\u003cAdaptiveText\u003e` plays nicely with `\u003cView\u003e`, `\u003cModal\u003e`, `\u003cScrollView\u003e`,\n`\u003cFlatList\u003e`, and `\u003cAnimated.View\u003e` out of the box. See the example app for\neleven ready-made demos.\n\n## Props\n\n| Prop | Type | Default | Description |\n| --- | --- | --- | --- |\n| `children` / `text` | `string` | `''` | The text to render. Children win over `text`. |\n| `style` | `StyleProp\u003cViewStyle \\| TextStyle\u003e` | — | 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. |\n| `splitBy` | `'word' \\| 'grapheme'` | `'word'` | Tokenization granularity. Use `'grapheme'` for CJK or per-character flow effects. |\n| `animation` | `AdaptiveAnimationConfig` | spring (18, 220, 1) | Motion curve used when the layout reflows. |\n| `wordSpacing` | `number` | 6 | Horizontal spacing between tokens, in points. |\n| `lineSpacing` | `number` | 4 | Vertical spacing between lines, in points. |\n| `textColor` | `ColorValue` | text style `color` | Override colour applied only to the text glyphs. |\n| ...rest | `ViewProps` | — | Standard view props (`testID`, `accessibility*`, layout, etc.). |\n\n### Animation config\n\n```ts\ntype AdaptiveAnimationConfig =\n  | { type: 'spring'; damping?: number; stiffness?: number; mass?: number }\n  | { type: 'timing'; duration?: number; easing?: 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' }\n  | { type: 'none' };\n```\n\n### `textAlign` and RTL\n\n`'start'` / `'end'` flip with the active layout direction (RTL → reversed).\n`'left'` / `'right'` are absolute. Toggling `textAlign` smoothly animates\nevery token to its new line position on both platforms.\n\n## Accessibility\n\n- The container exposes a single `accessibilityLabel` defaulting to the\n  rendered string. Per-word native views are hidden from the accessibility\n  tree, so VoiceOver and TalkBack read the phrase as one logical unit.\n- Both iOS and Android respect Dynamic Type / Font Scale; pass an absolute\n  `fontSize` and the OS will scale it relative to `.body` text style.\n- RTL strings (Arabic, Hebrew, etc.) flow correctly when `textAlign` is\n  `'start'` or `'end'`. Mirror behaviour follows `I18nManager`.\n\n## Performance notes\n\n- Tokenization runs once per `text` / `splitBy` change and is memoised.\n- Per-token widths are cached process-wide keyed by `(font config, token text)`,\n  so a drag-resize that pushes a new width per touch frame collapses N\n  `TextMeasurer.measure(...)` / `boundingRectWithSize:` calls to N hash-map\n  lookups after the first measurement of each token.\n- Per-token re-renders are skipped via Compose's smart skipping and SwiftUI's\n  view-identity tracking — only tokens whose props actually change are\n  recomposed.\n- The library does **not** depend on Reanimated. All motion is OS-native.\n- Animation is **placement-only**: each token's intrinsic size never\n  interpolates, so adjacent tokens can never overdraw each other's trailing\n  glyphs (the \"missing trailing letter\" bug class).\n\n## Example app\n\nThe repository ships a comprehensive example app showing `\u003cAdaptiveText\u003e`\ninside every common React Native container:\n\n```sh\nyarn install\nyarn example ios     # or: yarn example android\n```\n\nScreens:\n\n| # | Screen | What it demonstrates |\n| --- | --- | --- |\n| 1 | Resizable container | Drag-handle resize; words spring per touch frame. |\n| 2 | Animated width | `Animated.timing` driving the wrapper width. Reflow follows. |\n| 3 | Inside a `\u003cModal\u003e` | Reflow inside RN's stock modal. |\n| 4 | Inside a `\u003cScrollView\u003e` | Long paragraphs + sticky header; verifies measurement under prop toggles. |\n| 5 | Inside a `\u003cFlatList\u003e` | Each row is `\u003cAdaptiveText\u003e`. Verifies measurement under virtualization. |\n| 6 | Dynamic content | Add / remove / shuffle words; survivors slide, new ones fade in. |\n| 7 | Style morphing | Sliders for fontSize / letterSpacing / lineHeight + chips for weight / italic / color. |\n| 8 | RTL | Arabic \u0026 Hebrew samples × full `textAlign` matrix (animates on both platforms). |\n| 9 | Animation config | Live-tune spring vs timing vs none. |\n| 10 | Showcase card | Avatar + adaptive bio that grows on tap (the README screenshot). |\n| 11 | Dark / Light theme | Card chrome interpolates, AdaptiveText snaps to new color (color is a non-layout prop). |\n\n## Testing\n\nThe library ships a **six-layer test suite** that guards everything from\nthe JS façade down to the user-observable Compose / SwiftUI motion. The\nquick commands:\n\n```sh\nyarn lint \u0026\u0026 yarn typecheck                                              # static checks\nyarn test                                                                # Jest (JS unit)\ncd example/android \u0026\u0026 ./gradlew :react-native-textflow:testDebugUnitTest # Android JVM unit\nyarn maestro:android                                                     # E2E (Android)\nyarn maestro:ios                                                         # E2E (iOS)\n```\n\niOS XCTest runs via Xcode (open `example/ios/AdaptiveTextExample.xcworkspace`\nand pick the `AdaptiveTextExampleTests` scheme) or via `xcodebuild`; the\nAndroid Compose UI test runs via `connectedDebugAndroidTest` against a\nbooted emulator. CI runs every layer on `master` — cheap layers on every\nPR, simulator/emulator-bound layers gated by `paths-filter`.\n\nFull instructions including prerequisites, what each layer covers, and a\nMaestro debugging recipe live in [TEST_GUIDE.md](TEST_GUIDE.md).\n\n## Architecture in one diagram\n\n```\nJS:  \u003cAdaptiveText ...props /\u003e\n       │\n       ▼   Fabric codegen\nC++: AdaptiveTextShadowNode (LeafYogaNode + MeasurableYogaNode)\n       │             │\n       │             └─ measureContent()  → adaptive_text::measure(props, ctx, constraints)\n       │                                     │\n       │                                     ├─ iOS:    AdaptiveTextMeasurer.mm  (CoreText)\n       │                                     └─ Android: AdaptiveTextMeasurer.cpp\n       │                                                  └─ JNI → AdaptiveTextNativeMeasurer.kt\n       │                                                            └─ Compose TextMeasurer\n       │\n       ▼   Fabric mount\nNative view: AdaptiveTextView (UIView / FrameLayout)\n       │\n       ▼\nRenderer:\n  • iOS:    UIHostingController → AdaptiveTextContent (SwiftUI)\n            └─ AdaptiveTextFlowLayout (custom Layout)\n  • Android: ComposeView → AdaptiveTextFlow (Compose)\n            └─ AdaptiveFlowLayout (custom Layout) inside LookaheadScope\n              └─ per-token Text with Modifier.animateTokenPlacement\n```\n\nTwo invariants keep the system honest:\n\n1. **Measurer ↔ renderer wrap parity.** The Yoga measurer and the renderer\n   use the same shaper (Compose `TextMeasurer` / `boundingRectWithSize:`)\n   and the same wrap algorithm. A 6 dp horizontal safety margin further\n   compensates for subpixel disagreement between Compose's\n   `ParagraphIntrinsics` and `MultiParagraphIntrinsics` paths.\n2. **`ComposeView.layoutParams.height = MATCH_PARENT`.** Pins the\n   ComposeView's outer Android size to whatever Yoga gave AdaptiveTextView,\n   so a one-frame-stale Compose tree can never report a smaller size that\n   leaves recomposed content clipped at the bottom. (See the long-form\n   comment in `AdaptiveTextView.kt` for the trace.)\n\n## Roadmap\n\nDeliberately out of scope for v1, planned for future versions:\n\n- Per-word `onPress` / `onLongPress`\n- Magazine-style exclusion paths (text wrapping around shapes)\n- Shared-element transitions across screens\n- Web platform fallback (Reanimated `LinearTransition`)\n- Smoothly animated text colors (via `Animated.createAnimatedComponent` or Reanimated `useAnimatedProps`)\n\n## Contributing\n\n- [Development workflow](CONTRIBUTING.md#development-workflow)\n- [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)\n- [Code of conduct](CODE_OF_CONDUCT.md)\n\nSee [AGENTS.md](AGENTS.md) for an architecture map and the design\ndecisions behind every non-obvious choice in this codebase.\n\n## License\n\nMIT\n\n---\n\nMade with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaitrungduc1410%2Freact-native-textflow","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmaitrungduc1410%2Freact-native-textflow","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaitrungduc1410%2Freact-native-textflow/lists"}