-
Notifications
You must be signed in to change notification settings - Fork 141
Description
Introduction
<Text selectable> works very differently on iOS and Android.
On Android a long-press lets the user drag selection handles and copy any substring.
On iOS it only shows a single-action "Copy" tooltip that copies the entire block. No partial highlight is possible. This has been reported repeatedly since RN 0.39 (issues #13938, #35997) but is still not implemented today.
Digging through the native code (and Apple docs) shows why: React Native renders iOS <Text> with UILabel, and UILabel has no built-in text-selection APIs.
UITextView does, but isn’t used here(stackoverflow.com).
Because many apps (chat, note-taking, markdown viewers) need full selection, developers resort to work-arounds such as an invisible <TextInput editable={false}> or the community module react-native-uitextview (stackoverflow.com). Both options carry heavy trade-offs.
Details
Current behaviour
<Text selectable>
Long-press me on iOS -> you can only "Copy" everything.
</Text>- Android – expected: drag handles, contextual menu, partial copy.
- iOS – actual: "Copy" tooltip, whole paragraph copied.
Why we shouldn't use UILabel
Apple’s docs and multiple blog posts confirm UILabel cannot expose selection.
Making it copyable requires custom UIMenuController and still yields block-level copy only(stackoverflow.com, medium.com).
Work-arounds and limitations
| Work-around | Pros | Cons |
|---|---|---|
TextInput (editable={false}) |
Full selection | Breaks scrolling inside parent ScrollView, shows keyboard on some OS builds, different styling and accessibility issues |
react-native-uitextview |
Full selection, translation, share sheet | Component is a LeafYogaNode. Any wrapper <View> / <ScrollView> inside it gets a 0×0 frame unless you write custom native recursion. Integrating into rich-text layouts (markdown, chat bubbles) becomes complex |
Typical real-world use case
A markdown viewer renders heterogeneous nodes:
<MarkdownViewer
content={`
1. First item
2. Second item
Inline \\(a^2+b^2=c^2\\).
\`\`\`js
console.log('code');
\`\`\`
`}
/>During parsing the tree may look like:
<View>
<Text>“1. First…”</Text>
<View class="bulletList">…</View>
<ScrollView horizontal> …LaTeX SVG… </ScrollView>
<CodeBlock>…</CodeBlock>
<Text>“More text”</Text>
</View>
Both <View> and <ScrollView> nodes need Yoga layout (padding, scroll) and must not break the contiguous selection across the full content.
Proposed direction: container + leaf
A platform component pair:
<SelectableContainer>
<Text selectable>Inline prose …</Text>
<MyBulletList/> {/* ordinary View – keeps Yoga layout */}
<CodeBlock>…</CodeBlock>
<Text selectable>More prose …</Text>
</SelectableContainer><Text selectable>– leaf,MeasurableYogaNode + LeafYogaNode, backed byUITextViewfragments.<SelectableContainer>– normal Yoga view. At draw time it walks its subtree, flattens all descendantSelectableTextinto one nativeUITextView, and overlays it. Non-text children keep their own layout boxes, so code blocks, LaTeX scroll views, images, etc., still render and scroll correctly. (To be confirmed if it's faisable, not sure about that 😅, but I hope so 🤞)
Main features we want:
- Full-range selection across multiple paragraphs, list items, etc.
- No layout regressions for block.
Discussion points
- API – Would core maintainers consider acceptable adding something like a
SelectableContainer? - Alternative – Instead of new components, expose a
selectionMode="range"prop on existing<Text>that internally switches fromUILabeltoUITextView? - Performance & memory – Any concerns with large strings in a single
UITextView? - Use-case coverage – Does the container/leaf split satisfy other complex scenarios ?
- Am I missing something – Maybe we have other trade-off that are blocking doing this ?
Looking forward to feedback and guidance, or if a different approach would be preferred.