Elements

Text

<Text> draws a paragraph. It is the only element that shows strings: a string child of a <View> draws nothing. The paragraph is typeset natively (CoreText on iOS and Mac Catalyst, StaticLayout on Android) and measured on the engine thread, so the main thread only draws it.

el-text.tsx Edit in Playground Open in new tab
import { mount, signal, Text, View } from "nsh";

const LONG =
  "Text is typeset natively (CoreText on iOS, StaticLayout on Android) and measured on the engine thread, so the main thread only draws it.";

function App() {
  const tapped = signal("nothing yet");
  const lines = signal(2);
  const link = { color: "#0969da", textDecorationLine: "underline" } as const;
  return (
    <View style={{ flex: 1, backgroundColor: "#ffffff", padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: "#1f2328" }}>text</Text>

      <Text style={{ fontSize: 16, lineHeight: 24, color: "#1f2328" }}>
        {"Nested text elements are spans: "}
        <Text style={{ fontWeight: "700" }}>bold</Text>
        {", "}
        <Text style={{ fontStyle: "italic", color: "#8250df" }}>italic</Text>
        {", and "}
        <Text style={link} onPress={() => tapped.set("the first link")}>
          links
        </Text>
        {" that take presses of "}
        <Text style={link} onPress={() => tapped.set("the second link")}>
          their own
        </Text>
        .
      </Text>
      <Text style={{ fontSize: 13, color: "#6e7781" }}>Pressed: {tapped()}</Text>

      <View style={{ height: 1, backgroundColor: "#d0d7de" }} />

      <Text numberOfLines={lines()} style={{ fontSize: 15, color: "#1f2328" }}>
        {LONG}
      </Text>
      <View style={{ flexDirection: "row", gap: 8 }}>
        <View
          onPress={() => lines.set(lines() === 2 ? 0 : 2)}
          activeOpacity={0.6}
          style={{ backgroundColor: "#ddf4ff", borderRadius: 8, paddingHorizontal: 12, paddingVertical: 8 }}
        >
          <Text style={{ color: "#0969da" }}>{lines() === 2 ? "Show all" : "numberOfLines 2"}</Text>
        </View>
      </View>

      <Text numberOfLines={1} ellipsizeMode="middle" style={{ fontSize: 15, color: "#57606a" }}>
        {LONG}
      </Text>

      <Text style={{ fontSize: 17, color: "#1f2328" }}>
        <Text style={{ textDecorationLine: "line-through", textDecorationColor: "#cf222e" }}>struck</Text>{" "}
        <Text style={{ textTransform: "uppercase", letterSpacing: 2 }}>spaced</Text>{" "}
        <Text style={{ fontVariant: ["tabular-nums"] }}>0123456789</Text>
      </Text>

      <Text
        style={{
          fontSize: 28,
          fontWeight: "800",
          color: "#ffffff",
          backgroundColor: "#54aeff",
          paddingHorizontal: 10,
          borderRadius: 8,
          textShadowColor: "rgba(0,0,0,0.5)",
          textShadowOffset: { width: 2, height: 2 },
          textShadowRadius: 4,
        }}
      >
        Text shadow
      </Text>
    </View>
  );
}

mount(App);
<Text style={{ fontSize: 16, lineHeight: 24, color: "#1f2328" }}>
  Read the <Text style={{ fontWeight: "700" }}>terms</Text> before you continue.
</Text>

#Spans

A <Text> inside a <Text> is a span of the same paragraph, not a separate view. Spans inherit the text styles of the paragraph (color, fontSize, fontWeight, …) and override what they set. The whole block is one native view.

<Text style={{ fontSize: 17 }}>
  <Text style={{ fontWeight: "300" }}>Light </Text>
  <Text style={{ fontWeight: "bold" }}>Bold </Text>
  <Text style={{ fontStyle: "italic", color: "#8250df" }}>Italic</Text>
</Text>

