npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@lonli-lokli/react-mosaic-component

v0.20.0

Published

A React Tiling Window Manager

Readme

NPM Version React TypeScript

A powerful React Tiling Window Manager for building sophisticated, user-controlled interfaces

React Mosaic is a full-featured React component library that provides complete control over complex workspace layouts. Built with TypeScript, it offers a flexible API for creating tiled interfaces that users can dynamically resize, rearrange, and customize.

🚀 Live Demo | Documentation

✨ Features

  • 🎯 N-ary Tree Structure: Support for complex layouts with multiple panels and tabs
  • 🎨 Drag & Drop: Intuitive drag-and-drop interface for rearranging panels
  • 📱 Responsive: Works seamlessly on desktop and touch devices
  • 🎭 Themeable: Built-in Blueprint theme support with dark mode
  • 💪 TypeScript: Full TypeScript support with comprehensive type definitions
  • 📦 ESM & CJS Support: Available as both ES Modules and CommonJS for maximum compatibility
  • 🔧 Extensible: Customizable toolbar buttons and controls
  • 📚 Well Documented: Comprehensive API documentation and examples

Screencast

screencast demo

🚀 Quick Start

Installation

# Using npm
npm install @lonli-lokli/react-mosaic-component

# Using yarn
yarn add @lonli-lokli/react-mosaic-component

# Using pnpm
pnpm add @lonli-lokli/react-mosaic-component

Basic Setup

  1. Install the package
  2. Import the CSS file: import '@lonli-lokli/react-mosaic-component/react-mosaic-component.css'
  3. (Optional) Install Blueprint for theming: npm install @blueprintjs/core @blueprintjs/icons

Simple Example

import React from 'react';
import { Mosaic, MosaicWindow } from '@lonli-lokli/react-mosaic-component';
import '@lonli-lokli/react-mosaic-component/react-mosaic-component.css';

// Optional: Add Blueprint theme
import '@blueprintjs/core/lib/css/blueprint.css';
import '@blueprintjs/icons/lib/css/blueprint-icons.css';

export type ViewId = 'a' | 'b' | 'c';

const TITLE_MAP: Record<ViewId, string> = {
  a: 'Left Panel',
  b: 'Top Right Panel',
  c: 'Bottom Right Panel',
};

export const MyApp = () => (
  <div style={{ height: '100vh', width: '100vw' }}>
    <Mosaic<ViewId>
      renderTile={(id, path) => (
        <MosaicWindow<ViewId>
          path={path}
          createNode={() => 'new' as ViewId}
          title={TITLE_MAP[id]}
        >
          <div style={{ padding: '20px' }}>
            <h2>{TITLE_MAP[id]}</h2>
            <p>This is the content for {id}</p>
          </div>
        </MosaicWindow>
      )}
      initialValue={{
        type: 'split',
        direction: 'row',
        splitPercentages: [40, 60],
        children: [
          'a',
          {
            type: 'split',
            direction: 'column',
            splitPercentages: [50, 50],
            children: ['b', 'c'],
          },
        ],
      }}
      className="mosaic-blueprint-theme"
      blueprintNamespace="bp5"
    />
  </div>
);

📖 Documentation

Core Concepts

🌳 Tree Structure

React Mosaic uses an n-ary tree structure to represent layouts:

  • Split Nodes: Container nodes that divide space between children
  • Tab Nodes: Container nodes that stack children in tabs
  • Leaf Nodes: Individual panels identified by unique keys
type MosaicNode<T> = MosaicSplitNode<T> | MosaicTabsNode<T> | T;

interface MosaicSplitNode<T> {
  type: 'split';
  direction: 'row' | 'column';
  children: MosaicNode<T>[];
  splitPercentages?: number[];
}

interface MosaicTabsNode<T> {
  type: 'tabs';
  tabs: T[];
  activeTabIndex: number;
}

🛣️ Paths

Paths in Mosaic are arrays of numbers representing the route to a node:

  • [] - Root node
  • [0] - First child of root
  • [1, 2] - Third child of second child of root

API Reference

Mosaic Component

interface MosaicProps<T> {
  // Required
  renderTile: (id: T, path: MosaicPath) => ReactElement;

  // State Management (use one or the other)
  initialValue?: MosaicNode<T> | null; // Uncontrolled
  value?: MosaicNode<T> | null; // Controlled

  // Event Handlers
  onChange?: (newNode: MosaicNode<T> | null) => void;
  onRelease?: (newNode: MosaicNode<T> | null) => void;

  // Styling & Theming
  className?: string;
  blueprintNamespace?: string;

  // Functionality
  createNode?: CreateNode<T>;
  resize?: ResizeOptions;
  zeroStateView?: ReactElement;
  renderTabTitle?: TabTitleRenderer<T>;

