# FlexLayout gives React a dockable tab layout driven by a JSON model

> FlexLayout is an MIT licensed React layout manager that arranges components into tabsets, splitters and borders, with the whole arrangement held in a serializable model object that React code can create, save and restore. It is ESM-only, needs Node 20 or newer, and its npm package has a single runtime dependency, React.

**caplin/FlexLayout** — Docking Layout Manager for React

- Repository: https://github.com/caplin/FlexLayout
- Stars: 1,385 · Forks: 216
- Language: TypeScript
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/caplin-flexlayout

## The layout is a tree of nodes you can serialize

FlexLayout is described as a layout manager for React that arranges components in multiple tabsets, where tabs can be resized, moved and organised into complex layouts. The mechanism is the interesting part, and it is a data model rather than a set of callbacks.

The layout is a tree of Node objects. The model JSON's top-level elements include global for optional global options, borders and layout, and the layout is a row of tabsets whose weights determine the split. A minimal configuration looks like this.

```javascript
const json = {
    global: {},
    borders: [],
    layout: {
        type: "row",
        weight: 100,
        children: [
            {
                type: "tabset",
                weight: 50,
                children: [
                    {
                        type: "tab",
                        name: "One",
                        component: "placeholder",
                    }
                ]
            },
            {
                type: "tabset",
                weight: 50,
                children: [
                    {
                        type: "tab",
                        name: "Two",
                        component: "placeholder",
                    }
                ]
            }
        ]
    }
};
```

Models are created with the static method Model.fromJson(jsonObject) and saved with model.toJson(). That pair is the contract with your backend: the user drags panels around, you persist the resulting JSON, and next session you load it back. A layout manager that can only live in React state is a demo, and this one is designed around the round trip.

The second half of the contract is the factory. The Layout component takes a model and a factory function, and the factory takes a Node and returns the React component to host inside that tab. The node carries the component name, and the factory maps names to your actual components.

## A factory function instead of a component registry

Here is the whole example from the documentation.

```javascript
const model = Model.fromJson(json);

function App() {

  const factory = (node) => {
    const component = node.getComponent();

    if (component === "placeholder") {
      return <div>{node.getName()}</div>;
    }
  }

  return (
    <Layout
      model={model}
      factory={factory} />
  );
}
```

The factory returns undefined for a component name it does not recognise, which is worth noticing. Nothing in the shown code throws for an unknown component, so a saved layout that references a panel you have since deleted renders an empty tab rather than an error. Whether that is a feature depends on how you treat persisted layouts, and it is the kind of behaviour you should decide on deliberately rather than inherit by accident.

The two required props on Layout are model and factory, and the model prop is where the state lives. Additional tabs are added by sending actions to the model, which means the library exposes an action vocabulary rather than a set of imperative methods for every manipulation.

The naming convention in the example is instructive as well. A tab carries a name, which is the display label, and a component, which is the key the factory switches on. Those are separate concepts, and keeping them separate is what lets you rename a panel in the UI without invalidating saved layouts, or render the same component under several tabs. The library also preserves component state when tabs are moved, which is a promise worth testing in your own application because it depends on how you host the components.

## ESM only, Node 20 minimum, and React as the only dependency

