# react-mosaic: a tiling window manager that owns your panel tree

> react-mosaic gives a React app drag-to-resize panels, n-ary splits and tab containers, and it converts v6 binary trees at render time so an old layout keeps working. The layout is a tree value you either own yourself or hand over with initialValue, and a parent with no height renders nothing.

**nomcopter/react-mosaic** — A React tiling window manager

- Repository: https://github.com/nomcopter/react-mosaic
- Website: https://nomcopter.github.io/react-mosaic/
- Stars: 4,800 · Forks: 244
- Language: TypeScript
- License: NOASSERTION
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/nomcopter-react-mosaic

## Mosaic fills its parent, and an unheighted parent gives you a blank page

Installation is three packages and one stylesheet import:

```bash
npm install react-mosaic-component react react-dom
```

```tsx
import 'react-mosaic-component/react-mosaic-component.css';
```

The minimal layout renders a two panel row from an `initialValue` tree and paints each panel through `renderTile`, which receives the panel id and its path:

```tsx
<Mosaic<string>
  renderTile={(id, path) => (
    <MosaicWindow<string> path={path} title={`Panel ${id}`}>
      <div style={{ padding: 20 }}>Contents of {id}</div>
    </MosaicWindow>
  )}
  initialValue={{
    type: 'split',
    direction: 'row',
    children: ['a', 'b'],
  }}
/>
```

That produces a draggable divider, drag handles on each title bar and the default toolbar buttons in fifteen lines. The failure mode to know about is sizing: the component fills its parent, so a parent with no height renders an empty page. The quick start wraps it in a `div` with `height: '100vh'` for exactly that reason.

## A split holds any number of children, and tabs are a node type

Two design choices separate this from a grid of fixed cells. The first is the n-ary tree. A single split can hold any number of children rather than exactly two, so a three panel layout is one node with three children instead of two nested binary splits, and adding a panel does not mean rebuilding the tree shape.

The second is that tab containers are a node type in that tree, not a convention layered on top of it. A tabbed group is something the tree can express, which means a user can drag a panel into a tab strip and the result is still a tree value you can serialise, save and restore.

Both choices are about the data model rather than the pixels, and they are why the tree, not the DOM, is the thing to think about when you build on top of it. The n-ary model, custom toolbars, tabs and theming are documented with live editable examples on the documentation site.

## value plus onChange, or initialValue, and you own the choice

The component works controlled or uncontrolled, and the difference is who holds the tree. Pass `value` together with `onChange` and your state is the source of truth, so anything that changes the layout, a persisted workspace, a reset button or a test, goes through your own code. Pass `initialValue` and the component owns it from then on, which is the fifteen line version in the quick start.

That decision has consequences beyond style. In the controlled case your layout is a plain value in your store, so you can version it, diff it between releases, migrate it when the tree format changes and restore it after a reload. In the uncontrolled case none of that is available without reaching into the component.

The tree value is the same shape in both cases: a `type` of `split`, a `direction` of `row`, and a `children` array of panel ids.

## v6 binary trees are converted when they render

Upgrading is not a rewrite. Legacy v6 binary trees are auto-converted at render time, so a stored layout written against the old two child `first` and `second` shape keeps working without a migration script, and `convertLegacyToNary` is exported for the cases where you want the conversion explicitly rather than at paint time.

That is the point where the old and new data models meet, and it is also where mistakes happen. The Agent Skill the package ships calls out writing the old v6 `first` and `second` trees as one of the common errors, which tells you the conversion is forgiving at runtime and unforgiving in your source.

A migration guide from v6 is published alongside the API reference, and the practical advice for an existing app is to switch on the controlled form first, save whatever the component produces, and only then delete the old layout code.

## The package ships a SKILL.md for coding agents

This is the part that is new in v7 and worth knowing about before you write the integration by hand. The package ships an Agent Skill at `skills/react-mosaic/SKILL.md` containing the v7 API, the tree model and the common mistakes, and it is installed with:

```bash
npx skills add nomcopter/react-mosaic
```

It targets Claude Code, Codex, Cursor, GitHub Copilot and Gemini CLI, along with other agents that support skills. If you would rather wire it up manually, the same file ships inside the installed package at `node_modules/react-mosaic-component/skills/react-mosaic/SKILL.md`, which you can point at from `AGENTS.md` or `CLAUDE.md`.

For chat assistants rather than coding agents, the documentation is also served as `llms.txt` and `llms-full.txt`. The build copies the `skills` directory into the published package, so the file a user installs with `npm install` is the same one in this repository, which is why it is worth keeping in step with the API.

## The library is one workspace inside an Nx monorepo

