Skip to content

[android][TextInput] Placeholder is not respecting custom font family and weight #50137

Open

Description

@lorenc-tomasz

Description

On some Android devices user can change default font to e.g. Cool jazz.

In the app we don't want to allow user to change font family, so we apply fontFamily and fontWeight.

Example:

           <TextInput style={{
                height: 60,
                borderColor: 'gray',
                borderWidth: 1,
                marginBottom: 20,
                fontFamily: 'Roboto',
                fontWeight: '400',
                fontSize: 20,
            }} placeholder={"Search..."} />

We are expecting the placeholder will be using the same fontFamilyas set in styles. However it's not a case. Placeholder will use device's default font instead.

To workaround this I manage to find this solution:

import React, { forwardRef, useRef, useCallback } from 'react';
import {Platform, StyleSheet, TextInput, TextInputProps} from 'react-native';

const CustomTextInput = forwardRef<TextInput, TextInputProps>((props, ref) => {
  const inputRef = useRef<TextInput | null>(null);

  const setRef = useCallback(
    (node: TextInput | null) => {
      if (node) {
        inputRef.current = node;

        if (typeof ref === 'function') {
          ref(node);
        } else if (ref) {
          const mutableRef = ref as React.MutableRefObject<TextInput | null>;
          mutableRef.current = node;
        }

        // Workaround for Android to apply style before setting placeholder
        // to apply font family correctly
        if (Platform.OS === 'android') {
          node.setNativeProps({
            style: StyleSheet.flatten([{
                fontFamily: 'Roboto',
                fontWeight: '400',
            }, props.style ?? {}]),
          });
        }
      }
    },
    [ref],
  );

  return <TextInput ref={setRef} {...props} />;
});

export default CustomTextInput;

so the fontFamily is applied correctly.

        <View
          style={{
            backgroundColor: isDarkMode ? Colors.black : Colors.white,
            paddingHorizontal: safePadding,
            paddingTop: safePadding,
            paddingBottom: safePadding,
          }}>
            <TextInput style={{
                height: 60,
                borderColor: 'gray',
                borderWidth: 1,
                marginBottom: 20,
                fontFamily: 'Roboto',
                fontWeight: '400',
                fontSize: 20,
            }} placeholder={"Search..."} />
        </View>
          <View
              style={{
                  backgroundColor: isDarkMode ? Colors.black : Colors.white,
                  paddingHorizontal: safePadding,
                  paddingTop: safePadding,
                  paddingBottom: safePadding,
              }}>
              <CustomTextInput style={{
                  height: 60,
                  borderColor: 'gray',
                  borderWidth: 1,
                  marginBottom: 20,
                  fontFamily: 'Roboto',
                  fontWeight: '400',
                  fontSize: 20,
              }} placeholder={"Custom search..."} />
          </View>

As you can see the first text input has wrong fontFamily and the second one uses the correct one.

However expected behaviour is that the custom font will be applied based on style.

All React-Native versions are affected.

Steps to reproduce

Set custom font into TextInput and set placeholder.
Change Font Style on device.
You will see that placeholder is not respecting the custom font.

React Native Version

0.78.1

Affected Platforms

Runtime - Android

Output of npx @react-native-community/cli info

System:
  OS: macOS 15.3.2
  CPU: (12) arm64 Apple M2 Pro
  Memory: 218.94 MB / 32.00 GB
  Shell:
    version: "5.9"
    path: /bin/zsh
Binaries:
  Node:
    version: 22.13.1
    path: /usr/local/bin/node
  Yarn:
    version: 1.22.22
    path: /usr/local/bin/yarn
  npm:
    version: 10.9.2
    path: /usr/local/bin/npm
  Watchman:
    version: 2025.02.17.00
    path: /opt/homebrew/bin/watchman
Managers:
  CocoaPods:
    version: 1.16.2
    path: /Users/tomaszlorenc/.rbenv/shims/pod
SDKs:
  iOS SDK:
    Platforms:
      - DriverKit 24.2
      - iOS 18.2
      - macOS 15.2
      - tvOS 18.2
      - visionOS 2.2
      - watchOS 11.2
  Android SDK:
    API Levels:
      - "28"
      - "31"
      - "34"
      - "35"
    Build Tools:
      - 30.0.3
      - 34.0.0
      - 35.0.0
      - 35.0.1
    System Images:
      - android-26 | Google APIs ARM 64 v8a
      - android-28 | Google ARM64-V8a Play ARM 64 v8a
      - android-34 | Google APIs ARM 64 v8a
      - android-34 | Google Play ARM 64 v8a
      - android-35 | Google APIs ARM 64 v8a
      - android-35 | Google Play ARM 64 v8a
    Android NDK: Not Found
IDEs:
  Android Studio: 2024.3 AI-243.22562.218.2431.13114758
  Xcode:
    version: 16.2/16C5032a
    path: /usr/bin/xcodebuild
Languages:
  Java:
    version: 17.0.14
    path: /usr/bin/javac
  Ruby:
    version: 3.3.0
    path: /Users/tomaszlorenc/.rbenv/shims/ruby
npmPackages:
  "@react-native-community/cli":
    installed: 15.0.1
    wanted: 15.0.1
  react:
    installed: 19.0.0
    wanted: 19.0.0
  react-native:
    installed: 0.78.1
    wanted: 0.78.1
  react-native-macos: Not Found
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: true
  newArchEnabled: true
iOS:
  hermesEnabled: Not found
  newArchEnabled: false

Stacktrace or Logs

No logs.

Reproducer

https://github.com/lorenc-tomasz/react-native-text-input-placeholder-font-issue-reproducer

Screenshots and Videos

No response

