DEV Community

Cover image for Instagram story text in React Native: rounded highlighted text that you can edit
Aqeel Ahmad
Aqeel Ahmad

Posted on

Instagram story text in React Native: rounded highlighted text that you can edit

If you have ever tried to build an Instagram or TikTok style story editor in React Native, you probably started the same way I did.

<Text style={{ backgroundColor: '#FF3D7F', color: 'white', fontSize: 32 }}>
  golden hour in lisbon
</Text>
Enter fullscreen mode Exit fullscreen mode

And you got a flat pink rectangle. No rounded corners, no padding around each line, and when the text wrapped to a second line, the box just grew into one big block. That is not the story look. The story look is a rounded background that hugs every line separately, with a little breathing room around the glyphs, and it grows line by line while you type.

I needed exactly that in an app I was building and could not find a library that did it well on both platforms. So I built one, react-native-highlight-text-view.

Typing story text on iOS: the rounded highlight grows line by line

Why plain backgroundColor cannot do this

The usual tricks all break somewhere.

  • backgroundColor on <Text> paints the whole text box. You get one rectangle, and borderRadius rounds that rectangle, not each line.
  • Nested <Text> with a background paints behind the glyphs of each run, but you cannot add padding to an inline span, and the corners stay square. The background also stops tight at the glyph edges, so lines look cramped.
  • Splitting the text into one <Text> per line works for a fixed caption, but you have to guess where the line breaks are. The moment the font, size or screen width changes, your breaks are wrong.
  • Editing makes all of this worse. A TextInput does its own line wrapping on the native side, and you only learn about it after the fact. Trying to mirror the layout in JS on every keystroke leads to flicker and backgrounds that lag a frame behind the cursor.

The real problem is that the line breaks are only known inside the native text layout engine. So that is where the background has to be drawn.

The approach: draw it in the native text layout

The library is a Fabric component (New Architecture) with a native implementation on each platform.

  • iOS: the view wraps a UITextView whose layout manager is a custom NSLayoutManager subclass. It overrides drawBackgroundForGlyphRange:atPoint:, so after the system has laid out and wrapped the text, it draws rounded, padded backgrounds for the glyphs that are actually on each line.
  • Android: the view is a custom EditText that draws the rounded highlights itself in onDraw(), using the Layout the system already computed, and then lets EditText draw the text, cursor and selection on top. Neighbouring character backgrounds are filled as one shape, so a line reads as one continuous rounded block instead of separate boxes per word.

Because both sides read the platform's own line layout, the highlight follows wrapping automatically, redraws as you type, and keeps working when you change the font, size, colors or alignment at runtime without remounting. As of 1.1.0, Android uses the same units as iOS (points), so values you tune on one platform give the same result on the other.

Install

npm install react-native-highlight-text-view
# or
yarn add react-native-highlight-text-view
Enter fullscreen mode Exit fullscreen mode

For the React Native CLI on iOS, run cd ios && pod install afterwards.

On Expo:

npx expo install react-native-highlight-text-view
npx expo prebuild   # or build with EAS
Enter fullscreen mode Exit fullscreen mode

This package contains native code, so it works in Expo development builds and EAS builds, but not in Expo Go. No config plugin is needed.

Requirements: React Native 0.76 or newer with the New Architecture (it is a Fabric only component), or Expo SDK 52 or newer in a dev build. Android minSdk is 24.

Quick start

Here is a controlled story input. It behaves like a regular TextInput: text plus onChange, a placeholder, keyboard props, and a ref with focus(), blur(), clear() and setText().

import { useRef, useState } from 'react';
import {
  HighlightTextView,
  type HighlightTextViewRef,
} from 'react-native-highlight-text-view';

