How to Use Fragment Refs in React 19.3
Use React 19.3 Fragment refs to handle events, focus, observers, and measurements across sibling elements without a wrapper. See method limits and when to keep a DOM element.
In React 19.3, passing a ref to <Fragment> gives you a FragmentInstance: an object whose DOM methods act on all of the Fragment’s children as a group, without adding an element to the page.
You have probably wrapped a group of siblings in a <div> just to get a ref onto them, and then watched a CSS grid or flex row break because the cards stopped being direct children of the container. Fragment refs let you attach listeners, observers and focus handling to sibling groups while leaving the DOM structure unchanged. This guide skips the full release recap (what’s new in React 19 covers the earlier features). It covers how the API works, which children each method can reach, and when a wrapper element is still the better choice.
Key Takeaways
- Fragment refs are stable in React 19.3. The
<></>shorthand cannot take a ref, so you need to importFragmentand write<Fragment ref={ref}>. - The ref holds a
FragmentInstance, not a DOM node and not an array, and it exposes twelve methods in four groups: events, focus, observers, and measurement/scrolling. - Event, observer and measurement methods only reach the Fragment’s first-level DOM children.
focus()andfocusLast()search nested elements depth-first. scrollIntoView()on aFragmentInstanceaccepts only thealignToTopboolean and throws if you pass an options object.
What Is the Wrapper Div Problem?
Before React 19.3, the only way to act on a group of sibling elements was to wrap them in a real DOM element and put the ref on that element. The wrapper changes the layout tree, and layout systems such as CSS grid and flexbox only see direct children.
// Before: the div is now the only grid item
<div className="grid">
<div ref={groupRef}><Card /><Card /><Card /></div>
</div>
// After: each card is still a grid item
<div className="grid">
<Fragment ref={groupRef}><Card /><Card /><Card /></Fragment>
</div>
In the wrapper-div version, the cards become children of the wrapper, not of the grid. The Fragment version leaves the DOM exactly as it would be without any ref at all.
How Does a React Fragment Ref Work?
A React Fragment ref works like any other ref, except that the value React assigns is a FragmentInstance. Fragment Refs are a stable feature in the React 19.3 release, published September 9, 2026. You need React 19.3 or later (react and react-dom 19.3.0+, including the 19.3.1 patch), so install matching 19.3.x versions of both packages. If you are coming from an older major version, check the common mistakes when upgrading to React 19 first.
The <Fragment> reference lists twelve methods:
| Method group | Methods | Reaches | Notes |
|---|---|---|---|
| Events | addEventListener, removeEventListener, dispatchEvent | First-level children | dispatchEvent fires on the Fragment and can bubble to its DOM parent |
| Focus | focus, focusLast, blur | Nested, depth-first | focus() accepts FocusOptions |
| Observers | observeUsing, unobserveUsing | First-level children | IntersectionObserver or ResizeObserver; text nodes are not supported |
| Measure/scroll | getClientRects, getRootNode, compareDocumentPosition, scrollIntoView | First-level children | getRootNode works from the parent; scrollIntoView takes only a boolean |
Most surprises with this API come from the first-level rule. A short nested example shows exactly what each method group can reach:
<Fragment ref={groupRef}>
<div className="row"> {/* first-level: gets listeners, observed, measured */}
<button>Save</button> {/* nested: not observed; focus() can reach it */}
</div>
<Card /> {/* component: the element Card renders at its top level is first-level */}
Some text {/* text node: observeUsing does not work on it */}
</Fragment>
Components don’t count as a level. What matters is the DOM element each component renders at its top level, so <Card /> counts as first-level even though it is a React component.
How Do You Add One Event Listener to Every Child?
Calling addEventListener on a FragmentInstance registers the handler on every first-level DOM child at once. Register it in an effect and remove it in the cleanup, passing the same function reference both times.
import { Fragment, useEffect, useRef } from 'react';
function ClickGroup({ onClick, children }) {
const groupRef = useRef(null);
useEffect(() => {
const group = groupRef.current;
if (!group) return;
group.addEventListener('click', onClick);
return () => group.removeEventListener('click', onClick);
}, [onClick]);
return <Fragment ref={groupRef}>{children}</Fragment>;
}
The reference states that React adds or removes the listener as children enter or leave the Fragment. Pass a stable onClick (for example, wrapped in useCallback), because a new function on every render makes the effect unsubscribe and resubscribe each time.
Nested elements still trigger the listener when the event bubbles. A click on a <button> inside a first-level <div> bubbles up to that <div>, where the handler is attached. Events that don’t bubble, such as focus or mouseenter, will not reach the listener from deeper elements.
How Do You Watch the Group with observeUsing?
observeUsing() connects an IntersectionObserver or ResizeObserver you have already created to every first-level DOM child. unobserveUsing() disconnects that same observer instance from them.
import { Fragment, useEffect, useRef } from 'react';
function VisibilityGroup({ onVisibilityChange, children }) {
const groupRef = useRef(null);
useEffect(() => {
const group = groupRef.current;
if (!group) return;
const visible = new Set();
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting) visible.add(entry.target);
else visible.delete(entry.target);
}
onVisibilityChange(visible.size > 0);
});
group.observeUsing(observer);
return () => {
group.unobserveUsing(observer);
observer.disconnect();
};
}, [onVisibilityChange]);
return <Fragment ref={groupRef}>{children}</Fragment>;
}
The Set tracks which children are currently intersecting, so the group counts as visible while any card is on screen. The children can be components that never accept a ref. As the Fragment reference notes, text nodes are not observed, and React logs a development warning when a Fragment contains only text.
Move Focus with focus()
Calling focus() on a Fragment ref moves focus to the first focusable element in the group. This works even when the input sits inside a <label> or <fieldset> rendered by a child component that doesn’t expose a ref, because the focus methods search nested children depth-first.
import { Fragment, useEffect, useRef, useState } from 'react';
function EditAddress() {
const [editing, setEditing] = useState(false);
const fieldsRef = useRef(null);
useEffect(() => {
if (editing) fieldsRef.current?.focus();
}, [editing]);
return (
<section>
<button onClick={() => setEditing(true)}>Edit address</button>
{editing && (
<Fragment ref={fieldsRef}>
<AddressFields />
</Fragment>
)}
</section>
);
}
A session replay of a form or modal can show focus landing nowhere after a state change, so the next keystroke goes to <body>. Calling focus() on a Fragment ref fixes that without a wrapper. Use focusLast() to target the last focusable element, and pass FocusOptions such as { preventScroll: true } if needed. A ref callback also works, but keep it stable so it doesn’t re-run on every render.
When Do You Still Want a Wrapper Element?
Use a wrapper element when the DOM node itself is what you need. A Fragment ref works on a group of children. It doesn’t create a box. Keep the wrapper when:
- You need to style the group. Backgrounds, borders, padding and grid areas all need a real element.
- You need one bounding box.
getClientRects()returns a flat array of rects for the children, not a single combined rectangle. - Your targets are nested deeper than first-level children. Observers and non-bubbling events never reach those elements.
Remember too that scrollIntoView() accepts only alignToTop. If you need smooth scrolling or block alignment, use the element method on a real node.
Conclusion
Fragment refs let you attach listeners, observers and focus to sibling groups without breaking grid or flex layouts. Before you use one, check the first-level rule: events, observers and measurement stop at direct DOM children, while focus searches deeper. To try it, upgrade to React 19.3 and replace one ref-only wrapper div with <Fragment ref>. The same release also added Trusted Types support, described in the React 19.3 release post.
FAQs
How do I type a Fragment ref in TypeScript?
Use useRef with the FragmentInstance type exported from react and an initial value of null. In @types/react 19.3 the FragmentInstance interface is empty by default. @types/react-dom augments it through declare module 'react' to add focus, observeUsing, addEventListener and the other methods. If those methods appear to be missing, install @types/react-dom and make sure the react-dom module is part of your project's type graph.
Which child does scrollIntoView scroll to on a Fragment ref?
If alignToTop is true or omitted, scrollIntoView() aligns the Fragment's first child with the top of the scrollable ancestor. If alignToTop is false, it aligns the last child with the bottom. An empty Fragment falls back to the nearest sibling or the parent. Passing an options object, such as one with behavior set to 'smooth', throws an error instead of scrolling.
Do Fragment ref event listeners work inside a hidden Activity boundary?
No. React does not apply listeners added through a FragmentInstance's addEventListener to children inside a hidden Activity boundary. When the boundary switches from hidden to visible, React applies those listeners automatically. Your own code does not have to register them again after the content becomes visible.
What does blur() do on a Fragment ref if focus is outside the group?
Nothing. blur() on a FragmentInstance removes focus from document.activeElement only when that element is inside the Fragment. If the active element is anywhere else on the page, the call does nothing. That means you can call blur() in a close or cancel handler without first checking where focus is.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k