Activity

  1. lorenc-tomasz commented on Mar 19, 2025

    @lorenc-tomasz
    Author

    Hi @cortinico,
    could you look into this, please? I tried to find native solution but nothing worked for me. That's way at the moment I'm using the mentioned workaround.

  2. cortinico commented on Mar 19, 2025

    @cortinico
    Contributor

    Change Font Style on device.

    I don't have a device that allows this so I can't reproduce at the moment

  3. lorenc-tomasz commented on Mar 19, 2025

    @lorenc-tomasz
    Author

    Just to let you know that any new Samsung Android phone allows that. It's interesting that some users use e.g. Comic Sans on device and it's funny how apps look with that font, and even Google Maps doesn't manage it well :D

  4. narekmalk commented on Mar 19, 2025

    @narekmalk

    Hi, can I take this issue?

  5. lorenc-tomasz commented on Mar 20, 2025

    @lorenc-tomasz
    Author

    @narekmalk Do you have an idea how to fix it on native side?

  6. cortinico commented on Mar 20, 2025

    @cortinico
    Contributor

    Hi, can I take this issue?

    Please do

  7. narekmalk commented on Mar 24, 2025

    @narekmalk

    @cortinico, @lorenc-tomasz, I tried several physical devices and android emulators, but wasn't able to find the feature of changing system font
    I think would be better to assign this issue to someone else
    Apologies

  8. mensonones commented on Mar 25, 2025

    @mensonones

    Hey, @cortinico and @lorenc-tomasz

    I found out that the cause may be related to autoCapitalize

    <TextInput
      style={{
        height: 60,
        borderColor: "gray",
        borderWidth: 1,
        marginBottom: 20,
        fontFamily: "Roboto",
        fontWeight: "400",
        fontSize: 20,
      }}
      autoCapitalize="none"
      placeholder={"Search..."}
    />;

    If you pass autoCapitalize="none" the problem does not occur

    Image

    You must run Android again to get the changes related to this case. hot reload does not always work with this change

  9. lorenc-tomasz commented on Mar 26, 2025

    @lorenc-tomasz
    Author

    Hi @mensonones,
    I have checked your workaround. And it seems to work with 0.78.1. However when I tried the same solution on our production app that still uses 0.76.x, it doesn't fixed the issue. Only the workaround with ref is working fine.

  10. lorenc-tomasz commented on Mar 28, 2025

    @lorenc-tomasz
    Author

    Hi @cortinico,
    Could you unassign @narekmalk, as he doesn't have a device to look into the issue, please?

  11. lorenc-tomasz commented on Apr 2, 2025

    @lorenc-tomasz
    Author

    Hi @cortinico, @narekmalk and @mensonones,

    I have pushed changed into patch branch. No idea about quality of the solution but this works for me:

    diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java
    index a037077..2573aec 100644
    --- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java
    +++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java
    @@ -1298,11 +1298,37 @@ public class ReactEditText extends AppCompatEditText {
     
       @Override
       public void onDraw(Canvas canvas) {
    +    // Clip the canvas to the padding box if overflow is not visible.
         if (mOverflow != Overflow.VISIBLE) {
           BackgroundStyleApplicator.clipToPaddingBox(this, canvas);
         }
     
    +    // Call the superclass's onDraw method to draw the EditText's default content.
         super.onDraw(canvas);
    +
    +    // Check if we need to manually draw the hint with a custom font.
    +    if (mFontFamily != null && getText().length() == 0 && !isFocused() && getHint() != null) {
    +      // Get the Paint object used for drawing.
    +      Paint paint = getPaint();
    +
    +      // Apply the custom font family, style, and weight to the Paint object.
    +      Typeface typeface = ReactTypefaceUtils.applyStyles(
    +              paint.getTypeface(), mFontStyle, mFontWeight, mFontFamily, getContext().getAssets());
    +      paint.setTypeface(typeface);
    +
    +      // Store the original hint and text colors.
    +      int originalHintColor = getCurrentHintTextColor();
    +      int originalTextColor = getCurrentTextColor();
    +
    +      // Set the text color to the hint color to prevent EditText's default hint drawing.
    +      setTextColor(originalHintColor);
    +
    +      // Manually draw the hint text with the custom font.
    +      canvas.drawText(getHint().toString(), getCompoundPaddingLeft(), getBaseline(), paint);
    +
    +      // Restore the original text color immediately after drawing the hint.
    +      setTextColor(originalTextColor);
    +    }
       }
     
       /**
    

    What do you think?

  12. cortinico commented on Apr 2, 2025

    @cortinico
    Contributor

    I have pushed changed into patch branch. No idea about quality of the solution but this works for me:

    @lorenc-tomasz can you send this as a Pull Request and we'll look into it?

  13. lorenc-tomasz commented on Apr 2, 2025

    @lorenc-tomasz
    Author

    @cortinico

    I take different approach to fix it (branch patch2 on reproducer):

    diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditHintTypefaceSpan.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditHintTypefaceSpan.java
    new file mode 100644
    index 0000000..1a4fb45
    --- /dev/null
    +++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditHintTypefaceSpan.java
    @@ -0,0 +1,23 @@
    +package com.facebook.react.views.textinput;
    +
    +import android.graphics.Typeface;
    +import android.text.TextPaint;
    +import android.text.style.MetricAffectingSpan;
    +
    +public class ReactEditHintTypefaceSpan extends MetricAffectingSpan {
    +    private final Typeface typeface;
    +
    +    public ReactEditHintTypefaceSpan(Typeface typeface) {
    +        this.typeface = typeface;
    +    }
    +
    +    @Override
    +    public void updateDrawState(TextPaint paint) {
    +        paint.setTypeface(typeface);
    +    }
    +
    +    @Override
    +    public void updateMeasureState(TextPaint paint) {
    +        paint.setTypeface(typeface);
    +    }
    +}
    diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java
    index a037077..b3fdcb7 100644
    --- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java
    +++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java
    @@ -21,6 +21,7 @@ import android.os.Bundle;
     import android.text.Editable;
     import android.text.InputType;
     import android.text.Spannable;
    +import android.text.SpannableString;
     import android.text.SpannableStringBuilder;
     import android.text.Spanned;
     import android.text.TextPaint;
    @@ -125,9 +126,11 @@ public class ReactEditText extends AppCompatEditText {
       private boolean mOnKeyPress = false;
       private TextAttributes mTextAttributes;
       private boolean mTypefaceDirty = false;
    +  private boolean mForceSetTypeFaceOnDraw = false;
       private @Nullable String mFontFamily = null;
       private int mFontWeight = ReactConstants.UNSET;
       private int mFontStyle = ReactConstants.UNSET;
    +  private @Nullable Typeface mTypeface = null;
       private boolean mAutoFocus = false;
       private boolean mContextMenuHidden = false;
       private boolean mDidAttachToWindow = false;
    @@ -613,10 +616,17 @@ public class ReactEditText extends AppCompatEditText {
     
         mTypefaceDirty = false;
     
    -    Typeface newTypeface =
    +    mTypeface =
             ReactTypefaceUtils.applyStyles(
                 getTypeface(), mFontStyle, mFontWeight, mFontFamily, getContext().getAssets());
    -    setTypeface(newTypeface);
    +    setTypeface(mTypeface);
    +
    +    // Ensure the hint uses the same typeface
    +    if (getHint() != null) {
    +      SpannableString hintSpan = new SpannableString(getHint());
    +      hintSpan.setSpan(new ReactEditHintTypefaceSpan(mTypeface), 0, hintSpan.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    +      setHint(hintSpan);
    +    }
     
         // Match behavior of CustomStyleSpan and enable SUBPIXEL_TEXT_FLAG when setting anything
         // nonstandard
    @@ -628,6 +638,8 @@ public class ReactEditText extends AppCompatEditText {
         } else {
           setPaintFlags(getPaintFlags() & (~Paint.SUBPIXEL_TEXT_FLAG));
         }
    +
    +    mForceSetTypeFaceOnDraw = true;
       }
     
       // VisibleForTesting from {@link TextInputEventsTestCase}.
    @@ -1302,6 +1314,11 @@ public class ReactEditText extends AppCompatEditText {
           BackgroundStyleApplicator.clipToPaddingBox(this, canvas);
         }
     
    +    if (mForceSetTypeFaceOnDraw) {
    +      mForceSetTypeFaceOnDraw = false;
    +      setTypeface(mTypeface);
    +    }
    +
         super.onDraw(canvas);
       }
     
    

    Will try to create PR today :)

  14. lorenc-tomasz commented on Apr 5, 2025

    @lorenc-tomasz
    Author

    Final patch based on PR:

    diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java
    index a037077..33e67de 100644
    --- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java
    +++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java
    @@ -125,6 +125,7 @@ public class ReactEditText extends AppCompatEditText {
       private boolean mOnKeyPress = false;
       private TextAttributes mTextAttributes;
       private boolean mTypefaceDirty = false;
    +  private boolean mForceUpdateTypefaceAndFlagsOnLayout = false;
       private @Nullable String mFontFamily = null;
       private int mFontWeight = ReactConstants.UNSET;
       private int mFontStyle = ReactConstants.UNSET;
    @@ -254,6 +255,12 @@ public class ReactEditText extends AppCompatEditText {
           // Prevent text on being selected for next layout pass
           mSelectTextOnFocus = false;
         }
    +
    +    // Resolves issue with custom fonts not being applied, especially on hint text
    +    if (mForceUpdateTypefaceAndFlagsOnLayout) {
    +      mForceUpdateTypefaceAndFlagsOnLayout = false;
    +      updateTypefaceAndFlags();
    +    }
       }
     
       @Override
    @@ -606,13 +613,7 @@ public class ReactEditText extends AppCompatEditText {
         }
       }
     
    -  public void maybeUpdateTypeface() {
    -    if (!mTypefaceDirty) {
    -      return;
    -    }
    -
    -    mTypefaceDirty = false;
    -
    +  public void updateTypefaceAndFlags() {
         Typeface newTypeface =
             ReactTypefaceUtils.applyStyles(
                 getTypeface(), mFontStyle, mFontWeight, mFontFamily, getContext().getAssets());
    @@ -630,6 +631,16 @@ public class ReactEditText extends AppCompatEditText {
         }
       }
     
    +  public void maybeUpdateTypeface() {
    +    if (!mTypefaceDirty) {
    +      return;
    +    }
    +
    +    mTypefaceDirty = false;
    +    updateTypefaceAndFlags();
    +    mForceUpdateTypefaceAndFlagsOnLayout = true;
    +  }
    +
       // VisibleForTesting from {@link TextInputEventsTestCase}.
       public void requestFocusFromJS() {
         requestFocusInternal();
    
    
  15. lorenc-tomasz commented on Apr 16, 2025

    @lorenc-tomasz
    Author

    Bump the issue

  16. dilanbopanna commented on May 3, 2025

    @dilanbopanna

    I encountered a similar issue: the placeholder text in TextInput was not applying the custom font. After setting autoCapitalize="none", it started working.
    However, for Input fields with type 'password', the problem persists even after setting autoCapitalize="none".

  17. sayan-avantai commented on Nov 6, 2025

    @sayan-avantai

    Any update?

  18. olegmelnyk123 commented on Aug 5, 2026

    @olegmelnyk123

    any solution to this ?

  19. rifadhkp-circles commented on Sep 24, 2026

    @rifadhkp-circles

    Any update here?

  20. ziishaned commented on Oct 16, 2026

    @ziishaned

    I can reproduce this on a Samsung SM-S938W (Android 17, One UI 9) with a custom font picked in Settings, on RN 0.86.2 with the New Architecture. I think I found the root cause, and it also explains why it can't be reproduced on emulators or Pixels.

    Root cause: Samsung changes TextView.getTypeface()

    When a custom font is on, Samsung's TextView.getTypeface() does not return the view's typeface. This is from /system/framework/framework.jar pulled off the device and disassembled with dexdump:

    android.widget.TextView.getTypeface:()Landroid/graphics/Typeface;
      invoke-static {}, Landroid/graphics/Typeface;.semIsDefaultFontStyle:()Z   // false when a custom font is on
      iget-object v1, v3, Landroid/widget/TextView;.mOriginalTypeface
      if-eqz v0, 0009
      return-object v1                                                         // only when no custom font
      ...
      invoke-static {v0}, Landroid/graphics/Typeface;.defaultFromStyle:(I)Landroid/graphics/Typeface;
      return-object v3                                                         // otherwise the system font
    

    So with a custom font on, getTypeface() always returns the system font in the view's style, no matter what was passed to setTypeface(). The view still draws with the typeface that was set; only reading it back is wrong. Swapping the system font on an emulator doesn't change this method, which is why the bug only shows up on Samsung.

    How it reaches the placeholder

    ReactEditText.setInputType saves and restores the typeface around super.setInputType (for the password monospace case):

    override fun setInputType(type: Int) {
      val tf = super.getTypeface()   // on Samsung with a custom font: the system font
      super.setInputType(type)
      stagedInputType = type
      super.setTypeface(tf)          // writes the system font onto the view
      ...

    In 0.86, onAfterUpdateTransaction calls maybeUpdateTypeface(), which sets the custom font, and then commitStagedInputType(), which calls setInputType and swaps it for the system font. Typed text keeps the right font because it's drawn with CustomStyleSpan. The hint has no span, so it uses the view's typeface.

    This fits the workarounds above:

    • autoCapitalize="none" likely helps because the staged input type then matches the current one, so setInputType never runs. It doesn't help password fields, as reported above.
    • setNativeProps({ style }) sends the font again in a later transaction, after the input type is committed. It breaks again on the next input type change, such as toggling secureTextEntry.

    On main / 0.88

    #57805 (in 0.88.0) moved maybeUpdateTypeface() after commitStagedInputType(), which probably fixes the placeholder on first render. But setInputType still reads super.getTypeface(), and maybeUpdateTypeface() only reapplies the font when a font prop changes. So a later input type change, like a secureTextEntry toggle or a keyboardType change, should still drop the font. I haven't tested 0.88.

    Suggested fix

    Don't read the typeface back from the view. Remember the one ReactEditText last set, and restore that:

    private var appliedTypeface: Typeface? = null
    
    override fun setTypeface(tf: Typeface?) {
      appliedTypeface = tf
      super.setTypeface(tf)
    }
    
    override fun setInputType(type: Int) {
      val typefaceBeforeInputType = appliedTypeface
      super.setInputType(type)
      stagedInputType = type
      if (typefaceBeforeInputType != null) {
        setTypeface(typefaceBeforeInputType)
      }
      ...
    }

    Restoring paint.typeface instead wouldn't be safe: with Android 12+'s Bold text setting, the paint holds a weight-adjusted copy, and applying it again would bold it twice.

    We can't patch the prebuilt ReactAndroid in our app, so we ship this as a ReactEditText subclass. A ReactTextInputManager subclass serves it, registered in an on-demand package ahead of MainReactPackage so it wins the AndroidTextInput lookup. On the Samsung, placeholders on email, password, multiline and search fields now all keep the app font.

    Happy to open a PR if this direction looks right.

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions