toolchains/javascript/frameworks/react/SKILL.md
FlexLayout for React - Advanced docking layout manager with drag-and-drop, tabs, splitters, and complex window management
npx skillsauth add bobmatnyc/claude-mpm-skills flexlayout-reactInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
FlexLayout-React provides IDE-quality docking layouts with drag-and-drop, tabs, splitters, and complex window management. Perfect for dashboards, IDEs, admin panels, and any interface requiring flexible, user-customizable layouts.
Key Features:
Installation:
npm install flexlayout-react
import { Model, IJsonModel } from 'flexlayout-react';
const initialLayout: IJsonModel = {
global: {
tabEnableClose: true,
tabEnableRename: false,
},
borders: [],
layout: {
type: 'row',
weight: 100,
children: [
{
type: 'tabset',
weight: 50,
children: [
{
type: 'tab',
name: 'Explorer',
component: 'explorer',
}
]
},
{
type: 'tabset',
weight: 50,
children: [
{
type: 'tab',
name: 'Editor',
component: 'editor',
}
]
}
]
}
};
// Create model
const model = Model.fromJson(initialLayout);
import React, { useRef } from 'react';
import { Layout, Model, TabNode, IJsonTabNode } from 'flexlayout-react';
import 'flexlayout-react/style/dark.css'; // or light.css
interface ComponentRegistry {
explorer: React.ComponentType;
editor: React.ComponentType;
terminal: React.ComponentType;
}
function App() {
const modelRef = useRef(Model.fromJson(initialLayout));
const factory = (node: TabNode) => {
const component = node.getComponent();
switch (component) {
case 'explorer':
return <ExplorerPanel />;
case 'editor':
return <EditorPanel />;
case 'terminal':
return <TerminalPanel />;
default:
return <div>Unknown component: {component}</div>;
}
};
return (
<div style={{ width: '100vw', height: '100vh' }}>
<Layout
model={modelRef.current}
factory={factory}
/>
</div>
);
}
function ExplorerPanel() {
return (
<div className="panel-explorer">
<h3>File Explorer</h3>
<ul>
<li>src/</li>
<li>public/</li>
<li>package.json</li>
</ul>
</div>
);
}
function EditorPanel() {
return (
<div className="panel-editor">
<textarea
style={{ width: '100%', height: '100%' }}
placeholder="Start typing..."
/>
</div>
);
}
const complexLayout: IJsonModel = {
global: {
tabEnableClose: true,
tabEnableRename: false,
tabEnableDrag: true,
tabEnableFloat: true,
borderSize: 200,
},
borders: [
{
type: 'border',
location: 'left',
size: 250,
children: [
{
type: 'tab',
name: 'Explorer',
component: 'explorer',
}
]
},
{
type: 'border',
location: 'bottom',
size: 200,
children: [
{
type: 'tab',
name: 'Terminal',
component: 'terminal',
},
{
type: 'tab',
name: 'Output',
component: 'output',
}
]
}
],
layout: {
type: 'row',
weight: 100,
children: [
{
type: 'tabset',
weight: 70,
children: [
{
type: 'tab',
name: 'Editor 1',
component: 'editor',
},
{
type: 'tab',
name: 'Editor 2',
component: 'editor',
}
]
},
{
type: 'tabset',
weight: 30,
children: [
{
type: 'tab',
name: 'Properties',
component: 'properties',
},
{
type: 'tab',
name: 'Outline',
component: 'outline',
}
]
}
]
}
};
const nestedLayout: IJsonModel = {
global: {},
borders: [],
layout: {
type: 'row',
children: [
{
type: 'col',
weight: 50,
children: [
{
type: 'tabset',
weight: 70,
children: [
{ type: 'tab', name: 'Top Left', component: 'panel-a' }
]
},
{
type: 'tabset',
weight: 30,
children: [
{ type: 'tab', name: 'Bottom Left', component: 'panel-b' }
]
}
]
},
{
type: 'col',
weight: 50,
children: [
{
type: 'tabset',
weight: 30,
children: [
{ type: 'tab', name: 'Top Right', component: 'panel-c' }
]
},
{
type: 'tabset',
weight: 70,
children: [
{ type: 'tab', name: 'Bottom Right', component: 'panel-d' }
]
}
]
}
]
}
};
import { useState, useEffect } from 'react';
import { Model, Actions } from 'flexlayout-react';
function LayoutManager() {
const [model, setModel] = useState(() => {
// Load from localStorage
const saved = localStorage.getItem('layout');
return saved
? Model.fromJson(JSON.parse(saved))
: Model.fromJson(defaultLayout);
});
// Save on model change
const onModelChange = (newModel: Model) => {
const json = newModel.toJson();
localStorage.setItem('layout', JSON.stringify(json));
};
return (
<Layout
model={model}
factory={factory}
onModelChange={onModelChange}
/>
);
}
function LayoutControls({ model }: { model: Model }) {
const resetLayout = () => {
const newModel = Model.fromJson(defaultLayout);
// Need to replace model reference
window.location.reload(); // Simple approach
};
const saveLayout = () => {
const json = model.toJson();
const blob = new Blob([JSON.stringify(json, null, 2)], {
type: 'application/json'
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'layout.json';
a.click();
};
return (
<div className="layout-controls">
<button onClick={resetLayout}>Reset Layout</button>
<button onClick={saveLayout}>Export Layout</button>
</div>
);
}
import { Actions, DockLocation } from 'flexlayout-react';
function addNewTab(model: Model, tabsetId: string) {
model.doAction(Actions.addNode(
{
type: 'tab',
name: `New Tab ${Date.now()}`,
component: 'editor',
},
tabsetId,
DockLocation.CENTER,
-1
));
}
// Add to specific tabset
const addToExplorer = () => {
addNewTab(model, 'explorer-tabset-id');
};
// Add to active tabset
const addToActive = () => {
const activeTabset = model.getActiveTabset();
if (activeTabset) {
addNewTab(model, activeTabset.getId());
}
};
function closeTab(model: Model, tabId: string) {
model.doAction(Actions.deleteTab(tabId));
}
function closeAllTabs(model: Model) {
const tabsets = model.getRoot().getChildren();
tabsets.forEach(tabset => {
if (tabset.getType() === 'tabset') {
const tabs = tabset.getChildren();
tabs.forEach(tab => {
if (tab.getType() === 'tab') {
model.doAction(Actions.deleteTab(tab.getId()));
}
});
}
});
}
interface EditorTabProps {
node: TabNode;
}
function EditorTab({ node }: EditorTabProps) {
const filepath = node.getConfig()?.filepath as string;
const readonly = node.getConfig()?.readonly as boolean;
return (
<div>
<p>Editing: {filepath}</p>
<textarea readOnly={readonly} />
</div>
);
}
// Factory with data passing
const factory = (node: TabNode) => {
const component = node.getComponent();
switch (component) {
case 'editor':
return <EditorTab node={node} />;
default:
return <div>Unknown</div>;
}
};
// Create tab with config
const newTab: IJsonTabNode = {
type: 'tab',
name: 'my-file.ts',
component: 'editor',
config: {
filepath: '/src/my-file.ts',
readonly: false,
}
};
function SmartPanel({ node }: { node: TabNode }) {
const name = node.getName();
const isActive = node.isSelected();
const isVisible = node.isVisible();
return (
<div className={isActive ? 'active' : 'inactive'}>
<h3>{name}</h3>
{isVisible && <p>This tab is visible</p>}
</div>
);
}
/* Override FlexLayout styles */
.flexlayout__layout {
background: #1e1e1e;
}
.flexlayout__tab {
background: #2d2d2d;
color: #cccccc;
}
.flexlayout__tab:hover {
background: #3e3e3e;
}
.flexlayout__tab_button--selected {
background: #1e1e1e;
border-bottom: 2px solid #007acc;
}
.flexlayout__splitter {
background: #2d2d2d;
}
.flexlayout__splitter:hover {
background: #007acc;
}
import 'flexlayout-react/style/dark.css';
// or
import 'flexlayout-react/style/light.css';
function ThemeToggle() {
const [theme, setTheme] = useState<'dark' | 'light'>('dark');
useEffect(() => {
// Dynamically load theme
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = `flexlayout-react/style/${theme}.css`;
document.head.appendChild(link);
return () => {
document.head.removeChild(link);
};
}, [theme]);
return (
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
Toggle Theme
</button>
);
}
import { invoke } from '@tauri-apps/api/core';
async function saveLayoutToTauri(model: Model) {
const json = model.toJson();
await invoke('save_layout', {
layout: JSON.stringify(json)
});
}
async function loadLayoutFromTauri(): Promise<Model> {
const layout = await invoke<string>('load_layout');
return Model.fromJson(JSON.parse(layout));
}
// Tauri command (Rust)
// #[tauri::command]
// async fn save_layout(layout: String) -> Result<(), String> {
// let app_dir = app.path_resolver().app_data_dir()?;
// let layout_file = app_dir.join("layout.json");
// tokio::fs::write(layout_file, layout).await?;
// Ok(())
// }
import { invoke } from '@tauri-apps/api/core';
import { getCurrent } from '@tauri-apps/api/window';
function WindowLayout() {
const [model, setModel] = useState<Model | null>(null);
useEffect(() => {
const currentWindow = getCurrent();
const windowLabel = currentWindow.label;
// Load layout for this specific window
invoke<string>('load_window_layout', { windowLabel })
.then(layout => {
setModel(Model.fromJson(JSON.parse(layout)));
})
.catch(() => {
setModel(Model.fromJson(defaultLayout));
});
}, []);
const onModelChange = (newModel: Model) => {
const currentWindow = getCurrent();
const json = newModel.toJson();
invoke('save_window_layout', {
windowLabel: currentWindow.label,
layout: JSON.stringify(json)
});
};
if (!model) return <div>Loading...</div>;
return (
<Layout
model={model}
factory={factory}
onModelChange={onModelChange}
/>
);
}
import { Layout, Model, TabNode, ITabRenderValues } from 'flexlayout-react';
function App() {
const onRenderTab = (
node: TabNode,
renderValues: ITabRenderValues
) => {
const modified = node.getConfig()?.modified as boolean;
renderValues.content = (
<div className="custom-tab-header">
<span>{node.getName()}</span>
{modified && <span className="modified-indicator">●</span>}
</div>
);
};
return (
<Layout
model={model}
factory={factory}
onRenderTab={onRenderTab}
/>
);
}
const onRenderTab = (node: TabNode, renderValues: ITabRenderValues) => {
renderValues.buttons.push(
<button
key="save"
onClick={() => saveTabContent(node)}
title="Save"
>
💾
</button>
);
renderValues.buttons.push(
<button
key="duplicate"
onClick={() => duplicateTab(node)}
title="Duplicate"
>
📋
</button>
);
};
❌ Not memoizing model:
// WRONG - creates new model on every render
function App() {
const model = Model.fromJson(layout); // Bad!
return <Layout model={model} />;
}
// CORRECT
function App() {
const modelRef = useRef(Model.fromJson(layout));
return <Layout model={modelRef.current} />;
}
❌ Forgetting CSS import:
// WRONG - layout won't display correctly
import { Layout } from 'flexlayout-react';
// Missing: import 'flexlayout-react/style/dark.css';
❌ Not handling onModelChange:
// WRONG - layout changes not persisted
<Layout model={model} factory={factory} />
// CORRECT
<Layout
model={model}
factory={factory}
onModelChange={saveLayout}
/>
When using React, these skills enhance your workflow:
[Full documentation available in these skills if deployed in your bundle]
development
Optimize web performance using Core Web Vitals, modern patterns (View Transitions, Speculation Rules), and framework-specific techniques
development
Best practices for documenting APIs and code interfaces, eliminating redundant documentation guidance per agent.
development
Comprehensive API design patterns covering REST, GraphQL, gRPC, versioning, authentication, and modern API best practices
development
Visual verification workflow for UI changes to accelerate code review and catch ...