The package metadata is strict in a way that will catch you out during setup. The package is ESM-only and must be consumed via import, and it cannot be loaded with require(). The type field in package.json is module, and the exports map offers only an import condition alongside a default, with the stylesheets exposed separately under ./style/*.

```javascript
import { Layout, Model, Actions, DockLocation } from 'flexlayout-react';
```

The engines field requires node >=20, and the packageManager field pins pnpm@10.28.0, so the project itself is developed with pnpm even though the installation instructions in the README use npm.

```bash
npm install flexlayout-react
```

The README also states that FlexLayout's only dependency is React, which is a genuine constraint on the package graph and worth checking when you evaluate bundle size and supply chain exposure. The npm badge in the README links to the package as flexlayout-react, and the version in package.json is 0.11.1, matching the tagged release v0.11.1 from 2026-09-26.

One packaging detail has practical consequences. The sideEffects field lists only CSS and SCSS files, so the bundler is told it may tree shake the JavaScript freely. That is why the theme import is written the way it is, as a side effect import of a stylesheet rather than an assignment.

```javascript
import 'flexlayout-react/style/alpha_light.css';
```

The available themes are alpha_light, alpha_dark, alpha_rounded, light, dark, underline, gray, rounded and combined, and the README links to a section on changing the theme dynamically in code.

## Tabsets, borders, splitters and the popout path to a new window

The feature list is long, so it is worth sorting it by what kind of problem each item solves.

Inside the frame, the basics are resizing and moving, tabset dragging that moves all tabs in a tabset in one operation, docking to tabsets or to the edges of the frame, and maximising a tabset by double clicking its header or using an icon. Tabsets can be scrolled or wrapped, there is a menu for overflow when tabs do not fit, and mouse wheel scrolling moves through them.

Borders are the more interesting structural feature. A border tabsets can split the layout or overlay it, and it has an autohide when empty option, so a floating panel that is not in use does not take space. Popout is the step beyond that: tabs can be popped out into floating panels or into new browser windows, which is what makes it possible to put a log viewer on a second monitor.

On top of that sit the behaviours that separate a real workbench from a tab strip. Tab groups, described as Chrome-style coloured group pills. Pinned tabs. Submodels, which are layouts inside layouts, so a panel can itself contain a docking arrangement. A tabset placeholder for empty tabsets. Internationalization support. And per-element attributes such as enableTabStrip, enableDock and enableDrop, which is how you make one region of the layout read-only or non-draggable while leaving the rest interactive.

The demos directory in the repository shows the same spread in practice, with separate example files for tab rendering, tabset rendering, borders, popout, sublayout, theming, i18n, many tabs, pinned tabs, accessibility and external drag. The main demo application also includes an action log, a JSON view, a model explorer and a model tree, so you can watch the model change as you drag, which is the fastest way to understand what a layout serialisation contains.

## Accessibility and mobile support are treated as requirements

The feature list names accessibility with ARIA roles, keyboard operation with a configurable keymap, and visible focus, and links to a documentation page for it. A configurable keymap is the part that matters for an application with existing shortcuts, because docking a layout is entirely keyboard driven if you are not using a mouse, and a library that hard codes its key bindings will collide with the host application.

Mobile support is also listed, for iPad and Android, and there is a dedicated demo for it. This is a stronger claim than it sounds for a drag and drop library, since drag and drop on touch devices is a different interaction model from pointer events, and the fact that the feature list calls out specific platforms suggests the touch path was tested rather than assumed.

The repository structure backs the engineering claims. There are unit tests in a tests/ directory, Playwright browser tests in tests-playwright/ with a playwright.config.ts at the root, and three separate Vite configurations, one for the library build, one for the demo and one for the general case. The build script is a chain of nine steps, from build:clean through generate:interfaces, typecheck, build:demo, build:examples, css, build:lib, build:types and doc.

The interfaces generation step is the interesting one for a consumer. A script called generate:interfaces runs a TypeScript file named generate-json-interfaces.ts, and there is a matching check:interfaces script. That means the TypeScript types for the model JSON are generated from a single source, so the type declarations that ship in the package,./types/index.d.ts, stay in step with the runtime validation of the model.

## Where FlexLayout is the wrong tool

The first case is a content site. If your product is a marketing page, a documentation site or a blog, a docking layout is the wrong abstraction. A dockable workspace is a tool for people who arrange their own views and expect to keep that arrangement, and no user wants to rearrange a blog post.

The second case is a build pipeline that cannot handle ESM. The package cannot be loaded with require(), so a CommonJS-only test harness, a legacy server renderer or an older bundler setup will fail at the import step rather than at some later stage. The engine requirement of Node 20 or newer is the other half of that constraint.

The third case is when you need the layout to be the source of truth for application state. FlexLayout owns the arrangement of panels, not their content. A form inside a tab, a data grid with server side paging, a component with its own subscription: those live inside the React components your factory returns, and the library does not know about them. The library does preserve component state when tabs are moved, but that is a rendering guarantee, not a data model guarantee, and it does not mean your component's state survives being closed and reopened, because a closed tab is unmounted.

The fourth case is a global layout requirement. The Layout component must be hosted in a container element with CSS position absolute or relative, and the layout will fill the containing element. That is a sensible default for a full-screen application, but it means you cannot put a docking layout in a normal flow container without extra CSS, and it rules out rendering several independent layouts in one page without each having its own positioned wrapper.

## How it differs from writing tab state by hand

The comparison worth making is with the approach most teams reach for first, which is a set of React state variables holding the current tab, a flexbox with hard coded percentages, and a library for the drag and drop events. FlexLayout replaces all of that with one model object.

The difference in approach is that the arrangement is data rather than behaviour. In a hand rolled implementation, the current arrangement is scattered across state: which tab is active in each panel, the width of each column, which panels are open. Every change to the layout means writing code that updates those values, and every new feature, such as a floating panel or a maximised view, means writing more of that code.

With FlexLayout, dragging a tab mutates the model, the model drives the render, and toJson() gives you the entire arrangement as a value. Persisting the workspace is a single call, restoring it is a single call, and the split and floating state are handled by the same code path as everything else rather than by a separate mechanism per feature.

The cost is the same trade-off any declarative layout model makes. You give up direct control over the DOM and CSS of the arrangement, and the model becomes a vocabulary you have to learn. The generated model JSON, the actions sent to the model, and the attributes such as enableTabStrip are that vocabulary. A team that only needs two resizable panels is spending more on that vocabulary than it would on flexbox.

The library also does not try to be a design system. The demo directory includes Material UI and styled-components demonstrations, and the tab and tabset rendering is explicitly customizable, which is the opposite approach to a component kit that owns the appearance. You bring your own components and your own CSS, and FlexLayout provides the frame, the drag mechanics and the accessibility layer.

## Conclusion

FlexLayout fits an application that is a workspace rather than a document, an IDE-like tool, a monitoring console or a multi-pane back office, where users arrange their own panels and expect that arrangement to survive a reload. It does not fit a content site with one scrollable column, and it does not fit a CommonJS build, since the package is ESM-only and cannot be loaded with require(). Before adopting it, check that your bundler handles an ESM-only package with a CSS side effect entry, remember that the Layout component needs a container with position absolute or relative or it will not size itself, and note that the current release is v0.11.1 from 2026-09-26 with the last push on 2026-10-09.

## FAQ

### How do I install FlexLayout in a React project?

Install it from npm with npm install flexlayout-react, then import it with import { Layout, Model, Actions, DockLocation } from 'flexlayout-react'. The package is ESM-only and cannot be loaded with require(), and it requires node >=20.

### How do I save and restore a FlexLayout arrangement?

Create the model with the static method Model.fromJson(jsonObject) and read the current arrangement back with model.toJson(). The Layout component takes the model as its model prop, and a factory function that receives a Node and returns the React component to host in that tab.

### Why is my FlexLayout not rendering with the expected size?

The Layout component must be hosted in a container element with CSS position absolute or relative, and the layout will fill the containing element. Without a positioned ancestor it has no dimensions to fill.

### Which themes does FlexLayout ship?

The README lists alpha_light, alpha_dark, alpha_rounded, light, dark, underline, gray, rounded and combined, each imported as a side effect stylesheet such as import 'flexlayout-react/style/alpha_light.css'. The styles are exposed through the package exports under ./style/*.

### Can FlexLayout open a tab in a separate browser window?

Yes, the feature list names popout tabs into floating panels or new browser windows, and the demos include a popout example. The demo directory also has a PopoutStyleProvider, which is how the styling of a popped out window is supplied.

## Sources

- [caplin/FlexLayout on GitHub](https://github.com/caplin/FlexLayout)
- [Issues](https://github.com/caplin/FlexLayout/issues)
- [License: MIT](https://github.com/caplin/FlexLayout/blob/master/LICENSE)
- [README](https://github.com/caplin/FlexLayout/blob/master/README.md)
- [Releases](https://github.com/caplin/FlexLayout/releases)

---

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