# Text Styles, revisited (Oct 2025)
## Background
Bevy's ability to display text and fonts has evolved substantially over the last several releases. The biggest change came in Bevy 0.15, which introduced "text as entities": the ability to represent individual text spans as ECS entities, allowing components to be styled and animated using familiar ECS patterns.
This change unlocked a number of capabilities that we didn't have before, such as the ability to easily animate individual text spans within a word-wrapped paragraph. An example of the kinds of special effects that are now possible can be see in the [pretty-text crate](https://github.com/void-scape/pretty-text?tab=readme-ov-file).
(Sadly, one of the primary motivating use cases, which was the ability to mix text and icons within a word-wrapped block, has still not been addressed.)
## Pain points of the current system
Unfortunately, the font APIs are still very low-level and often tedious to use. Having to individually assign a `Handle<Font>` for each text span is both a chore and hurts a number of important use cases.
* A developer should be able to specify the default font for their application once, without needing to plug in a font handle to every individual span of text.
* The ability to globally increase the app's font size is an important feature for users who have less-than-perfect eyesight.
* Buttons and other widgets often want to be able to dynamically affect the text styles of their children: for example, a disabled button will show its label in a lighter color than a non-disabled button would. While Bevy Feathers does support dynamic text inheritance as part of it's theming system, this capability is not currently available generally.
* Bevy internally creates font atlases (textures which hold rasterized copies of every glyph) for each font face/size combination, and these are not reclaimed when the font goes out of use.
* For text which consists of multiple spans, users ought to be able to override the font size of individual spans separately, without being forced to override other style properties.
* By the same token, a user ought to be able to easily make individual text spans bold or italic without having to fetch the specific asset for that font variant.
## Lessons from CSS
The general policy stance that Bevy has towards CSS is to avoid slavishly imitating the design of CSS, but to borrow ideas from it where it makes sense:
* **Pro**: Many Bevy users have a background in web development and are familiar with CSS concepts.
* **Pro**: The amount of developer expertise and thought that has been put into the design of CSS is orders of magnitude greater than Bevy.
* **Con**: Some parts of CSS are less than perfect, as even its creators will admit.
* **Con**: Some parts of CSS may not be appropriate for an ECS game engine or for the Rust ecosystem.
In the context of this discussion, what we are interested in is the way CSS supports inheritance of text styles: properties such as font face, text color, and size are automatically propagated from parent elements if not overridden.
> [!NOTE]
> Interestingly, while many CSS style properties propagate downward as *constraints*, the only properties that are truly inherited by default are the ones relating to text styles. That's a small handful of style properties out of several hundred possible.
>
> The reason for this is easy to deduce: a typical HTML document contains a vast number of text spans, and it would be onerous in the extreme to have to assign style properties to each span individually. While a typical game might not have as many text spans as an encyclopedia page, it still has quite a few.
## Inherited Styles Considered
A mechanism for inheriting text styles in Bevy would solve a number of pain points: it would allow text styles to be specified in fewer places, and it would allow widgets to influence the dynamic styles of their children.
Unfortunately, this kind of inheritance has a nonzero performance impact. Consider the following simplified hierarchy:
```mermaid
flowchart TD
A["Root Entity
(font: FiraSans)"] --> B
B[Entity A] --> C
C[Entity B] --> E
E[Text]
```
In this diagram, we have a root element with an inheritable style property (font: `FiraSans`), two intermediate entities, and a text entity at the bottom. The goal is to keep the text entity's style in sync with the root.
There are four things that can happen which would require updating the style of the `Text` entity:
* The root element could change: that is, the component that represents the font face could be removed or replaced.
* The text entity could change: either it is newly spawned, or an override style component could be added or removed.
* One of the intermediate entities could change, due to an override style component being added or removed.
* The hierarchy could change: either the text entity or one of the intermediate entities could be reparented.
Some of these changes are more easily tracked than others. For example, detecting the initial spawn of a new `Text` entity can easily be accomplished using an `on_add` component hook. Similarly, reacting to insertions, removals, or replacements of a style property component can be done either with observers or systems.
However, what's not so easy is tracking arbitrary changes to the hierarchy. If we want to put Entity B under a new parent, this could have an impact on the style of the text entity; however, because Entity B doesn't itself have any text-related properties, the reparenting operation is indistinguishable from any other hierarchy change. As a result, we'd have to recompute text styles every time someone modified a `Children` component, which would entail an unacceptable degree of overhead.
An alternative is to store hidden components on Entity A and B letting them know that they are participating in text inheritance. However, this creates a different kind of overhead, albeit perhaps not as much. If we only marked entities that were ancestors of text nodes, it might not be *too* bad. Something to discuss!
## Inheritance in Feathers
Bevy Feathers currently supports a more limited, opt-in kind of inheritance that relies on Bevy's hierarchical component propagation plugin.
Any entity can have an `InheritableFont` component. This changes the `TextFont` component of descendant entities that have the `ThemedText` marker component. There's also a `ThemeFontColor` component which affects the `TextColor` component in a similar way.
Somewhat confusingly, this only works if all the intermediate entities also have `ThemedText`, or have `PropagateOver::<TextFont>` This is a limitation of the hierarchy propagation feature.
## Alternative Approaches: Truncated Inheritance
The inheritance of text style properties in CSS extends all the way from the document root down to the leaves of the hierarchy. However, the place where inheritance is most valuable is near the bottom of the tree, just one or two levels above those leaves. By "most valuable" I mean that this is where the majority of text styles are specified. A typical example in HTML is:
```html
<p class="dialogue">to be or <i>not</i> to be, that is the question.</p>
```
From the perspective of the text node "not", when we want to search up the ancestor chain to compute the effective text style, we don't have to look very far. While we do have the capability to search all the way up to the root of the document, we might not need that capability.
This is especially true in a world in which largely consists of BSN widgets or UI components which fully specify the text styles of their children. In this world, each widget would have to opt-in to inheritance using a marker component of some kind.
In this world, we wouldn't search all the way up to the root of the hierarchy, instead we'd have some component that designates a sub-tree as a styling context. This is in effect what Feathers does. Propagation would be confined to that sub-tree rather than the entire hierarchy.
The advantage of a scheme like this is reduced cost: limiting inheritance to a small number of tree levels means less CPU time spent propagating changes throughout the tree.
The downside is that this is harder to understand: we'd need to educate users as to when and how inheritance is enabled.
There are actually a bunch of alternate approaches possible, but they generally require some compromise on the ergonomics. Whether that's acceptable is a topic for future discussions.
## Fine-Grained Property Components
The current organization of text style components represents a compromise between the needs of the renderer (which wants everything in one place that's easy to find) and users who might want to be able to specify and override font properties in isolation.
If we didn't have to take into account the needs of the renderer and the cost of propagation, then it would make sense for each different font property (face, size, slant, weight, color, etc.) to be a separate component.
> [!NOTE]
> This argument towards fine-grained components *only* applies to properties that are inheritable, because inheritance happens at the granularity of components. While there may be reasons to break up other kinds of style properties into separate components, those reasons should be considered unrelated to this discussion.
An alternate approach would have just one style component with a bunch of optional properties.
## ComputedTextStyle
A way to address both the needs of fine-grained properties and inheritance is to cache the combined properties in a dedicated component, which is called `ComputedTextStyle`. This component would do a number of jobs:
* It combines all of the various text style properties in a single component which is easily accessed by the renderer.
* This includes both inherited values as well as values set directly on the entity - in other words, it represents the current *effective* style.
* It also holds any additional housekeeping information needed by the renderer.
`ComputedTextStyle` is not intended to be user-facing (although, like all Bevy components, users who want to do advanced things can gain access to it). The fields of this component would be set automatically by various style propagation systems and observers.
This means that the user is free to specify different font properties at different levels of the hierarchy: a typical game might use the same font face everywhere, so it would specify that property globally; whereas properties like size and slant might be specified for individual spans.
(Note that in practice, `ComputedTextStyle` might not be just one component, depending on the needs of change detection.)
The alternative, without `ComputedTextStyle`, is to do the inheritance calculation in the renderer directly, walking up the ancestor chain of each text span to calculate the effective style.
## DefaultTextStyle Resource
Enabling inheritance means that text styles can be *partial*: if a given property (such as text color) is missing from a text node, we can search the ancestor chain for it.
But what if we don't find anything? The current behavior is to revert to a built-in default.
However, we could instead define a global default text style using a resource. Looking up resources is relatively cheap - much cheaper than searching the ancestor chain.
## Font Sizing
In current Bevy, there's only one way to specify font size: as an `f32`. This means different things depending on whether we're talking about text in a UI context (it means pixels) or text in 2d or 3d contexts. This is inconsistent with both CSS and other Bevy UI constructs which use `Val` to represent different size units.
However, this doesn't mean that we should just switch over to using `Val` for font sizes. Even in CSS, font sizes are special, and not every kind of length option makes sense for font sizes, or vice-versa.
Instead, the plan is to introduce a new enum, `FontSize`, which has variant for pixels, screen size, and other units that make sense for fonts.
In web development, there are two units in particular that are frequently used, especially for authors who are following best practices for accessibility: `em` and `rem`. `em` units scale the font relative to its parent, while `rem` units scale the font relative to the font size of the root element. (The word "em" means "the height of an upper-case letter 'M'", which is typically the same as the font size.)
These units are particularly important for users with impaired vision who struggle with readability: by increasing the font size of the root element, all of the other font sizes automatically adjust proportionally. Note that this is distinct from merely scaling or magnifying the UI, which has the effect of increasing the size of all the non-text elements such as borders and padding.
However, it's not clear what `rem` means in the context of Bevy, since there isn't necessarily a single UI root element. One possibility is to walk up the hierarchy looking for font size components until we run out of parents; another approach is to store the global default font size in a resource and call that the "root". This latter approach would be considerably cheaper to evaluate, but is less pedantically accurate in naming.
Note that we'll likely also want to add `em` and `rem` options to `Val`, so that widgets like buttons can increase their size along with the fonts contained within them.
## Slant and Weight
Making text **bold** and *italic* are probably the most common things to do for users who are composing text in more than one style. They are so common, in fact, that HTML, Markdown, and even Discord have special shorthand syntax for them. It's ironic, then, that using inline styles are some of the hardest things to do in Bevy text!
With the exception of variable fonts, most fonts represent different slants and weights as separate asset files. Worse, there's no standard naming convention for these files: even within a single download site, such as Google Fonts, you will see names with ".Regular", "\_regular" or "-regular".
The only way to use these fonts in Bevy presently is to load the font asset for the variant you want, and then assign the resulting handle to the span entity. This means that:
* You can't make an individual span bold or italic without knowing the name of the current font.
* You need to know not just the name of the font but the entire path to the asset, with the correct spelling for the variant name.
* You'll also need a reference to the asset server to load the handle.
Wouldn't it be nice if we could just say "make this section bold" and have everything work? That's how it works in HTML/CSS, after all.
To fix this, we'll need to move away from specifying fonts using asset handles directly, and instead add a layer of indirection. In CSS, there is the `@font-face` directive, which allows the developer to organize a group of related font variants under a single alias.
However, CSS's solution is probably overkill for our needs. For one thing, in CSS, you can specify a font family using a comma-separated list of alternate font names, whereby it tries each one in turn until it finds one that it can load. In a game where assets are pre-baked, we probably don't need this, except for the case of system fonts, which we can handle as a special case.
(Oh yes, we had to bring up the issue of system fonts too: this is it's one of the oldest unresolved tickets still active.)
The general idea is to establish a global "font registry" in which multiple font variants can be organized under a single font alias. The font alias can then be associated with individual text spans, or inherited like other text style properties.
To make inheritance / overriding work properly, I think that probably `FontFace` should be an enum, with "handle" and "alias" options. This means that an alias can override an inherited handle, and vice-versa:
```rust
#[derive(Component)]
enum FontFace {
// Reference to an asset handle
Handle(Handle<Font>),
// Reference to a font alias, which is a newtype
// wrapper around `SmolStr`.
Alias(FontAlias),
}
```
The font registry should allow adding font assets under an alias, but it should also hide whether the font is a variable font or a system font. This means that code using the alias should not have to care what kind of font it is.
Using `SmolStr` to represent the font alias name gives us the benefit that we can define these names as static consts and not have to worry about memory lifetimes or copying.
When a text span contains a reference to an alias name, it uses both that and the current slant/weight options (as well as any other relevant options such as condensed) to determine exactly which font asset is to be used.
As an example, suppose we want to render the string "that's a **bold** step". This would consist of three spans, and we can insert a single `FontFace` component at the parent level, which is inherited by all three spans. In the middle span, we can then insert a `FontWeight::Bold` component; the other two spans will not have this component and so will default to the regular weight.
When the `ComputedTextStyle` is computed for the middle span, it looks up the font alias in the registry, along with the weight, slant, and other style bits, to find the best matching font asset. If it turns out this is a variable font, then other steps can be taken to ensure that the text is rendered with the proper weight.
In the special case of system fonts, we might want to arrange to have a fallback in case the system font can't be found. Rather than specifying this at the level of each individual text span or style, I propose something more basic: at the registry level, for each registered alias, we can specify a fallback alias.
Note that if we want to support the full capabilities of OpenType fonts, the registry needs to know about more attributes than just slant, weight and stretch: there can be an arbitrary number of "features" (like "small caps"), each of which has a standard 4-letter ID. So the entries in the registry would probably have to look something like this:
```rust
struct FontFaceSpec {
pub weight: FontWeight,
pub style: FontStyle,
pub stretch: FontStretch,
pub features: HashSet<OpenTypeFeature>,
pub src: AssetPath,
// There may be additional attributes
}
```
## Alternative Approaches: All-in on Aliases
The existence of font aliases knocks out one of the justifications for font face inheritance: the need to inject `AssetServer` everywhere. If we can simply specify fonts by alias (a token which is easily cloneable and can be statically declared), then assigning aliases to individual spans is relatively easy. So one might imagine a world in which we didn't have text style inheritance, but just used font aliases for every span.
However, this doesn't address the other motivations for inheritance, such as widgets that dynamically affect the styles of their children.
## The fate of `TextSpan`
There are some controversial aspects about the current implementation of `TextSpan`, which are discussed [here](https://github.com/bevyengine/bevy/issues/21226). The proposal on the table is not to regress "text as entities" but to rely less on required components in representing text spans.
## Text vs Text2D
Because UI uses a different renderer than floating text in 2d or 3d cameras, it requires that text be a different type. This affects things such as what required components are dragged along with the text - for example, `Text` requires `Node` so that it can participate in Taffy layout, which free-floating text would not want.
At the same time, however, we would like to share as much as possible of the text infrastructure between the two contexts: the same algorithms and components where possible. The current design works, but is not entirely satifactory; more discussion is needed and proposals are welcome.
## Font Atlas Hygiene
Currently unused font atlases are not cleaned up, and we don't have a means to track which font atlases are no longer needed. This means that, as long as we keep switching font sizes at runtime, we could potentially use up arbitrary and unpredictable amounts of RAM.
Part of Bevy's goal is to be able to support platforms with limited or fixed memory budgets - game consoles and handhelds often do not have virtual memory, so if you run out, you crash. This is, in a word, bad.
Ideally, what we'd like is to be able to put a hard ceiling (user configurable) on the amount of RAM used, so that game creators can factor in font altas memory use into their overall budgeting process.
We're currently experimenting with an LRU (least-recently-used) algorithm to cache font altases and drop the ones that haven't been needed in a while.
To assist in the budgeting process, we should also add diagnostic APIs that allow game studios to inspect the amount of font atlas memory being utilized.
## Order of Work
This document has covered a lot of topics, one question is: which should we work on first?
Assuming we can come to a consensus, I would suggest that `ComputedTextStyle` is the most fundamental change, and should probably be worked on first. (A prototype PR with this, and many other changes, already exists.)
Text style inheritance would follow immediately after. Most of the other changes suggested in this document (font aliases, default styles, etc.) can likely be done in parallel with each other.
## Relevant Issues
* [Free Unused Font Aliases](https://github.com/bevyengine/bevy/issues/21210)
* [Val::Em variant](https://github.com/bevyengine/bevy/issues/21209)
* [Val::Rem variant](https://github.com/bevyengine/bevy/issues/21208)
* [Add support to bevy_ui for units relative to font sizes](https://github.com/bevyengine/bevy/issues/21207)
* [Rethinking Font Propagation](https://github.com/bevyengine/bevy/issues/21175)
* [Font Aliases and Font Registration](https://github.com/bevyengine/bevy/issues/20974)
* [Intersperse text and images](https://github.com/bevyengine/bevy/issues/17968)
* [Remove TextSpan](https://github.com/bevyengine/bevy/issues/21226)
* [Support using system fonts](https://github.com/bevyengine/bevy/issues/1325)
* [(All Text-Related tickets)](https://github.com/bevyengine/bevy/issues?q=is%3Aissue%20state%3Aopen%20label%3AA-Text)