Skip to content

Hydration error caused by mismatched id and/or ARIA attributes #3700

@ZeroWave022

Description

@ZeroWave022

Bug report

We get a hydration error from various components that generate their id or aria-controls using React (or your own internal implementation) of the useId hook. The common factor seems to be the usage of the Slot component. We're using the Next.js app router, which is using the latest version of React canary behind the scenes, as described in the documentation:

Good to know: The App Router uses React canary releases built-in, which include all the stable React 19 changes, as well as newer features being validated in frameworks.

The issue is happening starting from Next.js 15.5.0 which seems to use a newer version of React canary. It's also present on the latest version of Next.js.

Downgrading to Next.js 15.4.7 seems to resolve the issue (possibly because of the version using an older version of React).

Could be relevant: React has updated the default prefix of useId in React 19.2

Current Behavior

Hydration error, some examples follow:

Example 1
A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used:

- A server/client branch `if (typeof window !== 'undefined')`.
- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.

It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.

https://react.dev/link/hydration-mismatch

   ...
    <MenuProvider scope={{Menu:[...], ...}} open={false} onOpenChange={function} content={null} ...>
      <MenuProvider scope={{Menu:[...], ...}} onClose={function Menu.useCallback} isUsingKeyboardRef={{current:false}} ...>
        <DropdownMenuTrigger asChild={true}>
          <DropdownMenuTrigger asChild={true}>
            <MenuAnchor asChild={true} __scopeMenu={{Menu:[...], ...}}>
              <PopperAnchor __scopePopper={{Menu:[...], ...}} asChild={true} ref={null}>
                <Primitive.div asChild={true} ref={function}>
                  <Primitive.div.Slot ref={function}>
                    <Primitive.div.SlotClone ref={function}>
                      <Primitive.button type="button" id="radix-_R_6..." aria-haspopup="menu" aria-expanded={false} ...>
                        <Primitive.button.Slot type="button" id="radix-_R_6..." aria-haspopup="menu" ...>
                          <Primitive.button.SlotClone type="button" id="radix-_R_6..." aria-haspopup="menu" ...>
                            <Button className="h-fit" variant="nav" size="none" aria-label="Open navig..." type="button" ...>
                              <button
                                className="cursor-pointer rounded-md ring-offset-background focus-visible:outline-hidd..."
                                ref={function}
                                aria-label="Open navigation menu"
                                type="button"
+                               id="radix-_R_66itmdl5rlb_"
-                               id="radix-_R_166itmdl5rlb_"
                                aria-haspopup="menu"
                                aria-expanded={false}
                                aria-controls={undefined}
                                data-state="closed"
                                data-disabled={undefined}
                                disabled={false}
                                onPointerDown={function handleEvent}
                                onKeyDown={function handleEvent}
                              >
Example 2
A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used:

- A server/client branch `if (typeof window !== 'undefined')`.
- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.

It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.

https://react.dev/link/hydration-mismatch

  ...
    <Header>
      <header className="clamp-[px-...">
        ...
          <div className="flex">
            <MatrixLink isLoggedIn={false} t={{title:"Mat...", ...}} className="xs:flex hi...">
              <Dialog open={false} onOpenChange={function bound dispatchSetState}>
                <Dialog data-slot="dialog" open={false} onOpenChange={function bound dispatchSetState}>
                  <DialogProvider scope={undefined} triggerRef={{current:null}} contentRef={{current:null}} ...>
                    <DialogTrigger asChild={true}>
                      <DialogTrigger data-slot="dialog-tri..." asChild={true}>
                        <Primitive.button type="button" aria-haspopup="dialog" aria-expanded={false} ...>
                          <Primitive.button.Slot type="button" aria-haspopup="dialog" aria-expanded={false} ...>
                            <Primitive.button.SlotClone type="button" aria-haspopup="dialog" aria-expanded={false} ...>
                              <Button className="xs:flex hi..." variant="ghost" size="icon" title="Matrix Log..." ...>
                                <button
                                  className="cursor-pointer rounded-md ring-offset-background transition-colors focus-..."
                                  ref={function}
                                  title="Matrix Login Information"
                                  aria-label="Matrix Login Information"
                                  type="button"
                                  aria-haspopup="dialog"
                                  aria-expanded={false}
+                                 aria-controls="radix-_R_a6itmdl5rlb_"
-                                 aria-controls="radix-_R_q6itmdl5rlb_"
                                  data-state="closed"
                                  data-slot="dialog-trigger"
                                  onClick={function handleEvent}
                                >

Expected behavior

No hydration error caused by mismatching id, aria-controls or other ARIA attributes generated by randomized id.

Your environment

Software Name(s) Version
Radix Package(s) @radix-ui/react-dialog@1.1.15, @radix-ui/react-slot@1.2.3
React n/a 19.2.0
Browser Firefox 144.0, Chrome 141.0.7390.108
Assistive tech
Node n/a 22.14.0
bun 1.3

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions