You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Progress.Root and Meter.Root generate aria-valuetext by calling Intl.NumberFormat(locale, { style: 'percent' }) in formatNumberValue. When the locale prop is not passed, the formatter defaults to the runtime locale — which differs between the SSR environment and the client browser. React then reports a hydration mismatch:
Warning: A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.
<div
...
aria-valuenow={30}
+ aria-valuetext="30 %"
- aria-valuetext="30%"
role="progressbar"
>
The server (Bun/Node) renders "30%" while a browser with a locale that uses NBSP before % (e.g. fr-FR, nl-NL) renders "30 %". The two inputs to Intl.NumberFormat are identical — only the default runtime locale differs.
Source: packages/react/src/utils/formatNumber.ts — formatNumberValue(value, locale, format) falls through to Intl.NumberFormat(undefined, { style: 'percent' }) when no locale or format is provided.
The same formattedValue is also exposed via ProgressRootContext / MeterRootContext to Progress.Value / Meter.Value, so the visible text can mismatch too when those subcomponents are used.
Expected behavior
Progress.Root and Meter.Root should render the same aria-valuetext on the server and the client out of the box — either by producing a locale-independent default, or by deferring locale-sensitive formatting until after hydration.
Reproducible example
Minimal repro (any SSR framework — reproduced against TanStack Start on Bun, same outcome on Next.js App Router):
Trigger by running the server with a locale that formats percents without NBSP (e.g. LANG=C or en-US) and opening the page in a browser with one that does (e.g. fr-FR, nl-NL, de-DE). Console shows the hydration mismatch on aria-valuetext.
Base UI version
v1.3.0
Which browser are you using?
Chrome (reproduces on any browser whose locale differs from the server's default locale)
Which OS are you using?
macOS (server); any OS on the client
Which assistive tech are you using (if applicable)?
N/A — bug surfaces as a React hydration warning; screen reader impact is a brief mismatch until React discards the server tree.
Additional context
Root cause — packages/react/src/utils/formatNumber.ts:
When neither locale nor format is passed by the consumer, Intl.NumberFormat uses the runtime default locale. This is non-deterministic across SSR boundaries.
Slider likely has the same problem via the same utility, though I haven't reproduced it — flagging for triage.
Possible fixes (happy to send a PR once the preferred direction is decided):
Locale-independent default — when locale and format are both unset, skip Intl.NumberFormat and let the existing getDefaultAriaValueText fallback (${value}%) kick in. Breaking for consumers relying on localized Progress.Value output without passing locale.
Mount-gated formatting — render a deterministic fallback on the first render, swap in the localized value after mount (useEffect). Non-breaking, one extra render.
Bug report
Current behavior
Progress.RootandMeter.Rootgeneratearia-valuetextby callingIntl.NumberFormat(locale, { style: 'percent' })informatNumberValue. When thelocaleprop is not passed, the formatter defaults to the runtime locale — which differs between the SSR environment and the client browser. React then reports a hydration mismatch:The server (Bun/Node) renders
"30%"while a browser with a locale that uses NBSP before%(e.g.fr-FR,nl-NL) renders"30 %". The two inputs toIntl.NumberFormatare identical — only the default runtime locale differs.Source:
packages/react/src/utils/formatNumber.ts—formatNumberValue(value, locale, format)falls through toIntl.NumberFormat(undefined, { style: 'percent' })when nolocaleorformatis provided.The same
formattedValueis also exposed viaProgressRootContext/MeterRootContexttoProgress.Value/Meter.Value, so the visible text can mismatch too when those subcomponents are used.Expected behavior
Progress.RootandMeter.Rootshould render the samearia-valuetexton the server and the client out of the box — either by producing a locale-independent default, or by deferring locale-sensitive formatting until after hydration.Reproducible example
Minimal repro (any SSR framework — reproduced against TanStack Start on Bun, same outcome on Next.js App Router):
Trigger by running the server with a locale that formats percents without NBSP (e.g.
LANG=Coren-US) and opening the page in a browser with one that does (e.g.fr-FR,nl-NL,de-DE). Console shows the hydration mismatch onaria-valuetext.Base UI version
v1.3.0
Which browser are you using?
Chrome (reproduces on any browser whose locale differs from the server's default locale)
Which OS are you using?
macOS (server); any OS on the client
Which assistive tech are you using (if applicable)?
N/A — bug surfaces as a React hydration warning; screen reader impact is a brief mismatch until React discards the server tree.
Additional context
Root cause —
packages/react/src/utils/formatNumber.ts:When neither
localenorformatis passed by the consumer,Intl.NumberFormatuses the runtime default locale. This is non-deterministic across SSR boundaries.Sliderlikely has the same problem via the same utility, though I haven't reproduced it — flagging for triage.Possible fixes (happy to send a PR once the preferred direction is decided):
localeandformatare both unset, skipIntl.NumberFormatand let the existinggetDefaultAriaValueTextfallback (${value}%) kick in. Breaking for consumers relying on localizedProgress.Valueoutput without passinglocale.useEffect). Non-breaking, one extra render.suppressHydrationWarningon the root — matches the approach in [switch][checkbox][radio][select][combobox] Add suppressHydrationWarning to hidden inputs #4482. Silences the warning but screen readers may still read a mismatched value before hydration completes.Consumer workaround — pass an explicit
localetoProgress.Root/Meter.Root:This works because both server and client now use the same formatter input.