React applications often need direct access to DOM elements for tasks such as handling focus, observing visibility, measuring layout, or attaching browser event listeners.
For a single element, a normal ref is straightforward:
const inputRef = useRef(null);
<input ref={inputRef} />
The problem becomes more interesting when a component renders multiple sibling elements.
For example:
function CardGroup() {
return (
<>
<article>First card</article>
<article>Second card</article>
<article>Third card</article>
</>
);
}
There is no single DOM element representing the entire group.
Before React 19.3, developers often had to introduce a wrapper element, expose multiple refs, or modify child components to make DOM access possible.
React 19.3 makes this considerably easier with Fragment Refs.
A ref can now be attached directly to an explicit <Fragment>, giving the application a FragmentInstance that provides a controlled set of operations across the Fragment's DOM children. Fragment Refs became stable in React 19.3.
The Problem With Referencing Multiple Siblings
Consider a component that renders a collection of headings:
function PostList({ posts }) {
return (
<>
{posts.map(post => (
<h2 key={post.id}>
{post.title}
</h2>
))}
</>
);
}
Suppose another component needs to:
Add an event listener to the headings.
Observe when the headings become visible.
Measure their positions.
Scroll them into view.
Manage focus across the group.
A normal DOM ref cannot represent the whole sibling group.
One option is to add a wrapper:
function PostList({ posts }) {
return (
<div ref={containerRef}>
{posts.map(post => (
<h2 key={post.id}>
{post.title}
</h2>
))}
</div>
);
}
That solves the ref problem, but it changes the DOM structure.
Sometimes that is harmless.
Sometimes it is not.
An additional element can affect:
CSS selectors
Flexbox layouts
Grid layouts
Accessibility semantics
Styling
Existing DOM assumptions
Third-party integrations
Fragment Refs provide another option without requiring that wrapper.
What Is a Fragment Ref?
React 19.3 allows a ref to be passed to an explicit <Fragment>.
import { Fragment, useRef } from "react";
function PostList({ posts }) {
const fragmentRef = useRef(null);
return (
<Fragment ref={fragmentRef}>
{posts.map(post => (
<h2 key={post.id}>
{post.title}
</h2>
))}
</Fragment>
);
}
Instead of receiving an HTMLElement, the ref receives a FragmentInstance.
Fragment
|
+-- <h2>
+-- <h2>
+-- <h2>
The Fragment itself still does not create a DOM element.
The ref provides an API for interacting with the DOM children represented by the Fragment.
Why You Need an Explicit Fragment
There is an important syntax difference.
This shorthand:
<>
<h2>First</h2>
<h2>Second</h2>
</>
cannot receive a Fragment ref.
Instead, use the explicit form:
import { Fragment } from "react";
<Fragment ref={fragmentRef}>
<h2>First</h2>
<h2>Second</h2>
</Fragment>
The explicit <Fragment> is also the form required when you need to provide a key to a Fragment.
Understanding FragmentInstance
The value stored in the ref is not a normal DOM element.
For example:
const fragmentRef = useRef(null);
console.log(fragmentRef.current);
After mounting, fragmentRef.current represents the Fragment instance.
The FragmentInstance API provides operations for common DOM-related tasks, including:
Event listeners
Event dispatching
Focus management
Intersection and resize observation
Layout measurement
Root-node access
Document-position comparison
Scrolling
This gives developers lower-level DOM control while preserving the Fragment's zero-wrapper behavior.
Adding Events to Multiple Children
One practical use case is attaching the same event listener to a group of elements.
Consider:
function ClickableGroup({ children, onClick }) {
const fragmentRef = useRef(null);
useEffect(() => {
const fragment = fragmentRef.current;
if (!fragment) {
return;
}
fragment.addEventListener("click", onClick);
return () => {
fragment.removeEventListener("click", onClick);
};
}, [onClick]);
return (
<Fragment ref={fragmentRef}>
{children}
</Fragment>
);
}
You can then use it like this:
<ClickableGroup
onClick={() => {
console.log("Group child clicked");
}}
>
<button>Save</button>
<button>Share</button>
<button>Archive</button>
</ClickableGroup>
The listener is applied to the Fragment's first-level DOM children.
If children are added or removed, React keeps the Fragment's listener behavior synchronized with those children.
First-Level DOM Children Matter
One of the most important details about Fragment Refs is understanding what they target.
Consider:
<Fragment ref={fragmentRef}>
<div id="first" />
<Wrapper>
<div id="nested" />
</Wrapper>
<div id="last" />
</Fragment>
The Fragment's first-level host children are effectively:
Fragment
|
+-- #first
|
+-- #nested's outer DOM element
|
+-- #last
The deeply nested descendants are not directly targeted by methods such as addEventListener(), observeUsing(), and getClientRects(). React looks through components to identify the relevant first-level DOM children, but does not recursively target every descendant for those methods.
This distinction is important when designing components around Fragment Refs.
Managing Focus Across a Fragment
Fragment Refs are also useful for focus management.
For example:
function NavigationGroup() {
const fragmentRef = useRef(null);
return (
<>
<button
onClick={() => {
fragmentRef.current?.focus();
}}
>
Focus navigation
</button>
<Fragment ref={fragmentRef}>
<a href="/home">Home</a>
<a href="/products">Products</a>
<a href="/contact">Contact</a>
</Fragment>
</>
);
}
The focus() method searches through the Fragment's children for a focusable element.
There is also:
fragmentRef.current?.focusLast();
which focuses the last appropriate focusable element.
And:
fragmentRef.current?.blur();
can remove focus from the relevant elements.
Unlike methods that target first-level host children, focus() and focusLast() search nested children depth-first.
Building an InView Component
A more advanced example is creating a reusable visibility component.
Suppose you want a component that notifies its parent when any of its child elements enters or leaves the viewport.
You can use IntersectionObserver with a Fragment Ref:
function InView({ children, onChange }) {
const fragmentRef = useRef(null);
useEffect(() => {
const fragment = fragmentRef.current;
if (!fragment) {
return;
}
const observer = new IntersectionObserver(entries => {
const visible = entries.some(
entry => entry.isIntersecting
);
onChange(visible);
});
fragment.observeUsing(observer);
return () => {
fragment.unobserveUsing(observer);
observer.disconnect();
};
}, [onChange]);
return (
<Fragment ref={fragmentRef}>
{children}
</Fragment>
);
}
This creates a component that can add visibility behavior to multiple children without requiring a wrapper.
Using ResizeObserver
The same observeUsing() API can work with a ResizeObserver.
For example:
function ResponsiveGroup({ children }) {
const fragmentRef = useRef(null);
useEffect(() => {
const fragment = fragmentRef.current;
if (!fragment) {
return;
}
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
console.log("Size changed:", entry.contentRect);
}
});
fragment.observeUsing(observer);
return () => {
fragment.unobserveUsing(observer);
observer.disconnect();
};
}, []);
return (
<Fragment ref={fragmentRef}>
{children}
</Fragment>
);
}
This can be useful for reusable components that need to monitor several sibling elements.
One limitation is that observeUsing() does not work on text nodes. React warns about this during development when a Fragment contains only text children.
Measuring Multiple Elements
Fragment Refs also provide getClientRects().
const rects = fragmentRef.current?.getClientRects();
console.log(rects);
The method returns an array of DOMRect objects representing the bounding rectangles of the Fragment's first-level DOM children.
For example:

Join the conversation! Your thoughts help the community grow.