A span with a press handler is a link: a press on its characters goes to the span, a press elsewhere to the paragraph.

<Text style={{ fontSize: 15 }} onPress={() => log("the sentence")}>
  {"By continuing you agree to the "}
  <Text style={{ color: "#0969da", textDecorationLine: "underline" }} onPress={() => open("terms")}>
    Terms of service
  </Text>
  .
</Text>

#Inline views and images

Any other element inside a <Text> (a <View>, an <Image>, even inside spans) is an inline attachment: it is laid out at its own size, sits on the baseline and wraps with the words. It takes presses of its own.

<Text style={{ fontSize: 16, lineHeight: 26 }}>
  {"Made with "}
  <Image src={logo} style={{ width: 18, height: 18, borderRadius: 4 }} />
  {" nsh. Status "}
  <View style={{ backgroundColor: "#1a7f37", borderRadius: 9, paddingHorizontal: 7, height: 18, justifyContent: "center" }}>
    <Text style={{ color: "#ffffff", fontSize: 11, fontWeight: "700" }}>LIVE</Text>
  </View>
</Text>

Attachments cut off by numberOfLines or an ellipsis are hidden. Copying a selectable text leaves them out.

#Whitespace

JSX text follows React’s rules: lines are trimmed except at the text’s ends, whitespace-only lines are dropped and the remaining lines are joined with one space. Spaces on one line are kept ({a} {b} keeps its space). Write {" "} or a string expression when a span needs a leading or trailing space.

#Props

Besides the common props (with style typed as TextStyle):

PropTypeDefaultDescription
numberOfLinesnumber0The most lines shown; 0 is unlimited.
ellipsizeMode"tail" | "head" | "middle" | "clip""tail"Where text that does not fit numberOfLines is cut.
allowFontScalingbooleantrueFollow the system text size setting.
maxFontSizeMultipliernumber0The upper bound of the system scale; 0 is none.
adjustsFontSizeToFitbooleanfalseShrink the font until the text fits its lines (numberOfLines) and its box.
minimumFontScalenumberHow far adjustsFontSizeToFit shrinks at most (0..1; at least 0.1).
selectablebooleanfalseA long press highlights the block and offers Copy.
onTextLayout(e: { lines: TextLayoutLine[] }) => voidThe laid-out lines after each layout.

The select-text Tailwind class is the same as selectable.

#Text styles

TextStyle adds to the view styles: color, fontSize, fontWeight ("normal", "bold" or 100–900), fontStyle, fontFamily, lineHeight, letterSpacing, textAlign, textAlignVertical, textTransform, textDecorationLine, textDecorationColor, textDecorationStyle, textDecorationThickness, textUnderlineOffset, textShadowColor, textShadowOffset, textShadowRadius, fontVariant and writingDirection. See the style reference and text.

fontFamily takes a system family or the name an imported font evaluates to:

import { Text } from "nsh";
import Inter from "./fonts/Inter-Regular.ttf";

<Text style={{ fontFamily: Inter, fontSize: 17 }}>Inter</Text>;

The font is registered before the first frame. See fonts.

#Events

EventPayloadWhen
onTextLayout{ lines: TextLayoutLine[] }After each layout of the paragraph.
onPress, onLongPress, …noneAs on view; on a span, presses on its characters.
onLayout{ x, y, width, height }The block’s frame changed.

TextLayoutLine is { text, x, y, width, height, ascender, descender, capHeight, xHeight }, in the text’s content box:

const lines = signal(0);
<Text onTextLayout={(e) => lines.set(e.lines.length)}>{article}</Text>;

#Ref methods

None of its own.

#Platform notes

  • selectable copies whole blocks: there is no range selection. iOS shows the edit menu, Android a floating action mode.
  • Text decorations other than a solid single line (double, dotted, dashed, wavy) are drawn by nsh on iOS.
  • iOS texts draw no gradient backgrounds or inset shadows.
  • fontScale() from nsh/device reports the current system text scale.