type
export type Loading = "eager" | "lazy";
How an image should be fetched relative to the rest of the page.
type
export type Source = {
readonly srcSet: string,
readonly type?: string,
readonly media?: string,
};
One alternative rendering of the same image.
type
export type ImageAsset = {
/** The widest variant in the fallback format — what `src` points at. */
readonly src: string | null,
/** The source's intrinsic width, or `null` when uf could not decode it. */
readonly width: number | null,
/** The source's intrinsic height. */
readonly height: number | null,
/** Every fallback-format variant, as a `srcset` string. */
readonly srcSet: string,
/** Modern formats that came out smaller, widest-preferred first. */
readonly sources: $ReadOnlyArray<Source>,
/**
* A tiny inline image to show until the real one lands.
*
* `null` for an image with transparency: the placeholder is painted under
* the image, so under one with holes in it it never goes away.
*/
readonly blurDataURL: string | null,
/** Why nothing was resized, when nothing was. */
readonly note: string | null,
};
What importing an image evaluates to.
Produced by uf:asset; see packages/vite/internal/assets.js. Every field is a fact about files that exist on disk, which is the difference between this and a set of props.
type
export type FontFace = {
/** Its URL. */
readonly url: string,
/** Its media type. */
readonly mime: string,
/** Its size in bytes. */
readonly bytes: number,
/** Which script bucket it is, when the family was split. */
readonly bucket: string | null,
/** The exact `unicode-range` of what is in it. */
readonly unicodeRange: string | null,
/**
* Whether a page should preload this one.
*
* At most one face is ever marked. A preload per bucket downloads the whole
* family up front, which is the one thing a `unicode-range` split exists to
* stop.
*/
readonly preload: boolean,
};
One file a font import put in the build.
type
export type FontAsset = {
/** The primary self-hosted file's URL. */
readonly src: string,
/** The family the `@font-face` declares. */
readonly family: string,
/** The metric-matched fallback's family, when there is one. */
readonly fallbackFamily: string | null,
/** The whole stack, ready for `font-family`. */
readonly fontFamily: string,
/** The file's media type, for the preload. */
readonly type: string,
/** The `@font-face` rules: every real face, then the matched fallback. */
readonly css: string,
/** Every emitted file, one per bucket when the family was split. */
readonly faces?: $ReadOnlyArray<FontFace>,
/** Why the whole font was hosted, when a subset was asked for and refused. */
readonly subsetDeclined?: string | null,
};
What importing a font evaluates to.
Produced by uf:asset from the font's own head, hhea and OS/2 tables.
type
export type OgAsset = {
/** The card's URL, relative to the site. */
readonly url: string,
/** Its width in pixels. */
readonly width: number,
/** Its height in pixels. */
readonly height: number,
/** Its media type. */
readonly type: string,
/** The `og:image:alt` text. */
readonly alt: string,
};
What importing an Open Graph template evaluates to.
Produced by uf:asset from a *.og.json, which is a declared template and not a document — see crates/uf_assets/src/og.rs for what uf will and will not draw.
type
export type IconAsset = {
/** The symbol's id in the sprite. */
readonly id: string,
/** The fragment a `<use>` points at. */
readonly href: string,
/** The symbol's `viewBox`. */
readonly viewBox: string,
/** Its intrinsic width. */
readonly width: number,
/** Its intrinsic height. */
readonly height: number,
};
What importing uf:icon/<name> evaluates to.
type
export type SpriteAsset = {
/** The `<svg>` holding one `<symbol>` per icon the build reached. */
readonly markup: string,
};
What importing uf:icon-sprite evaluates to.
component
export component Image(
src: string | ImageAsset,
alt: string,
width?: number,
height?: number,
loading?: Loading,
priority?: boolean = false,
sizes?: string,
srcSet?: string,
placeholder?: boolean = true,
quality?: number,
unoptimized?: boolean = false,
className?: string,
style?: { readonly [string]: string | number },
...rest: { readonly [string]: mixed }
) renders React.Node { ... }
An <img> that does not shift the page while it loads.
width and height are the *intrinsic* dimensions, not a size — CSS still decides how big it looks, and the ratio is what stops the shift. A browser that knows an image's intrinsic ratio reserves the space before the bytes arrive; one that does not reflows everything below when they do.
They come from the import when src is an imported image, because uf decoded the file and knows them. They are required when src is a string, because nothing else does — and a missing dimension throws rather than rendering an <img> that will move the page, which is the mistake this component exists to prevent and is not worth failing quietly at.
loading="lazy" and decoding="async" by default, because most images on a page are below the fold and decoding on the main thread blocks it. The one image that *is* above the fold should say priority, which makes it eager, gives it a high fetch priority, and preloads it — a srcSet behind a lazy <img> is discovered late, and for the largest image on the page that is usually the whole of the Largest Contentful Paint.
A remote src — an absolute http(s) URL — is sent through /__uf/image when the project lists remote hosts in app.builtins.images.remotePatterns: src and srcSet then name the endpoint, one rung per width it accepts, at quality or the project's own. width and height are still required, because the endpoint is asked for the image only after the page has laid it out. A srcSet of the author's own, or unoptimized, leaves the URL as it was written.
component
export component Picture(
src: string,
alt: string,
width: number,
height: number,
sources?: $ReadOnlyArray<Source> = [],
loading?: Loading = "lazy",
className?: string,
) renders React.Node { ... }
A <picture>: the same image, in formats a browser may prefer.
For sources a project assembled itself. An imported image needs none of this — Image renders the <picture> when the pipeline produced an alternative format, and skips it when it did not, which is the difference between offering a format and offering a format that is smaller.
The sources are offered in order and the browser takes the first it understands, so put the format you most want served first — avif, then webp, then whatever src is. The <img> is the fallback and is not optional: a <picture> with no <img> renders nothing at all, which is a mistake that only shows up in the one browser that took none of the sources.
component
export component Font(
src: string | FontAsset,
type?: string,
crossOrigin?: "anonymous" | "use-credentials" = "anonymous",
preload?: boolean,
) renders React.Node { ... }
Self-host a font, declare it, and preload it.
# The preload
A font referenced only from CSS is discovered late — the browser has to fetch and parse the stylesheet first — so the first paint uses a fallback and the text reflows when the real face lands. A preload moves the fetch to the start of the page.
It is also the thing most easily overdone, so exactly one file is ever preloaded: the build marks the face a page paints first, and a family split into eight unicode-range buckets still contributes one link. Preloading every bucket downloads the whole family up front, which is what the split existed to stop, and four preloaded faces have pushed the page's own stylesheet down the same connection. Pass preload={false} for a face that does not paint the first screen, and preload={true} for one the project's app.builtins.fonts.preload turned off but this page needs — the prop wins over the build in both directions, and one link is still the most it emits.
crossOrigin is set unconditionally and deliberately: a font is fetched in CORS mode whatever its origin, so a preload without it is a *second*, separate request rather than the same one — the preload is wasted and the font still arrives late. This is the single most common way to get a font preload wrong, and it fails silently.
# The @font-face, and the one that stops the reflow
Given an imported font, this also emits the rules that declare it. There are two: the real face, and a second face over a font the reader already has, carrying the size-adjust, ascent-override, descent-override and line-gap-override uf computed from the real font's own metrics. Scale the fallback until it occupies the same space and the swap moves nothing — which is the half of font-display: swap that everybody wants and nobody gets by default.
The rules only apply to text that asks for them, so use fontFamily from the import: it names the real face, then the matched fallback, then the local face it was scaled from, in that order. Naming only the real family gets the download and none of the metric matching.
# One weight per import
The generated face is font-weight: 400; font-style: normal, and the family is the file's stem: Inter-Bold.woff2 is the family Inter-Bold, not Inter at 700, so font-weight: bold will not reach it. Render it under its own fontFamily instead. A variable font is one file covering the range and is unaffected. uf assets accepts weight and style already; what does not exist yet is a way for an import to say them.
Write it beside the text that needs the font, not at the root of the page: React hoists both <link rel="preload"> and <style> into <head> itself, and dedupes them, so the component that depends on a face is the one that asks for it and two components asking for the same face still make one request.
component
export component IconSprite(sprite: SpriteAsset) renders React.Node { ... }
The sprite holding every icon this build reached.
Render it once, near the top of the document. It is a definitions block and not content: it paints nothing, it is aria-hidden, and every <Icon> on the page is a forty-byte <use> pointing into it.
The alternative — a component per icon, each carrying its own path data — repeats the same geometry once per use, and a runtime icon library ships every icon it has because at runtime nothing knows which ones the application imported. A build does, which is the whole reason this exists.
The markup comes from uf assets, which parses each file, refuses the constructs that must not be inlined into a document — <script>, <foreignObject>, on… handlers, javascript:, anything fetching from another origin — and namespaces every internal id so two icons defining the same gradient do not collide. dangerouslySetInnerHTML is the only way to put an already-serialised subtree into the DOM, and what makes it safe here is that the string was produced by uf from files in the repository, not by anything at runtime.
component
export component Icon(
icon: IconAsset,
label?: string,
size?: number = 24,
width?: number,
height?: number,
className?: string,
...rest: { readonly [string]: mixed }
) renders React.Node { ... }
One icon out of the sprite.
<Icon icon={star} label="Favourite" /> — with a label when the icon *is* the control, and without one when there is text beside it. That is the whole accessibility decision an icon needs and the one people most often get backwards: an unlabelled icon button is a button a screen reader announces as "button", and a labelled icon next to its own visible text is the same word read twice.
size sets both dimensions, because an icon whose aspect ratio is not its viewBox's is a squashed icon. Pass width and height separately only when that is what you mean.
component
export component OgImage(card: OgAsset, origin: string, alt?: string) renders React.Node { ... }
The <meta> tags for one Open Graph card.
<OgImage card={card} /> beside the page that owns it. Three things go wrong with an Open Graph image and this gets all three right: the URL has to be **absolute** — a relative og:image is not an Open Graph image at all, and every crawler drops it — the dimensions have to be declared or the card flickers at whatever size the crawler guesses, and twitter:card has to say summary_large_image or X renders a thumbnail of a 1200x630 picture.
origin is the site's own, and it is required for exactly the reason above. A project using @uniflowed/router's Metadata already has it as metadataBase and should pass card through openGraph.images instead of rendering this — one page's metadata belongs in one place. This component is for a page assembling its own head.
The card itself is drawn at build time by uf assets from a *.og.json template. It is a template and not a renderer: uf will not turn JSX into an image, and it refuses text it cannot lay out rather than drawing it wrong.
@uniflowed/web/time
type
TimeValue
export type TimeValue = Instant | Date | string | number;
Whatever a caller has an instant written as.
type
export type TimeFormat =
/** `2026-09-04T06:00:00Z`. Unambiguous, and the same on every machine. */
| "iso"
/** `2026-09-04`. The calendar date in the render's zone. */
| "date"
/** `2026-09-04 15:00 +09:00`. The wall clock in the render's zone. */
| "zoned"
/** The reader's own locale and zone, applied after hydration. */
| "local"
/** "3 minutes ago", relative to now, after hydration. */
| "relative";
What to show. Everything but local and relative is the same everywhere.
function
asInstant
export function asInstant(value: TimeValue): Instant { ... }
Whatever the caller passed, as a Temporal.Instant.
A string must carry an offset, because Temporal refuses one that does not and this component must not be more permissive than the standard it is built on: "2026-09-04" is a date and not an instant, and every implementation that has guessed which midnight it meant has guessed differently.
function
relative
export function relative(at: TimeValue, from?: TimeValue): string { ... }
"3 minutes ago", or "in 3 minutes".
from defaults to the injected clock rather than to new Date(), so a test can decide what "now" is and a server render is reproducible. Both arguments take anything Time takes.
component
Time
export component Time(
value: TimeValue,
format?: TimeFormat = "iso",
zone?: string,
locale?: string,
className?: string,
) renders React.Node { ... }
An instant, as a <time> element.
The machine-readable value is always in dateTime, whatever the text says, so a crawler and a screen reader get the exact instant even when a reader sees "3 minutes ago".
iso, date and zoned render the same string on both sides and never change. local and relative render the deterministic form first and replace it after hydration — the text is there for the first paint and for anything that does not run JavaScript, and it becomes the reader's own format once it can.
zone is the zone the deterministic forms are written in. It defaults to the one the render fixed, so a page under a RenderProvider shows its server's zone until it can show the reader's; with no provider it is UTC, because a default that reads the host would be a different string on each side and would defeat the entire component.
@uniflowed/web/vitals
type
VitalName
export type VitalName = "TTFB" | "FCP" | "LCP" | "CLS" | "INP";
The metrics this module reports.
type
Rating
export type Rating = "good" | "needs-improvement" | "poor";
Where a value falls against the thresholds the metric is published with.
type
NavigationType
export type NavigationType = "navigate" | "reload" | "back-forward" | "prerender" | "unknown";
How the page was reached.
Carried on every metric because it changes what the metric means: a reload has a warm cache, a back-forward restore has a warm everything, and a prerender was fetched before anybody asked for it. Averaging the four together is how a fast site measures slow.
type
Vital
export type Vital = {
readonly name: VitalName,
/** Milliseconds, except `CLS`, which is a unitless layout-shift score. */
readonly value: number,
readonly rating: Rating,
readonly navigationType: NavigationType,
};
One measurement, final.
type
VitalsReporter
export type VitalsReporter = (vital: Vital) => void;
What a collector hands each finished measurement to.
type
VitalsReport
export type VitalsReport = {
readonly url: string,
readonly vitals: $ReadOnlyArray<Vital>,
};
The body posted to [VITALS_ENDPOINT].
A receiver gets more than one of these per page load — see vitalsBeacon — so the arrays are additive rather than a complete picture, and url identifies the page rather than the report.
type
CollectOptions
export type CollectOptions = {
readonly report: VitalsReporter,
};
How to collect.
variable
VITALS_ENDPOINT
export const VITALS_ENDPOINT: string = "/__uf/vitals";
The path vitalsBeacon posts to unless the project names another.
Under /__uf/ beside the update stream @uniflowed/hmr opens, for the same reason: it is a namespace the dev server owns and an application cannot collide with — a directory in app/ whose name begins with _ is not a route, so this path is unreachable by anything a project writes.
Which is also why it is only half of the answer. It is where a page being developed reports to, and in production the project passes a path of its own to vitalsBeacon and serves it from an ordinary route handler. The contract that makes both work is a path and a [VitalsReport] body, rather than an integration with anybody.
function
collectVitals
export function collectVitals(options: CollectOptions): () => void { ... }
Measure this page and report each metric once, as it becomes final.
Returns a function that stops collecting. Calling it does not report anything: stopping is not the page ending, and a collector that flushed on teardown would report a half-measured page every time a React root remounted.
That is also what makes this safe under StrictMode, where an effect is set up, torn down and set up again. Nothing here reports from the call itself: observer callbacks are delivered in a later task, the page-visibility listeners fire later still, and the one value that *could* be read at once — TTFB, off the navigation entry — is deferred by a microtask for exactly this reason. So the first collector is stopped before any of them runs, reports nothing at all, and the second collector, replaying the same buffered entries, reports each metric exactly once. uf dev mounts every application under Strict Mode since ubugeeei-prod/uf#516, so this is the ordinary case rather than a corner of it.
On a server this observes nothing and returns immediately, so a component that renders in both places can call it unconditionally.
function
useVitals
export function useVitals(report: VitalsReporter): void { ... }
Measure this page for as long as the component is mounted.
Belongs in a root layout, which is late — the page has usually painted by the time React commits. That is correct rather than tolerated: every observer asks for buffered entries, so the browser replays what it recorded before this ran, and the first paint of a server-rendered page is measured by a component that did not exist when it happened.
report is read through a ref so that an inline arrow does not tear the collector down and build a new one on every render. Restarting is not harmless: the replayed entries would be reported a second time.
function
vitalsBeacon
export function vitalsBeacon(endpoint?: string): VitalsReporter { ... }
A reporter that posts to endpoint on the page's own origin.
Defaults to [VITALS_ENDPOINT]. Nothing is sent until a metric is final, no connection is opened before that, and the destination is a path rather than a URL, so this cannot be pointed at somebody else's server by accident.
# Why more than one request
Metrics become final at different moments — TTFB and FCP early, LCP, CLS and INP when the page is put away — and the sends are coalesced across a microtask rather than held until the end. So a receiver normally sees two reports and must treat them as additive.
Holding everything until the page is hidden would have been one request, and it would have made the whole thing depend on which listener the browser called first: the collector's, which produces the last three metrics, or this one's, which sends them. That ordering is not something either side can state, so it is not something the design relies on. It also costs nothing to give up — a receiver that prints a number as soon as it is known is more useful than one that waits for a tab to close.