Skip to content

Partial Text selection on iOS: SelectableContainer + Text backed by UITextView #908

Open
@magrinj

Description

@magrinj

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 by UITextView fragments.
  • <SelectableContainer> – normal Yoga view. At draw time it walks its subtree, flattens all descendant SelectableText into one native UITextView, 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

  1. API – Would core maintainers consider acceptable adding something like a SelectableContainer ?
  2. Alternative – Instead of new components, expose a selectionMode="range" prop on existing <Text> that internally switches from UILabel to UITextView ?
  3. Performance & memory – Any concerns with large strings in a single UITextView?
  4. Use-case coverage – Does the container/leaf split satisfy other complex scenarios ?
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions