Skip to content

React 19 Support

Greg Douglas edited this page Jun 9, 2026 · 4 revisions

How to use Blueprint v6 with React 19, and how to migrate off the two components that don't support it.

Can I use Blueprint v6 with React 19?

Yes. Blueprint v6 supports React 19. For the cleanest install experience, use Blueprint v6.16.0 or newer, where the published@blueprintjs/* peer dependency ranges include React 19.

There is one application-code requirement: two deprecated components don't work under React 19 and must be replaced before they are removed in v7.

  • Legacy Popover → replace with PopoverNext
  • Legacy Overlay → replace with Overlay2

Everything else in Blueprint already works on React 19. An application that renders neither component directly needs only the install step below.

Only code that imports Popover or Overlay from @blueprintjs/core and renders them directly is affected. The fastest way to find every usage is to lint for it; see Finding usages.

Installing under React 19

Use Blueprint v6.16.0 or newer and upgrade any other @blueprintjs/* packages to the same version range.

npm install @blueprintjs/core@^6.16.0

Note

Prior to v6.16.0, the published v6 peer dependency ranges only listed React 18. Installing those older versions under React 19 can surface a peer-dependency conflict. How it shows up depends on the package manager: npm fails the install with ERESOLVE, while Yarn and pnpm only print a warning and install anyway.

Installing before the peer-dependency bump (or on an older v6 minor)

While @blueprintjs/* still asks for react@18, installing under React 19 surfaces a peer-dependency conflict. How it shows up depends on the package manager: npm fails the install with ERESOLVE, while Yarn and pnpm only print a warning and install anyway.

npm. Overrides don't relax a declared peer range, so tell npm to proceed past the conflict:

npm install --legacy-peer-deps

Optionally pin the React types so the whole tree resolves to v19, in package.json:

{
    "overrides": {
        "@types/react": "^19.0.0",
        "@types/react-dom": "^19.0.0"
    }
}

Yarn. The peer mismatch is only a warning, so the install already completes. To dedupe the React types to v19 (Yarn Classic), in package.json:

{
    "resolutions": {
        "@blueprintjs/core/@types/react": "^19.0.0",
        "@blueprintjs/core/@types/react-dom": "^19.0.0"
    }
}

On Yarn Berry, packageExtensions in .yarnrc.yml can amend Blueprint's declared peer range and silence the warning.

pnpm. The peer mismatch is a warning. To silence it and pin the React types, in pnpm-workspace.yaml:

overrides:
    "@types/react": "^19.0.0"
    "@types/react-dom": "^19.0.0"
peerDependencyRules:
    allowedVersions:
        react: "19"
        react-dom: "19"

(On pnpm 9–10 these settings live under a "pnpm" key in package.json instead.)

Remove these once Blueprint's peer range includes React 19.

The application's own react, react-dom, @types/react, and @types/react-dom must also be upgraded to 19, following React's upgrade guide. Blueprint's types are compatible with @types/react@19, but the app-side React upgrade is a separate, application-owned step.

What works, and what doesn't

In short: everything in Blueprint works on React 19 except two deprecated components rendered directly by application code.

Component Works under React 19? Replacement
Popover No PopoverNext
Overlay No Overlay2
Everything else Yes

Both blockers fail for the same reason: they reach ReactDOM.findDOMNode, which React 19 removed. Legacy Popover depends on react-popper, which calls it internally; legacy Overlay drives a react-transition-group CSSTransition without a nodeRef, which falls back to the same call. In both cases, rendering the component throws at runtime with no prior warning.

These commonly used components and APIs already avoid the React 19 incompatibilities and need no migration:

  • Dialog, Drawer, and Alert are built on Overlay2.
  • Select, MultiSelect, Suggest, Tooltip, ContextMenu, and DateInput are built on PopoverNext.
  • The imperative toaster (OverlayToaster.create()) and the singleton ContextMenu API render with createRoot rather than the removed ReactDOM.render.

Only direct use of Popover or Overlay requires migration.

Finding usages

Both components carry an @deprecated annotation, so editors and the @typescript-eslint/no-deprecated rule flag every usage:

// eslint.config.js
import tseslint from "typescript-eslint";

export default tseslint.config({
    rules: { "@typescript-eslint/no-deprecated": "error" },
});

@blueprintjs/eslint-plugin adds coverage for Blueprint-specific deprecations through its recommended config (blueprint.flatConfigs.recommended for flat config, or extends: ["plugin:@blueprintjs/recommended"] for the legacy format). Running the linter surfaces remaining usages.

Migrating PopoverPopoverNext

PopoverNext is the modern equivalent of Popover, built on Floating UI instead of Popper.js. Its API is close to the legacy component's, but the positioning props changed with the engine, and a few defaults moved (see migration guide).

The common case: rendering <Popover> directly

1. Swap the import and the tag.

// Before
import { Popover, type PopoverProps } from "@blueprintjs/core";

// After
import { PopoverNext, type PopoverNextProps } from "@blueprintjs/core";

2. Update the props that changed.

Legacy prop PopoverNext What to do
modifiers middleware Popper modifiers became Floating UI middleware. Convert with popperModifiersToNextMiddleware() or rewrite by hand.
modifiersCustom No direct equivalent. Fold any custom modifiers into the middleware prop.
position placement Rename. position="auto" (and the old default) becomes no placement prop, which selects Floating UI's auto-placement. Convert with popoverPositionToNextPlacement().
minimal={true} animation="minimal" + arrow={false} The single minimal flag split into an animation style and explicit arrow control.
boundary="clippingParents" boundary="clippingAncestors" Value renamed. An Element or Element[] boundary passes through unchanged.
shouldReturnFocusOnClose same prop, default changed The default flipped from false to true. To keep the old behavior, set it explicitly: shouldReturnFocusOnClose={false}.

Type names moved alongside the props: Placement/PopoverPositionPopoverNextPlacement, and BoundaryPopoverNextBoundary.

A worked example:

// Before
import { Popover, PopoverPosition } from "@blueprintjs/core";

<Popover
    position={PopoverPosition.BOTTOM_LEFT}
    minimal={true}
    boundary="clippingParents"
    modifiers={{ offset: { options: { offset: [0, 8] } } }}
>
    <Button text="Open" />
    {content}
</Popover>;
// After
import { PopoverNext } from "@blueprintjs/core";

<PopoverNext
    placement="bottom-start"
    animation="minimal"
    arrow={false}
    boundary="clippingAncestors"
    middleware={{ offset: { mainAxis: 8 } }}
    shouldReturnFocusOnClose={false}
>
    <Button text="Open" />
    {content}
</PopoverNext>;

The migration helpers perform these conversions automatically:

import {
    PopoverNext,
    popoverPositionToNextPlacement,
    popperModifiersToNextMiddleware,
    PopoverPosition,
} from "@blueprintjs/core";

<PopoverNext
    placement={popoverPositionToNextPlacement(PopoverPosition.BOTTOM_LEFT)}
    middleware={popperModifiersToNextMiddleware({ offset: { options: { offset: [0, 8] } } })}
    // ...
/>;

Important

The default for shouldReturnFocusOnClose has changed. When left unset, focus now returns to the trigger on close where it previously did not. When migrating, set shouldReturnFocusOnClose={false} to preserve the old behavior.

Gotchas

A few legacy options don't carry over cleanly:

  • offset as a function. Popper accepted a function for offset; Floating UI doesn't. The converter drops it with a console warning. Rewrite it as a static { mainAxis, crossAxis } object on middleware.
  • arrow element as a CSS selector string. Floating UI needs a real element, not a selector. In practice the arrow is now controlled with the arrow boolean, so this rarely matters.
  • portalStopPropagationEvents is gone. It has been inert since React 17 changed event delegation.
  • Placement vs. auto-placement are mutually exclusive. With a placement set, PopoverNext uses flip to reposition when space is tight; with no placement, it uses autoPlacement. The two cannot be combined.

Refs

Legacy Popover exposed a single popoverRef to the popover element, and some code reached the Popover class instance through a component ref. PopoverNext splits these into two:

import { PopoverNext, type PopoverNextRef } from "@blueprintjs/core";

const handle = useRef<PopoverNextRef>(null);

<PopoverNext
    ref={handle} // imperative handle: handle.current?.reposition()
    popoverRef={node => {
        /* ... */
    }} // DOM ref to the .bp6-popover element
/>;
  • popoverRef (the prop) provides the DOM node, the element with the Classes.POPOVER class. Use it wherever the popover element was previously read.
  • The component ref resolves to a PopoverNextRef, an imperative handle whose reposition() method replaces the old class instance method. Other instance internals (for example popoverElement) are no longer reachable; use the two refs above instead.

Wrapper components that re-expose popoverProps

A component that wraps Popover and forwards a popoverProps: PopoverProps prop can swap to PopoverNext without changing its own public API. Run the incoming props through popoverPropsToNextProps and spread the result:

import { PopoverNext, popoverPropsToNextProps, type PopoverProps } from "@blueprintjs/core";

function MyWidget({ popoverProps }: { popoverProps?: PopoverProps }) {
    return <PopoverNext {...popoverPropsToNextProps(popoverProps)}>{/* ... */}</PopoverNext>;
}

popoverPropsToNextProps applies the following transformations:

  • It converts position/placement, modifiersmiddleware, minimalanimation/arrow, and the boundary value, passing the rest through 1:1.
  • It pins shouldReturnFocusOnClose: false to preserve the legacy default (unless a value is supplied explicitly).
  • It drops modifiersCustom and portalStopPropagationEvents, each with a development-mode warning.
  • The onClose it returns is a new function on each call, not the original reference.

Migrating OverlayOverlay2

Legacy Overlay is the second component that breaks on React 19, for the same reason: its CSSTransition falls back to ReactDOM.findDOMNode. Overlay2 is the React-19-safe replacement.

See the Overlay2 migration guide.

This migration applies only to code that renders Overlay directly. The components that used to wrap it (Dialog, Drawer, Alert) already use Overlay2.

Roadmap to v7

  • v6.16.0+ includes React 19 in the published peer dependency ranges. Legacy Popover and Overlay remain available but stay deprecated; they are the only parts of Blueprint v6 that don't work under React 19.
  • v7 removes legacy Popover and Overlay. At that point the next-generation components take the canonical names (PopoverNext becomes Popover, Overlay2 becomes Overlay), and React 19 is the supported baseline.

Clone this wiki locally