@matiks/rn-stroke-text
v0.3.2
Published
rn-stroke-text
Readme
@matiks/rn-stroke-text
Stroked (outlined) text for React Native, with two interchangeable renderers: a Nitro native view and a React Native Skia renderer.
Installation
yarn add @matiks/rn-stroke-textPeer dependencies:
| Package | Required |
| --- | --- |
| react-native-nitro-modules | Yes |
| @shopify/react-native-skia | Yes, 2.9.1 — keep in step with the Matiks client |
| react / react-native | Yes |
The Skia renderer is always bundled, so Skia must be installed even if you only use mode="native".
iOS needs a pod install after adding the package. Android autolinks.
Usage
import StrokeText from '@matiks/rn-stroke-text';
<StrokeText
text="42"
fontSize={48}
fontFamily="Montserrat-700"
color="#FFFFFF"
strokeColor="#000000"
strokeWidth={6}
align="center"
/>;Props
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| text | string | — | Required. Coerced with String(text). |
| fontSize | number | 14 | |
| fontFamily | string | platform default | See Fonts. |
| color | string | #000000 | Fill colour. |
| strokeColor | string | #FFFFFF | Outline colour. |
| strokeWidth | number | 0 (Skia + iOS), 1dp (Android native) | |
| align | 'left' \| 'center' \| 'right' | 'left' | Matches RN <Text>. Android uses ALIGN_NORMAL, so it follows RTL. |
| width | number | unset | Fixed width. When unset the view sizes to the text. |
| numberOfLines | number | unset | Maximum lines. |
| ellipsis | boolean | false | Truncates with …. |
| letterSpacing | number | unset | Skia mode only. The Nitro view has no equivalent and ignores it. |
| lineHeight | number | unset | Skia mode only. Applied as a height multiplier of lineHeight / fontSize. |
| fontProvider | SkTypefaceFontProvider | unset | Skia mode only. Typefaces to match against, on top of the platform font manager. Required for app-bundled fonts on Android — see Fonts. |
| styles | StyleProp<ViewStyle> | — | Applied to the wrapper view. |
| mode | 'native' \| 'skia' | 'native' | Renderer to use. |
| accessibilityLabel | string | text | Read by screen readers. Both renderers expose the view as an accessible text element. |
Render modes
mode selects the renderer per instance. It defaults to 'native'.
native (default)
A Nitro HybridView backed by UILabel on iOS and a custom View drawing into android.graphics.Canvas on Android.
The view starts at 0×0 and measures itself after mount through the hybrid ref, so its size arrives one render after the text does. It exposes measureDimensions() natively.
skia
Builds a Skia paragraph, records it into a Picture, and draws it in a Canvas. When strokeWidth > 0 it strokes every line's outline path, then paints the paragraph on top, so the outline sits behind the glyphs. Colour glyphs such as emoji have no outline path, so when the text contains any, a dilated copy of the painted text outlines them. With no stroke it paints the paragraph directly.
Size is computed synchronously during render: the width is width when given, otherwise the longest line plus strokeWidth; the height is the paragraph height plus strokeWidth.
Choose skia when you want the text composited on the GPU alongside other Skia content, or to avoid the native view's two-pass measurement. Choose native for platform text behaviour and font handling.
Fonts
fontFamily is parsed into a family, a weight and an italic flag by splitting on -: the first segment is the family, a numeric segment becomes the weight, and italic sets the slant.
| Input | Family | Weight | Italic |
| --- | --- | --- | --- |
| Montserrat-700 | Montserrat | 700 | no |
| Montserrat-italic-800 | Montserrat | 800 | yes |
| BebasNeue-Regular | BebasNeue | 400 | no |
Skia mode resolves fonts only through the platform font manager, and matches on the font's real family name. It registers no typefaces of its own and is not given a typeface provider, so fonts that React Native or expo-font registered under an alias are not guaranteed to resolve.
| Platform | Font manager | Sees app-registered fonts |
| --- | --- | --- |
| Android | SkFontMgr_New_Android(nullptr, …) | No — system fonts only |
| iOS | SkFontMgr_New_CoreText(nullptr) | Yes, for fonts registered process-wide |
Candidates include a spaced form of the alias, so BebasNeue-Regular is tried as BebasNeue-Regular, BebasNeue, Bebas Neue and Bebas Neue-Regular. That covers the common case where the app alias is camel-case and the font's real family name is spaced.
Native mode does not have this limitation: Android loads from assets/fonts or the React font manager, and iOS resolves through UIFont.
Bundled fonts in Skia mode
On Android the system font manager cannot see app-bundled fonts at all, whatever they are called, so name matching alone is not enough. Pass fontProvider:
import { useFonts } from '@shopify/react-native-skia';
const fontProvider = useFonts({
'BebasNeue-Regular': [require('./assets/fonts/BebasNeue-Regular.ttf')],
'Montserrat-700': [require('./assets/fonts/Montserrat-Bold.ttf')],
});
<StrokeText text="42" fontFamily="BebasNeue-Regular" fontProvider={fontProvider ?? undefined} />;Register each typeface under the exact fontFamily string the call sites pass. useFonts registers by map key, so an alias key matches verbatim and neither the real family name nor the weight suffix has to line up. Build the provider once near the root of the app and share it — it loads asynchronously and returns null until ready, during which text falls back to a system font.
Known limitations in Skia mode
ellipsiswithnumberOfLinesdiffers on Android native. WithoutnumberOfLines, every renderer truncates to a single line. With it, Skia and iOS keep that many lines, but Android native still shows one.- Errors are not contained. Nothing in the renderer catches errors, so a failure while rendering propagates.
paragraph.getPathis unimplemented on React Native Web, so stroked text throws there. - Box size differs from Android native. Skia adds
strokeWidthto both axes; Android native addsstrokeWidth * 2 + 4pxto the width and nothing to the height. Layouts tuned against native Android can shift.
Migrating from 0.2.x
aligndefaults to'left', not'center', matching RN<Text>. Single-line text without awidthsizes to itself, so it does not move. Passalign="center"where multi-line text, or text given a fixedwidth, should stay centred.@shopify/react-native-skiais a required peer at2.9.1, even if you only usemode="native".- Screen readers now read the text. Both renderers expose it as an accessible text element labelled with
accessibilityLabel, ortextwhen that is unset. If a parent view already carries the label, hide the stroke text from accessibility withimportantForAccessibility="no-hide-descendants"andaccessibilityElementsHidden. - iOS
ellipsiswithnumberOfLineskeeps that many lines. It used to show one. - New props:
mode,letterSpacing,lineHeight,fontProviderandaccessibilityLabel.
Development
yarn specs # regenerate nitrogen bindings from src/specs/*.nitro.ts
yarn typescript # compile src/ to lib/
yarn typecheck # type-check without emittinglib/ is gitignored but is published via the files field, so build it before packing. There is no prepare or prepack hook to do it for you.
To test a build inside an app before publishing, pack it and install the tarball:
npm pack --pack-destination <dir>
# then in the app: "@matiks/rn-stroke-text": "file:./.local-packages/<tarball>"Bump the version before publishing — the registry already holds the current one.
Structure
src/— TypeScript source.index.tsxpicks the renderer,StrokeTextNative.tsxandStrokeTextSkia.tsximplement the two modes,specs/holds the Nitro spec that nitrogen reads.ios/— Swift implementation (StrokeTextView,StrokedTextLabel).android/— Kotlin implementation and the CMake/Gradle build.nitrogen/— generated bindings; commit this folder.nitro.json— nitrogen configuration and autolinking.