  // Drag & Drop
  dragAndDropManager?: DragDropManager;
  mosaicId?: string;
}

MosaicWindow Component

interface MosaicWindowProps<T> {
  // Required
  title: string;
  path: MosaicPath;

  // Styling
  className?: string;

  // Controls
  toolbarControls?: ReactNode;
  additionalControls?: ReactNode;
  additionalControlButtonText?: string;

  // Behavior
  draggable?: boolean;
  createNode?: CreateNode<T>;

  // Event Handlers
  onDragStart?: () => void;
  onDragEnd?: (type: 'drop' | 'reset') => void;
  onAdditionalControlsToggle?: (open: boolean) => void;

  // Customization
  renderPreview?: (props: MosaicWindowProps<T>) => ReactElement;
  renderToolbar?: (props: MosaicWindowProps<T>) => ReactElement;

  // Advanced
  disableAdditionalControlsOverlay?: boolean;
}

🎨 Theming

Blueprint Theme (Recommended)

React Mosaic includes built-in support for the Blueprint design system:

import '@blueprintjs/core/lib/css/blueprint.css';
import '@blueprintjs/icons/lib/css/blueprint-icons.css';

<Mosaic
  className="mosaic-blueprint-theme"
  blueprintNamespace="bp5" // Latest Blueprint version
  // ... other props
/>;

Dark Theme

Enable Blueprint's dark theme:

<Mosaic
  className="mosaic-blueprint-theme bp5-dark"
  // ... other props
/>

Custom Themes

Create your own themes by styling the CSS classes:

.my-custom-theme {
  --mosaic-window-border: 1px solid #e1e8ed;
  --mosaic-window-background: #ffffff;
  --mosaic-toolbar-background: #f5f8fa;
}

.my-custom-theme .mosaic-window {
  border: var(--mosaic-window-border);
  background: var(--mosaic-window-background);
}

.my-custom-theme .mosaic-window-toolbar {
  background: var(--mosaic-toolbar-background);
}

📝 Advanced Examples

Controlled Component

import React, { useState } from 'react';
import { Mosaic, MosaicNode } from '@lonli-lokli/react-mosaic-component';

const ControlledExample = () => {
  const [currentNode, setCurrentNode] = useState<MosaicNode<string> | null>({
    type: 'split',
    direction: 'row',
    splitPercentages: [50, 50],
    children: ['panel1', 'panel2'],
  });

  const handleAddPanel = () => {
    setCurrentNode({
      type: 'split',
      direction: 'column',
      splitPercentages: [70, 30],
      children: [currentNode!, 'new-panel'],
    });
  };

  return (
    <div>
      <button onClick={handleAddPanel}>Add Panel</button>
      <Mosaic<string>
        value={currentNode}
        onChange={setCurrentNode}
        renderTile={(id, path) => (
          <MosaicWindow title={id} path={path}>
            <div>Content for {id}</div>
          </MosaicWindow>
        )}
      />
    </div>
  );
};

Custom Toolbar Controls

import {
  RemoveButton,
  SplitButton,
  ExpandButton,
  Separator,
} from 'react-mosaic-component';

const customControls = (
  <>
    <SplitButton />
    <ExpandButton />
    <Separator />
    <RemoveButton />
    <button
      className="mosaic-default-control"
      onClick={() => console.log('Custom action')}
    >
      Custom
    </button>
  </>
);

<MosaicWindow title="My Panel" path={path} toolbarControls={customControls}>
  {/* Panel content */}
</MosaicWindow>;

Tab Groups

const tabsExample: MosaicNode<string> = {
  type: 'tabs',
  tabs: ['tab1', 'tab2', 'tab3'],
  activeTabIndex: 0,
};

<Mosaic
  initialValue={tabsExample}
  renderTile={(id, path) => (
    <div style={{ padding: '20px' }}>
      <h3>Tab Content: {id}</h3>
      <p>This is content for {id}</p>
    </div>
  )}
  renderTabTitle={(tabKey) => `📄 ${tabKey.toUpperCase()}`}
/>;

Complex Layout with Mixed Content

const complexLayout: MosaicNode<string> = {
  type: 'split',
  direction: 'column',
  splitPercentages: [30, 70],
  children: [
    {
      type: 'tabs',
      tabs: ['overview', 'settings', 'help'],
      activeTabIndex: 0,
    },
    {
      type: 'split',
      direction: 'row',
      splitPercentages: [60, 40],
      children: [
        'main-content',
        {
          type: 'split',
          direction: 'column',
          splitPercentages: [50, 50],
          children: ['sidebar1', 'sidebar2'],
        },
      ],
    },
  ],
};

🔧 Utilities & Tree Manipulation

Working with Trees

import {
  getLeaves,
  createBalancedTreeFromLeaves,
  updateTree,
  createRemoveUpdate,
  createExpandUpdate,
  getNodeAtPath,
} from '@lonli-lokli/react-mosaic-component';

