# React Fragment Refs: Attach Behavior Without the Wrapper Div

Every React codebase has at least one of these: a `<div>` that exists for no reason except to hold a ref. Maybe it wraps a list so an `IntersectionObserver` has something to watch. Maybe it wraps a group of form fields so you can focus the first one. It works, right up until it breaks a flex layout, adds a stray grid item, or lands inside a `<ul>` where only `<li>` belongs.

The other version of this problem is worse. You want to observe or focus a component from a library, and that component doesn't forward its `ref`. Now you're either forking the library or adding the wrapper div anyway.

React 19.3, released on September 9, 2026, made **Fragment refs** stable. You can now pass a `ref` to `<Fragment>` and get back a `FragmentInstance`, an object with a small set of DOM methods that operate on the fragment's children as a group. No wrapper, no changes to the children.

## What a FragmentInstance can do

The setup is the ref you already know, just on a Fragment. One catch: the `<>...</>` shorthand can't take props, so you need the explicit `Fragment` import.

```jsx
import { Fragment, useRef } from "react";

function Toolbar({ children }) {
  const groupRef = useRef(null);
  return <Fragment ref={groupRef}>{children}</Fragment>;
}
```

`groupRef.current` is now a `FragmentInstance`. Its methods fall into four groups:

*   **Events:** `addEventListener`, `removeEventListener`, and `dispatchEvent`
    
*   **Focus:** `focus`, `focusLast`, and `blur`
    
*   **Observers:** `observeUsing` and `unobserveUsing`, which take an `IntersectionObserver` or `ResizeObserver`
    
*   **Layout:** `getClientRects`, `getRootNode`, `compareDocumentPosition`, and `scrollIntoView`
    

The important detail is *which* nodes these touch. Event, observer, and layout methods target the fragment's **first-level DOM children**. React looks through component boundaries to find them, so if a child component renders a `<section>`, that `<section>` is a target. But it stops at the first real DOM node: an element nested inside that `<section>` is not. Focus methods are the exception. `focus()` and `focusLast()` search depth-first through everything, which is what you want when the inputs are buried inside a `<fieldset>` and a `<label>`.

Listeners are also kept in sync. If children are added or removed later, the `FragmentInstance` attaches or detaches the listener for you.

## Observing visibility with no wrapper and no forwarded refs

This is the case that used to force the wrapper div. Here is a reusable `InView` component that reports whether any of its children are on screen:

```jsx
import { Fragment, useLayoutEffect, useRef } from "react";

export function InView({ onChange, children }) {
  const fragmentRef = useRef(null);

  useLayoutEffect(() => {
    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);
      }
      onChange(visible.size > 0);
    });

    const fragment = fragmentRef.current;
    fragment.observeUsing(observer);
    return () => fragment.unobserveUsing(observer);
  }, [onChange]);

  return <Fragment ref={fragmentRef}>{children}</Fragment>;
}
```

Use it around anything, including third-party components that never heard of refs:

```jsx
<InView onChange={setHeroVisible}>
  <VendorBanner />
  <PricingTable />
</InView>
```

`VendorBanner` and `PricingTable` render exactly the DOM they always did. Your CSS grid still sees two items, not one wrapper. If `onChange` is an inline function, wrap it in `useCallback` (or let the React Compiler handle it) so the observer isn't torn down on every render.

One warning from the docs: `observeUsing` doesn't work on text nodes, and React logs a dev warning if a fragment contains only text.

## Focus management for groups

Focus is the other place this pays off, and it's an accessibility win. When a disclosure panel opens, a dialog-like section mounts, or a validation error appears, you usually want to move focus into it. Before, that meant a ref on the specific input, which meant threading refs through whatever components sat in between.

```jsx
import { Fragment, useEffect, useRef } from "react";

function StepPanel({ isActive, children }) {
  const stepRef = useRef(null);

  useEffect(() => {
    if (isActive) stepRef.current.focus({ preventScroll: true });
  }, [isActive]);

  return <Fragment ref={stepRef}>{children}</Fragment>;
}
```

`focus()` finds the first focusable element anywhere inside the children, and `focusLast()` finds the last one. That pair is most of what you need for a simple focus loop at the edges of a section. `blur()` only does something if the active element is actually inside the fragment, so it's safe to call without checking first.

Two small gotchas. `scrollIntoView` takes only a boolean (`true` aligns the first child to the top, `false` aligns the last child to the bottom), and passing a `ScrollIntoViewOptions` object throws. And listeners added through `addEventListener` are not applied to children inside a hidden `<Activity>` boundary until it becomes visible again.

## When to reach for it

Fragment refs aren't a replacement for regular refs. If you own the element and it's a single node, a plain `ref` on that element is still simpler. Reach for a Fragment ref when you need to attach behavior to a *group* of siblings, or to components you don't control, and a wrapper element would cost you layout, semantics, or both.

If you're already on React 19.3, try deleting one wrapper div this week. The [`<Fragment>` reference](https://react.dev/reference/react/Fragment) covers every method, and the [React 19.3 release post](https://react.dev/blog/2026/09/09/react-19-3) walks through View Transitions and the new `browser()` API that shipped alongside it.
