Repository navigation
DirectWrite text layout cannot resolve font families whose names contain spaces — 'Material Icons' renders tofu, same font renamed 'MaterialIcons' renders #16306
Description
Problem Description
On the new architecture, a fontFamily whose name contains a space (e.g. 'Material Icons') fails to resolve even though the font is registered and enumerable in DirectWrite — glyphs render as tofu / fall back to Segoe UI. The identical TTF re-emitted with a space-less family name ('MaterialIcons', name table IDs 1/16 rewritten, glyph outlines untouched) resolves and renders. The space in the requested family name is the only variable.
What we verified empirically (production app):
- Fonts registered via the packaged
windows.sharedFontsmanifest extension land in DirectWrite's per-user system font collection; WPF/DirectWrite enumeration shows the family present under its spaced name (Material Icons), correctcmap/namerecords. fontFamily: 'Material Icons'→ tofu.- Same file, family renamed to
MaterialIcons(only the OpenTypenametable changed) → renders correctly.
Root cause — diagnosed, NOT traced to a specific line (filing for visibility; confidence level stated honestly). The resolution lives in the DirectWrite text-layout path — vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/textlayoutmanager/WindowsTextLayoutManager and/or the font-collection/loader it uses — i.e. how the requested family string reaches CreateTextFormat / FindFamilyName. Plain DirectWrite handles spaced family names fine (Segoe UI is the platform default), so we suspect RNW-side handling of the fontFamily string (tokenizing/trimming, or the custom collection lookup) rather than DirectWrite itself. We could not pin the failing lookup with confidence from a shallow source read, so we did not attempt a patch.
Steps To Reproduce
- Take a TTF whose family name contains a space — e.g. stock
MaterialIcons.ttffromreact-native-vector-icons(family nameMaterial Icons). - Register it so DirectWrite can see it (we use the
windows.sharedFontspackage-manifest extension; confirm via any DirectWrite/WPF font enumeration that the family is present). - Render
<Text style={{ fontFamily: 'Material Icons' }}>{''}</Text>(any mapped codepoint). - Observe tofu / Segoe UI fallback.
- Control: rewrite the font's
nametable so the family isMaterialIcons(no other change), requestfontFamily: 'MaterialIcons'→ glyphs render.
Expected Results
Spaced family names resolve exactly as space-less ones do (and as they do on iOS/Android, and in DirectWrite generally).
CLI version
18.0.0
Environment
System:
OS: Windows 11 10.0.26200 (ARM64 device; app builds and runs ARM64)
CPU: (6) x64 Apple Silicon (Windows-on-ARM)
Memory: 6.49 GB / 15.99 GB
Binaries:
Node: 22.15.0
Yarn: 4.5.1
npm: 10.9.2
SDKs:
Windows SDK versions: 10.0.19041.0, 10.0.22621.0, 10.0.26100.0
IDEs:
Visual Studio: 18.6.11822.322 (Community 2026), 17.14.37314.3 (Community 2022)
npmPackages (yarn 4 workspaces — `cli info` reports Not Found in-workspace):
react-native: 0.83.2
react-native-windows: 0.83.2 (New Architecture / Fabric composition)
Microsoft.WindowsAppSDK: 1.8Community Modules
react-native-vector-icons 10.3.0 / @expo/vector-icons 15.1.1 are where the spaced-name fonts come from, but the bug is core: it reproduces with any registered TTF whose family name contains a space, rendered through core <Text>.
Target React Native Architecture
New Architecture (WinAppSDK) Only
Target Platform Version
10.0.22621
Visual Studio Version
Visual Studio 2026
Build Configuration
Debug
Snack, code example, screenshot, or link to a repository
Minimal probe is the two-step control above (steps 3 vs 5) — same file, same glyphs, only the family name differs.
Our production workaround: a dependency-free Node script re-emits the Windows copies of all affected fonts with space-less family names (rewriting name IDs 1/16 and recomputing table checksums), registered via windows.sharedFonts; a platform shim makes JS request the space-less names on Windows only.
Related issues: #15316 (umbrella: custom .ttf fontFamily support on new arch), #3463 (old-arch custom-font parity), #15750 (IProvideFontInfo font-loading unification). Related but distinct, filed separately: #16308 — registered stock icon TTFs render blank glyphs until table checksums/table directory are recomputed (loader validation vs. this issue's family-name lookup).
Context: found during a Windows hardening pass of a production RNW app (Facilitron FIT — RNW 0.83.2 new-arch, Windows 11 ARM64, WinAppSDK 1.8, 250% display scale). Sibling PRs from the same investigation: #16302, #16303, #16304. Happy to build a minimal repro repo or test candidate fixes in our app if useful.
Activity
FaithfulAudio commentedon Jul 26, 2026
Reporter here, with a correction and a fix.
My diagnosis in this issue is wrong. Spaces are not the problem. I built a DirectWrite probe that
replays RNW's exact call sequence and measured it. Sorry for the noise — here is what is actually
happening, and a PR.
Measured: spaces in family names are fine
Standalone probe, Windows 11 10.0.26200, VS 2022 x64,
DWRITE_FACTORY_TYPE_SHARED. It replaysCreateTextFormat(with the same emptylocaleNameRNW passes) →CreateTextLayout→Drawwith arecording
IDWriteTextRendererthat reports which face and which glyph index the run resolved to.Holding the collection and the font constant (stock
MaterialIcons.ttffromreact-native-vector-icons@10.3.0, in a collection that contains it) and changing only therequested string, drawing U+E7FD:
fontFamilyFindFamilyNameMaterial IconsMaterial Icons, glyph 782material iconsMaterial Icons, glyph 782MATERIAL ICONSMaterial Icons, glyph 782MaterialIcons" Material Icons"(leading space)"Material Icons "(trailing space)Material Icons(doubled interior space)The spaced name is the one that works. The space-less name fails for the mundane reason that
MaterialIconsis not this font's family name — its name table saysMaterial Icons. Lookup iscase-insensitive. Interior spaces are irrelevant.
So the control experiment in my issue body was confounded: the rename script rewrote name IDs 1/16 and
re-emitted the file and the new file had to be re-registered. I attributed the change to the space.
It wasn't the space.
The actual root cause
WindowsTextLayoutManager::GetTextLayoutpassesnullptras the font collection(
WindowsTextLayoutManager.cppL119):nullptrmeans system-collection-only, so no app-bundled font family resolves at all — everycodepoint falls back to Segoe UI glyph 0 (
.notdef). Measured for all eight stockreact-native-vector-iconsfonts: with a collection containing the font they draw real glyph indices(40, 1, 1, 13, 4, 4, 4, 2); with
nullptrevery single one gives Segoe UI glyph 0.There is no font-family parsing bug in RNW to find, either. I also checked that: the string goes
std::string→Utf8ToUtf16→.c_str()→ DirectWrite, verbatim, with the sole transformation being"if empty, use
Segoe UI". No tokenizer, no comma splitting, no trimming. And atc69cf55f67f9b03f467502dac1007ac2d9ebe209the repo contains no font loader or font-collection code atall —
FindFamilyName,CreateFontFileReference,IDWriteFontCollection,IDWriteFontSetBuilder,AddFontResourceandsharedFontshave zero hits, and the git tree (3956 blobs) has no source filewith "Font" in its name.
Fix
Fix in #16339: add
DWriteAppFontCollection()toDWriteHelpers— system font set merged with every*.ttf/*.otfunder the app'sAssets\andAssets\Fonts\, built once, failing closed tonullptrso behaviour is unchanged for apps that bundle no fonts — and pass it at that
CreateTextFormatcallinstead of
nullptr. The same PR (#16339) fixes #16308, which has the same root cause.What I could not verify, stated plainly
nullptrrows there fail because thefont is genuinely absent. I did not reproduce the
windows.sharedFonts-registered state from myoriginal report. What I have shown is that neither spaces nor the family-name string shape is the
mechanism, and that
nullptrcannot resolve an uninstalled font. My best explanation of the originalobservation is that the re-emit forced a re-registration that changed collection membership, and the
rename rode along — but that is inference, not measurement.
probe is separate standalone code.
One genuinely new, small finding
DirectWrite does not normalise leading, trailing or duplicated whitespace in a family name, and RNW
does not trim the string before passing it on.
fontFamily: 'Ionicons 'silently renders.notdefwith no diagnostic. Not fixed in the PR — whether RN should trim is arguable and I did not want to
smuggle it in. Flagging it because it is an easy trap.
Suggested retitle
"App-bundled font families never resolve: WindowsTextLayoutManager passes nullptr as the DirectWrite
font collection" — the current title is misleading and will send the next person hunting for a string
bug that does not exist.
Note on the workaround in my original report
Our build-time script that renames families space-less and recomputes checksums does work, but the
reason it works is not the reason I gave. Anyone copying it should read the PR first; with the
collection fix you should not need it.