export default function StoryText() {
  const [text, setText] = useState('');
  const ref = useRef<HighlightTextViewRef>(null);

  return (
    <HighlightTextView
      ref={ref}
      text={text}
      onChange={(e) => setText(e.nativeEvent.text)}
      placeholder="Tap to type"
      color="#FF3D7F"
      textColor="white"
      fontSize={32}
      fontWeight="800"
      textAlign="center"
      verticalAlign="center"
      paddingLeft={10}
      paddingRight={10}
      paddingTop={4}
      paddingBottom={4}
      highlightBorderRadius={10}
      returnKeyType="done"
      onSubmitEditing={() => ref.current?.blur()}
      style={{ width: '100%', height: 240 }}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

A few things worth knowing:

  • color is the highlight background and textColor is the text. Both accept any React Native color string, like "#FF3D7F", "rgb(255, 204, 0)", "hsl(158, 64%, 52%)" or "teal".
  • Numeric props take numbers (fontSize={32}) or strings (fontSize="32").
  • The view is sized by its style, like a TextInput. The highlight is drawn only behind the text inside it.
  • Setting returnKeyType to anything other than 'default' makes Return fire onSubmitEditing instead of inserting a new line. Leave it unset if you want multi-line typing.
  • With no verticalAlign, iOS draws the text at the top and Android centers it. Set verticalAlign to get the same result on both.

Three recipes

The example app in the repo has a story editor (live typing, color swatches, alignment, font size, ref methods) and a recipes screen. Here are three of them. Read-only views just use editable={false}.

Recipes on iOS and Android: story text, marker, tag chips and a quote

1. Story text

The classic look. A \n in the text gives you a manual break, and long lines wrap on their own.

<HighlightTextView
  editable={false}
  text={'golden hour\nin lisbon'}
  color="#FF3D7F"
  textColor="#FFFFFF"
  fontSize={30}
  fontWeight="800"
  textAlign="center"
  verticalAlign="center"
  paddingLeft={10}
  paddingRight={10}
  paddingTop={4}
  paddingBottom={4}
  highlightBorderRadius={10}
  style={{ height: 130 }}
/>
Enter fullscreen mode Exit fullscreen mode

2. Marker highlight

A soft color plus a large backgroundInsetTop paints only the lower part of each line, like a highlighter pen across a note.

<HighlightTextView
  editable={false}
  text="Ship the demo before Friday"
  color="#FFE066"
  textColor="#1D1D1F"
  fontSize={24}
  fontWeight="600"
  paddingLeft={3}
  paddingRight={3}
  backgroundInsetTop={14}
  highlightBorderRadius={3}
  verticalAlign="center"
  style={{ height: 92 }}
/>
Enter fullscreen mode Exit fullscreen mode

3. Tag chips

One small read-only view per tag. Give each chip a width. The example app measures the label with a hidden <Text> and uses that.

<HighlightTextView
  editable={false}
  text="#reactnative"
  color="#E0F2FE"
  textColor="#075985"
  fontSize={15}
  fontWeight="700"
  textAlign="center"
  verticalAlign="center"
  paddingLeft={10}
  paddingRight={10}
  paddingTop={6}
  paddingBottom={6}
  highlightBorderRadius={14}
  style={{ width: 130, height: 40 }}
/>
Enter fullscreen mode Exit fullscreen mode

Tips from tuning it

Padding grows the background, insets shrink it. The padding props (padding, which defaults to 4, plus paddingTop, paddingBottom, paddingLeft, paddingRight) expand the colored area outward from the text. The backgroundInset props (backgroundInsetTop, backgroundInsetBottom, backgroundInsetLeft, backgroundInsetRight, all default 0) pull it inward from the font's line box. Some fonts, like Eczar or Georgia, have tall built-in ascenders and descenders, so the highlight looks too tall. Insets fix that without touching the font size.

<HighlightTextView
  fontFamily="Eczar"
  fontSize={32}
  paddingLeft={8}
  paddingRight={8}
  paddingTop={4}
  paddingBottom={4}
  backgroundInsetTop={6}
  backgroundInsetBottom={6}
  text="Tight Background"
/>
Enter fullscreen mode Exit fullscreen mode

Touching line backgrounds. If you want the lines to join into one smooth vertical shape, combine lineHeight with insets. Start with lineHeight around fontSize + 4 to fontSize + 8, then raise backgroundInsetTop and backgroundInsetBottom until the backgrounds touch.

<HighlightTextView
  fontSize={32}
  lineHeight={36}
  paddingLeft={8}
  paddingRight={8}
  paddingTop={4}
  paddingBottom={4}
  backgroundInsetTop={14}
  backgroundInsetBottom={14}
  highlightBorderRadius={4}
  text="Multiple lines with touching backgrounds create smooth vertical flow"
/>
Enter fullscreen mode Exit fullscreen mode

Change fonts freely. fontFamily, fontSize and fontWeight can change at any time. The view re-measures and redraws the highlight. The old key={fontFamily} remount trick from before 1.0 is no longer needed.

Open the keyboard on mount with autoFocus, which is handy for a "create story" screen.

Try it

The current version is 1.1.0, fully typed, MIT licensed. Clone the repo and run yarn then yarn example ios (or yarn example android) to play with the story editor.

If it saves you a weekend of fighting backgroundColor, a star on GitHub helps other developers find it. And if something renders differently from what you expect on a font or device, open an issue with a screenshot. That kind of report is how the Android and iOS output got matched in 1.1.0.

Top comments (0)