// Get all leaf nodes (panel IDs)
const leaves = getLeaves(currentTree);
console.log('Panel IDs:', leaves); // ['panel1', 'panel2', 'panel3']

// Create a balanced layout from panels
const balancedTree = createBalancedTreeFromLeaves(leaves);

// Remove a node at a specific path
const removeUpdate = createRemoveUpdate([1, 0]); // Remove first child of second child
const newTree = updateTree(currentTree, [removeUpdate]);

// Expand a node to take more space
const expandUpdate = createExpandUpdate([0], 80); // Expand first child to 80%
const expandedTree = updateTree(currentTree, [expandUpdate]);

// Get a specific node
const nodeAtPath = getNodeAtPath(currentTree, [1, 0]);

Context API

Access Mosaic's context in child components:

import { useContext } from 'react';
import { MosaicWindowContext } from '@lonli-lokli/react-mosaic-component';

const MyCustomComponent = () => {
  const { mosaicActions, mosaicWindowActions } =
    useContext(MosaicWindowContext);

  const handleSplit = async () => {
    await mosaicWindowActions.split();
  };

  const handleRemove = () => {
    const path = mosaicWindowActions.getPath();
    mosaicActions.remove(path);
  };

  return (
    <div>
      <button onClick={handleSplit}>Split Window</button>
      <button onClick={handleRemove}>Remove Window</button>
    </div>
  );
};

🏗️ Development

Requirements

  • Node.js 18+
  • npm/yarn/pnpm
  • React 16-19

Setup

# Clone the repository
git clone https://github.com/lonli-lokli/react-mosaic.git
cd react-mosaic

# Install dependencies
npm install

# Start development server
npm start

# Run tests
npm test

# Build library
npm run build:lib

Project Structure

react-mosaic/
├── apps/
│   └── demo-app/          # Demo application
├── libs/
│   └── react-mosaic-component/  # Main library
├── test/                  # Test files
├── docs/                  # Documentation
└── tools/                 # Build tools

🤝 Contributing

We welcome contributions! Here's how to get started:

Development Workflow

  1. Fork the repository
  2. Clone your fork: git clone https://github.com/lonli-lokli/react-mosaic.git
  3. Create a feature branch: git checkout -b feature/amazing-feature
  4. Install dependencies: npm install
  5. Make your changes
  6. Add tests for new functionality
  7. Run the test suite: npm test
  8. Lint your code: npm run lint
  9. Commit your changes: git commit -m 'Add amazing feature'
  10. Push to your branch: git push origin feature/amazing-feature
  11. Submit a pull request

Code Style

This project uses:

  • ESLint for code linting
  • Prettier for code formatting
  • TypeScript for type checking

Run these commands to ensure code quality:

npm run lint        # Check linting
npm run lint:fix    # Fix linting issues
npm run format      # Format code with Prettier
npm run type-check  # Check TypeScript types

Guidelines

  • Follow the existing code style
  • Write tests for new features
  • Update documentation as needed
  • Keep pull requests focused and small
  • Use descriptive commit messages

🧪 Testing

# Run all tests
npm test

# Run tests in watch mode
npm run test:watch

# Run tests with coverage
npm run test:coverage

# Run specific test file
npm test -- --testNamePattern="Mosaic"

📦 Building

# Build the library
npm run build:lib

# Build the demo app
npm run build:app

# Build everything
npm run build

# Build and watch for changes
npm run build:watch

🚀 Deployment

The demo app is automatically deployed to GitHub Pages when changes are pushed to the main branch.

To deploy manually:

npm run build:app
npm run deploy

🐛 Browser Support

| Browser | Version | | ------- | ------- | | Chrome | 90+ | | Firefox | 88+ | | Safari | 14+ | | Edge | 90+ |

📋 Migration Guide

From react-mosaic-component to @lonli-lokli/react-mosaic-component

Version 1 introduces significant changes with n-ary tree support:

Tree Structure Changes:

  • Binary trees (first/second) → N-ary trees (children array)
  • splitPercentagesplitPercentages array
  • New tab node type: MosaicTabsNode

Path Changes:

  • String paths (['first', 'second']) → Numeric paths ([0, 1])

Type Changes:

Replace deprecated types with new ones:

// OLD: Deprecated types
import { 
  MosaicBranch,    // ❌ Use numeric indices instead
  MosaicParent,    // ❌ Use MosaicSplitNode instead
  MosaicNode       // ✅ Still valid but structure changed
} from 'react-mosaic-component';

