Skip to content

Fix TextInput placeholder keeping a removed letterSpacing on iOS - #59809

Open
hydromoll wants to merge 1 commit into
react:mainfrom
hydromoll:fix/textinput-placeholder-letter-spacing
Open

hydromoll wants to merge 1 commit into
react:mainfrom
hydromoll:fix/textinput-placeholder-letter-spacing

Conversation

@hydromoll

Copy link
Copy Markdown

Summary

On iOS (Fabric), a TextInput placeholder can keep the letterSpacing of a style it no longer has.

UIKit behaviour behind it: once a UITextField has shown a new placeholder string with NSKernAttributeName, it keeps drawing that kerning for later placeholders that carry no kern attribute (setting attributedPlaceholder to nil, or setNeedsDisplay, does not reset it; an explicit NSKernAttributeName value or a new text field does). React Native only adds NSKernAttributeName when letterSpacing is set (RCTAttributedTextUtils.mm), so removing letterSpacing cannot undo it.

Two ways an app gets there:

  1. View recycling. A TextInput with letterSpacing unmounts and its component view is reused for a TextInput without it and with a different placeholder. _props still holds the previous component's props, and updateProps sets the new placeholder before the new text attributes, so the new string first reaches UIKit with the old kerning — and keeps it. Example: a one-time code field with letterSpacing followed by a search field shows its placeholder as S e a r c h.
  2. Same view. The placeholder changes while letterSpacing is set, then letterSpacing is removed.

Setting letterSpacing: 0 works around it, but it is not equivalent to leaving it unset: NSKernAttributeName: 0 disables the font's pair kerning.

The fix, in RCTTextInputComponentView:

  • apply textAttributes before placeholder in updateProps, so a new placeholder string never reaches UIKit with stale attributes;
  • when letterSpacing goes from set to unset on a single-line input with no text, recreate the backed text input (the existing _setMultiline: path, which copies its configuration), restoring focus if it had it. An input that holds text is left alone, since its placeholder is hidden. Multiline inputs are not recreated: RCTUITextView draws its own placeholder label and does not show the bug.

Changelog:

[IOS] [FIXED] - TextInput placeholder no longer keeps a removed letterSpacing, including after view recycling

Test Plan:

RNTester built from source (main @ 225e8aa), iPhone 17 Pro simulator, iOS 26.0, New Architecture. Repro:

import React, {useState} from 'react';
import {
  Pressable,
  StyleSheet,
  Text,
  TextInput,
  View,
} from 'react-native';

export default function App() {
  const [swapped, setSwapped] = useState(false);
  const [step, setStep] = useState(0);

  return (
    <View style={styles.screen}>
      <View style={styles.content}>
        <Text>
          1. A spaced input unmounts; its native view is recycled for a new
          input without letterSpacing ({swapped ? 'new input' : 'spaced input'})
        </Text>
        {swapped ? (
          <TextInput key="plain" placeholder="Search" style={styles.input} />
        ) : (
          <TextInput
            key="spaced"
            placeholder="     "
            style={[styles.input, styles.spaced]}
          />
        )}
        <Pressable style={styles.button} onPress={() => setSwapped(v => !v)}>
          <Text>Swap</Text>
        </Pressable>

        <Text style={styles.title}>
          2. Same input: the placeholder changes while spaced, then letterSpacing
          is removed (step {step} of 2)
        </Text>
        <TextInput
          placeholder={step === 0 ? 'Code' : 'Search'}
          style={[styles.input, step < 2 ? styles.spaced : null]}
        />
        <Pressable
          style={styles.button}
          onPress={() => setStep(v => (v + 1) % 3)}>
          <Text>Next step</Text>
        </Pressable>

        <Text style={styles.title}>3. Control: never spaced</Text>
        <TextInput placeholder="Search" style={styles.input} />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  screen: {
    flex: 1,
    backgroundColor: '#fff',
  },
  content: {
    padding: 16,
    paddingTop: 80,
  },
  title: {
    marginTop: 24,
  },
  input: {
    height: 44,
    marginTop: 8,
    paddingHorizontal: 10,
    borderWidth: 1,
    borderColor: '#888',
    borderRadius: 8,
    fontSize: 17,
  },
  spaced: {
    letterSpacing: 6.8,
  },
  button: {
    alignSelf: 'flex-start',
    marginTop: 8,
    paddingHorizontal: 12,
    paddingVertical: 6,
    borderRadius: 6,
    backgroundColor: '#ddd',
  },
});
Case Before After
1. Recycled view: letterSpacing: 6.8 input new input without it, other placeholder S e a r c h Search
2. Same view: placeholder changes with letterSpacing: 6.8, then it is removed S e a r c h Search
3. Control: input never spaced Search Search
Spacing removed while focused and empty — keyboard and focus stay; placeholder and typed text unspaced
Spacing removed while the input holds text — text and focus are kept (input not recreated)
letterSpacing set again after removal — applied
Typing with spacing, then without — normal
4. Multiline, recycled view (as case 1) Search (not affected) Search
5. Multiline, same view (as case 2) Search (not affected) Search
Multiline, spacing removed while focused, then typing — focus kept, text unspaced

Before / after (same Playground, same steps, only the native build differs):

before-after

Unrelated, seen on main with and without this change: adding letterSpacing to an uncontrolled multiline TextInput that holds text clears the text.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Nov 5, 2026
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Nov 5, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant