From 2bf59787e1e6e2ebd7f44e7320c810d6f2c4a5ec Mon Sep 17 00:00:00 2001 From: Farhan Ali Raza Date: Wed, 3 Jun 2026 01:35:42 +0500 Subject: [PATCH 1/3] feat: add rx.google_font for faster font loading from the document head Loading fonts via rx.App(stylesheets=...) chains them behind an @import in the global stylesheet, so the browser only discovers the font after parsing that sheet, delaying first paint. rx.google_font emits preconnect hints and a display=swap stylesheet link for head_components, letting the browser fetch the font during initial HTML parse. Also adds the `as_` prop to el.link so rel="preload" links work. --- docs/styling/custom-stylesheets.md | 47 ++++++++++++++++ .../el/elements/metadata.py | 4 ++ pyi_hashes.json | 4 +- reflex/__init__.py | 2 +- reflex/utils/misc.py | 49 ++++++++++++++++- tests/units/components/el/test_metadata.py | 14 +++++ tests/units/utils/test_misc.py | 53 +++++++++++++++++++ 7 files changed, 168 insertions(+), 5 deletions(-) create mode 100644 tests/units/components/el/test_metadata.py create mode 100644 tests/units/utils/test_misc.py diff --git a/docs/styling/custom-stylesheets.md b/docs/styling/custom-stylesheets.md index 6b050933d2e..fb42fcd7ea6 100644 --- a/docs/styling/custom-stylesheets.md +++ b/docs/styling/custom-stylesheets.md @@ -146,6 +146,34 @@ app = rx.App( ) ``` +### Loading Google Fonts faster + +Stylesheets passed to `stylesheets` are bundled behind an `@import` chain, so the browser only +discovers the font after it has downloaded and parsed the app's global stylesheet. For fonts this +delays first paint. To load a Google Font as early as possible, use `rx.google_font` and add it to +`head_components` instead. It emits `preconnect` hints to the Google Fonts origins and requests the +font with `display=swap` so text paints immediately with a fallback face: + +```python +app = rx.App( + head_components=rx.google_font("JetBrains Mono", weights=[400, 700], italic=True), +) +``` + +`rx.google_font` is a convenience that builds the right `` tags. For a font from any other +provider, add the provider's stylesheet (and an optional `preconnect`) to `head_components` yourself: + +```python +app = rx.App( + head_components=[ + rx.el.link(rel="preconnect", href="https://fonts.bunny.net"), + rx.el.link( + rel="stylesheet", href="https://fonts.bunny.net/css?family=inter:400,700" + ), + ], +) +``` + Then you can use the font in your component by setting the `font_family` prop. ```python demo @@ -188,3 +216,22 @@ app = rx.App( ``` And that's it! You can now use `MyFont` like any other FontFamily to style your components. + +To avoid a flash of unstyled text, preload the font file from the document head so the browser fetches +it in parallel with the page. Fonts are always fetched in CORS mode, so the `cross_origin` is required +even for same-origin files: + +```python +app = rx.App( + head_components=[ + rx.el.link( + rel="preload", + href="/fonts/MyFont.otf", + as_="font", + type="font/otf", + cross_origin="anonymous", + ), + ], + stylesheets=["/fonts/myfont.css"], +) +``` diff --git a/packages/reflex-components-core/src/reflex_components_core/el/elements/metadata.py b/packages/reflex-components-core/src/reflex_components_core/el/elements/metadata.py index f6211538c8f..82471e3eb0f 100644 --- a/packages/reflex-components-core/src/reflex_components_core/el/elements/metadata.py +++ b/packages/reflex-components-core/src/reflex_components_core/el/elements/metadata.py @@ -30,6 +30,10 @@ class Link(VoidBaseHTML): tag = "link" + as_: Var[str] = field( + doc="Specifies the type of content being loaded, required by rel='preload'" + ) + cross_origin: Var[CrossOrigin] = field( doc="Specifies the CORS settings for the linked resource" ) diff --git a/pyi_hashes.json b/pyi_hashes.json index b4498b3581b..a9304920112 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -30,7 +30,7 @@ "packages/reflex-components-core/src/reflex_components_core/el/elements/forms.pyi": "21e51ccc7307c3c41f2556ffa7019f2c", "packages/reflex-components-core/src/reflex_components_core/el/elements/inline.pyi": "9c1432e70e6b9349f44df04a244a4303", "packages/reflex-components-core/src/reflex_components_core/el/elements/media.pyi": "f51120c31a1a8b79da9ecf58f19005b9", - "packages/reflex-components-core/src/reflex_components_core/el/elements/metadata.pyi": "73d19f3d9e389447ad8bbb68e1b7d1c9", + "packages/reflex-components-core/src/reflex_components_core/el/elements/metadata.pyi": "ea3986b62ca17952c1fac8d30e665cde", "packages/reflex-components-core/src/reflex_components_core/el/elements/other.pyi": "c86abf00384b5f15725a0daf2533848d", "packages/reflex-components-core/src/reflex_components_core/el/elements/scripts.pyi": "903432e316a781b342f2b8d334952da1", "packages/reflex-components-core/src/reflex_components_core/el/elements/sectioning.pyi": "fbbe0bf222d4196c32c88d05cb077997", @@ -118,7 +118,7 @@ "packages/reflex-components-recharts/src/reflex_components_recharts/polar.pyi": "1979bb6c22bb7a0d3342b2d63fb19d74", "packages/reflex-components-recharts/src/reflex_components_recharts/recharts.pyi": "c5288f311fe37b23539518ba2a3d4482", "packages/reflex-components-sonner/src/reflex_components_sonner/toast.pyi": "2c5fadcc014056f041cd4d916137d9e7", - "reflex/__init__.pyi": "12a863ddbcac050c702a3ec6092ae17c", + "reflex/__init__.pyi": "d8131b1b8edd52a918052545e83cd54d", "reflex/components/__init__.pyi": "f39a2af77f438fa243c58c965f19d42e", "reflex/experimental/memo.pyi": "5bfbbd60585132d7a76840a0dbacbdd2" } diff --git a/reflex/__init__.py b/reflex/__init__.py index 6e711166871..71e6c18e63b 100644 --- a/reflex/__init__.py +++ b/reflex/__init__.py @@ -233,7 +233,7 @@ "istate.wrappers": ["get_state"], "style": ["Style", "toggle_color_mode"], "utils.imports": ["ImportDict", "ImportVar"], - "utils.misc": ["run_in_thread"], + "utils.misc": ["google_font", "run_in_thread"], "utils.serializers": ["serializer"], "vars": ["Var", "field", "Field", "RestProp", "EMPTY_VAR_STR", "EMPTY_VAR_INT"], } diff --git a/reflex/utils/misc.py b/reflex/utils/misc.py index 67ae79a13ab..ceaacda53b4 100644 --- a/reflex/utils/misc.py +++ b/reflex/utils/misc.py @@ -1,11 +1,11 @@ -"""Miscellaneous functions for the experimental package.""" +"""Miscellaneous utility functions.""" import asyncio import contextlib import inspect import sys import threading -from collections.abc import Callable +from collections.abc import Callable, Sequence from pathlib import Path from typing import Any @@ -131,3 +131,48 @@ def preload_color_theme(): """ return Script.create(script_content) + + +def google_font( + family: str, + *, + weights: Sequence[int] = (400,), + italic: bool = False, + display: str = "swap", +): + """Create the components that load a Google Font from the document head. + + Adding these to ``head_components`` (instead of ``rx.App(stylesheets=...)``, which chains + fonts behind an ``@import`` in the global stylesheet) lets the browser discover the font + during the initial HTML parse and fetch it in parallel, with ``display=swap`` so text paints + immediately using a fallback face:: + + app = rx.App(head_components=rx.google_font("Inter", weights=[400, 700])) + + Args: + family: The font family name, e.g. ``"Open Sans"``. + weights: The font weights to request. + italic: Whether to also request italic styles for each weight. + display: The CSS ``font-display`` strategy. + + Returns: + The preconnect and stylesheet components to add to ``head_components``. + """ + from reflex_components_core.el.elements.metadata import Link + + family_param = family.replace(" ", "+") + sorted_weights = sorted(weights) + if italic: + axis = "ital,wght@" + ";".join( + f"{style},{weight}" for style in (0, 1) for weight in sorted_weights + ) + else: + axis = "wght@" + ";".join(str(weight) for weight in sorted_weights) + href = f"https://fonts.googleapis.com/css2?family={family_param}:{axis}&display={display}" + return [ + Link.create(rel="preconnect", href="https://fonts.googleapis.com"), + Link.create( + rel="preconnect", href="https://fonts.gstatic.com", cross_origin="anonymous" + ), + Link.create(rel="stylesheet", href=href), + ] diff --git a/tests/units/components/el/test_metadata.py b/tests/units/components/el/test_metadata.py new file mode 100644 index 00000000000..1ce3e25c755 --- /dev/null +++ b/tests/units/components/el/test_metadata.py @@ -0,0 +1,14 @@ +from reflex_components_core.el.elements.metadata import Link + + +def test_link_as_prop(): + """The link element renders the as_ prop as the HTML `as` attribute (for rel='preload').""" + props = Link.create( + rel="preload", + href="/fonts/My.woff2", + as_="font", + type="font/woff2", + cross_origin="anonymous", + ).render()["props"] + assert 'as:"font"' in props + assert 'rel:"preload"' in props diff --git a/tests/units/utils/test_misc.py b/tests/units/utils/test_misc.py new file mode 100644 index 00000000000..e1389c5a858 --- /dev/null +++ b/tests/units/utils/test_misc.py @@ -0,0 +1,53 @@ +"""Tests for reflex/utils/misc.py.""" + +from __future__ import annotations + +import reflex as rx +from reflex.utils.misc import google_font + + +def _props(component: rx.Component) -> list[str]: + """Return the rendered prop strings of a component. + + Args: + component: The component to render. + + Returns: + The list of ``name:value`` prop strings. + """ + return component.render()["props"] + + +def test_google_font_exported(): + """google_font is exposed as rx.google_font.""" + assert rx.google_font is google_font + + +def test_google_font_default(): + """google_font returns preconnects plus a swap stylesheet with a wght axis.""" + first, second, sheet = (_props(c) for c in google_font("Inter", weights=[700, 400])) + + assert 'rel:"preconnect"' in first + assert 'href:"https://fonts.googleapis.com"' in first + assert 'rel:"preconnect"' in second + assert 'crossOrigin:"anonymous"' in second + + href = "https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" + assert 'rel:"stylesheet"' in sheet + assert f'href:"{href}"' in sheet + + +def test_google_font_italic_and_spaces(): + """google_font encodes spaces and builds the ital,wght axis when italic=True.""" + sheet = google_font("Open Sans", weights=[400, 700], italic=True)[-1] + href = ( + "https://fonts.googleapis.com/css2?family=Open+Sans:" + "ital,wght@0,400;0,700;1,400;1,700&display=swap" + ) + assert f'href:"{href}"' in _props(sheet) + + +def test_google_font_display_override(): + """google_font respects a custom font-display strategy.""" + sheet = google_font("Roboto", display="optional")[-1] + assert "&display=optional" in "".join(_props(sheet)) From c49b0e58128964313bc346b1a6e2a87e08d0c2f1 Mon Sep 17 00:00:00 2001 From: Farhan Ali Raza <62690310+FarhanAliRaza@users.noreply.github.com> Date: Wed, 3 Jun 2026 01:46:31 +0500 Subject: [PATCH 2/3] Update reflex/utils/misc.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- reflex/utils/misc.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/reflex/utils/misc.py b/reflex/utils/misc.py index ceaacda53b4..3a8e7f09f63 100644 --- a/reflex/utils/misc.py +++ b/reflex/utils/misc.py @@ -160,6 +160,9 @@ def google_font( """ from reflex_components_core.el.elements.metadata import Link + if not weights: + msg = "weights must not be empty" + raise ValueError(msg) family_param = family.replace(" ", "+") sorted_weights = sorted(weights) if italic: From 1f44dbdc28da3d6213527e7b97bfc73099846942 Mon Sep 17 00:00:00 2001 From: Farhan Ali Raza <62690310+FarhanAliRaza@users.noreply.github.com> Date: Wed, 3 Jun 2026 01:46:41 +0500 Subject: [PATCH 3/3] Update reflex/utils/misc.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- reflex/utils/misc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflex/utils/misc.py b/reflex/utils/misc.py index 3a8e7f09f63..cac0b2af214 100644 --- a/reflex/utils/misc.py +++ b/reflex/utils/misc.py @@ -139,7 +139,7 @@ def google_font( weights: Sequence[int] = (400,), italic: bool = False, display: str = "swap", -): +) -> list: """Create the components that load a Google Font from the document head. Adding these to ``head_components`` (instead of ``rx.App(stylesheets=...)``, which chains