// NEW: Updated types
import { 
  MosaicSplitNode,     // ✅ Replaces MosaicParent
  MosaicTabsNode,      // ✅ New tab container type
  MosaicNode,          // ✅ Union of split, tabs, or leaf
  MosaicPath,          // ✅ Now number[] instead of string[]
  // Helper functions for type checking
  isSplitNode,
  isTabsNode,
  convertLegacyToNary  // ✅ Migration utility
} from '@lonli-lokli/react-mosaic-component';

Migration Steps:

  1. Update Type Checking Logic:
// OLD: Manual type checking
if ('first' in node && 'second' in node) {
  // Handle parent node
  const leftChild = node.first;
  const rightChild = node.second;
}

// NEW: Use helper functions
import { isSplitNode, isTabsNode } from '@lonli-lokli/react-mosaic-component';

if (isSplitNode(node)) {
  // Handle split node with multiple children
  node.children.forEach((child, index) => {
    // Process each child
  });
} else if (isTabsNode(node)) {
  // Handle tab container
  node.tabs.forEach((tab, index) => {
    // Process each tab
  });
} else {
  // Handle leaf node (panel)
  console.log('Panel ID:', node);
}
  1. Update Path Handling:
// OLD: String-based paths
const oldPath: MosaicBranch[] = ['first', 'second'];
const childPath = [...parentPath, 'first'];

// NEW: Numeric paths
const newPath: MosaicPath = [0, 1];
const childPath = [...parentPath, 0]; // First child
  1. Use Migration Utility for Existing Data:
import { convertLegacyToNary } from '@lonli-lokli/react-mosaic-component';

// Convert old tree structure to new format
const legacyTree = {
  direction: 'row',
  first: 'panel1',
  second: 'panel2',
  splitPercentage: 40
};

const modernTree = convertLegacyToNary(legacyTree);
// Result: { type: 'split', direction: 'row', children: ['panel1', 'panel2'], splitPercentages: [40, 60] }

Complete Migration Example:

// Old v6 structure
const oldTree = {
  direction: 'row',
  first: 'panel1',
  second: {
    direction: 'column',
    first: 'panel2',
    second: 'panel3',
    splitPercentage: 60,
  },
  splitPercentage: 40,
};

// New structure
const newTree = {
  type: 'split',
  direction: 'row',
  splitPercentages: [40, 60],
  children: [
    'panel1',
    {
      type: 'split',
      direction: 'column',
      splitPercentages: [60, 40],
      children: ['panel2', 'panel3'],
    },
  ],
};

// Or use the conversion utility
const convertedTree = convertLegacyToNary(oldTree);

Working with the New Tree Structure:

import { 
  MosaicNode, 
  MosaicSplitNode, 
  MosaicTabsNode,
  isSplitNode,
  isTabsNode,
  getNodeAtPath,
  getLeaves
} from '@lonli-lokli/react-mosaic-component';

// Type-safe tree traversal
function processNode<T>(node: MosaicNode<T>, path: number[] = []): void {
  if (isSplitNode(node)) {
    console.log(`Split node at path [${path.join(', ')}]:`, node.direction);
    node.children.forEach((child, index) => {
      processNode(child, [...path, index]);
    });
  } else if (isTabsNode(node)) {
    console.log(`Tab group at path [${path.join(', ')}]:`, node.tabs.length, 'tabs');
    node.tabs.forEach((tab, index) => {
      console.log(`  Tab ${index}:`, tab);
    });
  } else {
    console.log(`Panel at path [${path.join(', ')}]:`, node);
  }
}

// Get all panels in the tree
const allPanels = getLeaves(tree);

// Get specific node by path
const nodeAtPath = getNodeAtPath(tree, [1, 0]); // Second child's first child

Initial value can be specified in legacy format as the convertLegacyToNary utility is used internally to migrate old trees automatically.

❓ FAQ

Q: Can I use React Mosaic without Blueprint? A: Yes! Simply omit Blueprint CSS imports and use className="" instead of the Blueprint theme classes.

Q: How do I save and restore layouts? A: The tree structure is serializable JSON. Save the currentNode state and restore it as initialValue or value.

Q: Can I customize the drag handles? A: Yes, use the renderToolbar prop on MosaicWindow to completely customize the toolbar.

Q: Is server-side rendering (SSR) supported? A: Yes, React Mosaic works with SSR frameworks like Next.js, Gatsby, and others.

Q: How do I handle deep nested layouts? A: Use the utility functions like getNodeAtPath and updateTree to efficiently work with complex trees.

Q: Can I disable drag and drop? A: Yes, set draggable={false} on MosaicWindow components or don't provide createNode.

Q: How do I add keyboard shortcuts? A: Implement keyboard event handlers in your components and use the context API to trigger actions.

Q: Is there a maximum number of panels? A: No hard limit, but performance may degrade with very large numbers of panels (1000+).

🆘 Support

📄 License

Copyright 2019 Kevin Verdieck, originally developed at Palantir Technologies, Inc.

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.


⭐ Star this repository if you find it useful!