<text>

<text> renders a block of text. It is a full styled node, like a <node>, with layout, background, border, pointer events, state styles and layer styles, whose content is its string children. A <text> nested inside another <text> is an inline span that restyles part of the block.

Usage

<text style={{ fontSize: 18, color: "#c0caf5" }}>
  Hello, <text style={{ fontSize: 18, color: "#7aa2f7" }}>world</text>!
</text>

Content

  • Children are strings, numbers and nested <text> spans: <text>Score: {score}</text> works as in the DOM.
  • A "\n" in a string starts a new line, whatever the lineBreak mode.
  • JSX whitespace rules apply: a line break between text and a tag is dropped, so write {" "} where a span needs a space beside it.
  • There are no inline elements. Put images and boxes next to the text in a <node>, not inside the <text>.
  • A bare string outside any <text> still renders, but as unstyled text: white, 20px, in Bevy's built-in font rather than your default font. Always wrap text in <text>.

Spans

A nested <text> is a span: a styled run inside its block. It takes style, name, sharedTag and key.

  • Spans don't inherit. A span's unset properties take the defaults (white, 20px, the default font), not the parent's values. Restate fontSize and fontFamily on every span, for example by spreading a shared style object.
  • Bare strings do inherit: a string run takes the style of the <text> directly around it, block or span.
  • On a span, color, fontSize, fontWeight, fontFamily, lineHeight, letterSpacing and opacity (which fades the span's glyphs) apply. textAlign, lineBreak and textShadow belong to the block, and box styles (size, padding, background, border) have no effect: a span has no box.
  • filter, backdropFilter, morphFilter, transform3d and cache on a span are ignored with a spanLayerStyle warning. Put them on the block.
  • Spans take no event handlers or state styles (a propIgnored warning). A click on a span goes to its block.
const body: BevyStyle = { fontSize: 16, fontFamily: "Inter" };

<text style={{ ...body, color: "#c0caf5" }}>
  Status: <text style={{ ...body, color: "#9ece6a" }}>online</text>
</text>;

Text styles

Property Value Default Applies to
color CSS color, or an { animated } color binding white block, span
fontSize px number, or "px", "vw", "vh", "vmin", "vmax", "rem" string 20px block, span
fontWeight keyword, or a numeric string "1" to "1000" "normal" (400) block, span
fontFamily a registered family name the default font block, span
lineHeight multiple of the font size, px string, or { px } 1.2 block, span
letterSpacing px number, "px"/"rem"/"em" string, or { rem } 0 block, span
textAlign "left", "center", "right", "justify", "start", "end" "left" block
lineBreak "wordBoundary", "anyCharacter", "wordOrCharacter", "noWrap" "wordBoundary" block
textShadow { color?, offsetX?, offsetY? } none block

<editableText> uses the same color and font properties.

Color

color takes any CSS color; an invalid one renders magenta with a warning. opacity on the element multiplies it. There is no transition channel for color: to animate it, bind it to a shared value with interpolateColor (see Interpolation):

const t = useSharedValue(0);

<text
  style={{
    color: { animated: interpolateColor(t, [0, 1], ["#c0caf5", "#f7768e"]) },
  }}
>
  Alert
</text>;

Size and weight

  • fontSize: a bare number is logical pixels. "rem" is relative to Bevy's RemSize resource (20px by default); viewport units follow the window size. CSS em is not supported. An unparsable string becomes 0 (the text disappears) and reports a fontSize warning.
  • fontWeight: "thin" (100), "light" (300), "normal" (400), "medium" (500), "semibold" (600), "bold" (700), "black" (900), or any weight as a string, such as "800". A JSON number is rejected, which the TypeScript type prevents. An unknown keyword falls back to "normal" with a fontWeight warning.
  • Weights render only as far as the font provides them. A variable font covers every weight; a static font file has its own weight only, and Bevy does not synthesize bold.

Line height and letter spacing

  • lineHeight: a number is a multiple of the font size. As a string, "px" is absolute, and a unitless, "em" or "rem" value is a multiple of the font size. { px: 24 } is absolute. An invalid string falls back to 1.2 with a lineHeight warning.
  • letterSpacing: a number, a numeric string or a "px" string is logical pixels. "rem", "em" and { rem } are multiples of Bevy's RemSize (20px by default), not of the font size. "normal" is 0. An invalid string falls back to 0 with a letterSpacing warning.

Alignment and wrapping

  • textAlign aligns the lines within the block's width. A content-sized block is exactly as wide as its longest line, so give it a width (or let its parent stretch it) to center or right-align a single line. "start" and "end" follow the text direction.
  • lineBreak chooses where text wraps when it is wider than the block: "wordBoundary" breaks between words, "anyCharacter" anywhere, "wordOrCharacter" between words and inside words that don't fit a line alone, and "noWrap" never wraps (a "\n" still breaks).
  • Text wraps against the block's own width or, without one, the width its parent allows.

Shadow

textShadow draws one hard-edged copy of the text behind it: offsetX and offsetY in logical pixels (default 4 each) and a color (default black at 75% opacity). There is no blur. The element's opacity fades the shadow with the text.

<text style={{ textShadow: { color: "#000000cc", offsetX: 2, offsetY: 2 } }}>
  Game over
</text>

Fonts

Fonts are registered on the Rust side and selected by name from React:

TSX

<text style={{ fontFamily: "Mono", fontSize: 14 }}>let x = 1;</text>

Rust

app.add_plugins(
    ReactPlugins.set(
        ReactUiPlugin::default()
            .default_font("fonts/Inter-Variable.ttf")
            .font("Mono", "fonts/JetBrainsMono.ttf"),
    ),
);
  • Paths are asset paths, relative to the app's asset folder, loaded through the AssetServer at startup.
  • default_font applies to every <text>, span and <editableText> without a fontFamily. Without it, text uses Bevy's built-in font.
  • fontFamily names are exact and case-sensitive. An unknown name falls back to the default font with a fontFamily warning. Registering a name twice keeps the last path.
  • Each family is one font file. Register a variable font to get every weight, or register each static weight under its own name ("Inter Bold").
  • React cannot load fonts at runtime: every family must be registered up front.

Interaction and layers

A <text> block is a full element: it takes pointer handlers, state styles (a hoverStyle color for links), scroll props, and layer styles such as filter and transform3d. See <node> for the shared props and Layers for what promotes a block.

Limits

  • Spans don't inherit their parent's style, and they have no box, events or layer styles.
  • Unsetting both textAlign and lineBreak, including when a state style that set them ends, keeps the last alignment and wrapping. Set textAlign: "left" and lineBreak: "wordBoundary" explicitly instead.
  • Wrapped text inside a node that combines a percentage width with maxWidth can be measured at the unclamped width, so the node reserves too little height and overlaps what follows. Size such a container in pixels.
  • One shadow per block, without blur.

See <text> in the element reference, and color, fontSize, fontWeight, fontFamily, lineHeight, letterSpacing, textAlign, lineBreak and textShadow in the style reference.