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 byUITextView
fragments.<SelectableContainer>
– normal Yoga view. At draw time it walks its subtree, flattens all descendantSelectableText
into 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 fromUILabel
toUITextView
? - 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.