The repository root is a workspace, not the package. `libs/` holds the library, `apps/` the documentation site, `tools/` the package tests, and `nx.json` drives the tasks. The published npm name is `react-mosaic-component`, and its scripts show how a single artifact gets assembled: `build-lib:less` runs `lessc` over `libs/react-mosaic-component/src/lib/styles/index.less` to produce the stylesheet you import, `build-lib:js` runs tsup, `build-lib:types` emits declarations with `tsup --dts-only`, and `build-lib:check` runs `tsc --noEmit` against the library tsconfig.

The `copy` step is the interesting one. It renames `index.d.ts` to `index.d.cts` and copies `package.json`, `README.md`, `LICENSE` and the `skills` directory into the dist folder, which is what puts the Agent Skill inside the installed package.

Testing is split too. `npm test` runs every workspace test through Nx, `vitest.config.ts` sits in the root, and `test:package` runs a single `node --test` file that checks package exports. Contributing starts with `npm install`, `npm start` for the docs site, `npm run build:lib`, `npm test` and `npm run lint`; `lefthook.yml` and a `prepare` script install the git hooks, and `.verdaccio/` is a local registry for testing installs.

## The manifest reads 0.0.1 while the tag is v7.2.1

The version bookkeeping needs explaining before you use it to reason about versions. `package.json` in the repository root declares version `0.0.1`, while the published releases are v7.2.1 on 2026-09-28, v7.2.0 on the same day and v7.1.0 on 2026-09-10. The last push was on 2026-09-28, so the project is being worked on.

The root manifest is the workspace file, not the published one. Releases go through `nx release`, and the `release` script runs lint and tests before `nx release --verbose --skip-publish`, which is why the root version is not the number you install. Read the tag list rather than the root `version` field when you decide what to upgrade to.

Two other facts settle most compatibility questions. The package is published as modern JavaScript, so a bundler targeting older browsers has to transpile `react-mosaic-component` as well as your own code, and current Chrome, Edge, Firefox and Safari are supported on desktop and mobile with touch drag and drop built in, while Internet Explorer is not. The licence is Apache 2.0, and the code was originally developed by Kevin Verdieck at Palantir Technologies.

## Conclusion

Use react-mosaic when you want an IDE-style panel layout inside a React app and you are willing to model the layout as a tree value, because n-ary splits, tab containers and automatic conversion of v6 layouts are the reasons to pick it over a grid of fixed cells. Do not pick it for a simple responsive grid, for Internet Explorer, or for a bundle that must run untranspiled on old browsers, since the package ships modern JavaScript. Before you commit, check three things: that your wrapper element has a real height, since `Mosaic` fills its parent, whether you pass `value` with `onChange` or `initialValue`, since that choice decides who owns the tree, and that your bundler transpiles `react-mosaic-component` if you target anything older than a current browser.

## FAQ

### How do I install react-mosaic in a React app?

Run `npm install react-mosaic-component react react-dom`, then import the stylesheet once in your entry point with `import 'react-mosaic-component/react-mosaic-component.css';`. Mount your app with `createRoot` as usual and wrap `Mosaic` in a container that has a real height.

### Why does my react-mosaic layout render as an empty page?

`Mosaic` fills its parent, so a parent with no height produces nothing visible. The quick start wraps the component in a `div` with `style={{ height: '100vh' }}` for that reason.

### How do I control the panel layout tree in react-mosaic?

Pass `value` with `onChange` to own the tree in your own state, or `initialValue` to let the component manage it. The tree value is a node with a `type` of `split`, a `direction` such as `row`, and a `children` array of panel ids, and `renderTile` receives each panel id and its path.

### How do I migrate a layout from react-mosaic v6?

Legacy v6 binary trees are converted automatically at render time, and `convertLegacyToNary` is exported if you want the conversion explicitly. A v6 migration guide is published in the documentation, and the Agent Skill the package ships flags writing the old `first` and `second` trees as the common mistake.

### Does react-mosaic need Blueprint to be themed?

No. Theming works with or without Blueprint: the package ships a default CSS theme plus CSS variables you can override.

### Does react-mosaic support touch devices and older browsers?

Touch drag and drop is built in, using `react-dnd` with HTML5 and touch backends, and current Chrome, Edge, Firefox and Safari are supported on desktop and mobile. Internet Explorer is not supported, and because the package ships as modern JavaScript, older targets need your bundler to transpile `react-mosaic-component` too.

## Sources

- [Issues](https://github.com/nomcopter/react-mosaic/issues)
- [nomcopter/react-mosaic on GitHub](https://github.com/nomcopter/react-mosaic)
- [Project website](https://nomcopter.github.io/react-mosaic/)
- [README](https://github.com/nomcopter/react-mosaic/blob/master/README.md)
- [Releases](https://github.com/nomcopter/react-mosaic/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/nomcopter-react-mosaic
