mui / mui/base-ui

[progress][meter] SSR hydration mismatch on `aria-valuetext` due to runtime-locale `Intl.NumberFormat`

Open
#4,616 1 comment 0 reactions 0 assignees View on GitHub
component: meter component: progress component: slider
Dominant language
TypeScript
Stars
10.9k
Forks
543
Avg merge
1d 20h
Merged PRs (30d)
101

Description

# Bug report

## Current behavior

`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.


```

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):

```tsx
import { Progress } from '@base-ui/react/progress'

export default function Page() {
return (





)
}
```

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`:

```ts
export function formatNumberValue(value, locale, format) {
if (value == null) return ''
if (!format) {
return formatNumber(value / 100, locale, { style: 'percent' })
}
return formatNumber(value, locale, format)
}
```

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):

1. **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`.
2. **Mount-gated formatting** — render a deterministic fallback on the first render, swap in the localized value after mount (`useEffect`). Non-breaking, one extra render.
3. **`suppressHydrationWarning`** on the root — matches the approach in #4482. Silences the warning but screen readers may still read a mismatched value before hydration completes.

**Consumer workaround** — pass an explicit `locale` to `Progress.Root` / `Meter.Root`:

```tsx

```

This works because both server and client now use the same formatter input.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.