Back to Blog

React Resizable Panels in 2026: react-resizable-panels, Drag Handles, and Persistent Layout

Build resizable panel layouts in React with react-resizable-panels: drag handles, collapsible panels, keyboard navigation, persistent layout, and sidebar + code editor patterns.

Viprasol Tech Team
February 9, 2027
13 min read

React Resizable Panels in 2026: react-resizable-panels, Drag Handles, and Persistent Layout

Resizable panels are a staple of developer tools, data dashboards, and IDE-style interfaces. Users expect to drag a handle to resize the sidebar, collapse a panel to maximize working space, and have the layout persisted across sessions.

react-resizable-panels (by Brian Vaughn, the React DevTools author) is the 2026 standard for this โ€” it handles keyboard navigation, accessibility, SSR, and persistence out of the box. This post covers the common patterns: sidebar + main content, horizontal/vertical splits, collapsible panels, and saving layout to localStorage.


Installation

npm install react-resizable-panels

Basic Horizontal Split

// components/Layout/SplitLayout.tsx
"use client";

import {
  PanelGroup,
  Panel,
  PanelResizeHandle,
} from "react-resizable-panels";

export function SplitLayout({
  sidebar,
  content,
}: {
  sidebar: React.ReactNode;
  content: React.ReactNode;
}) {
  return (
    <PanelGroup
      direction="horizontal"
      className="h-full"
      // Persist layout to localStorage
      autoSaveId="main-layout"
    >
      {/* Sidebar panel */}
      <Panel
        defaultSize={20}     // 20% of available space
        minSize={15}          // Can't shrink below 15%
        maxSize={40}          // Can't grow beyond 40%
        className="overflow-hidden"
      >
        {sidebar}
      </Panel>

      {/* Drag handle */}
      <PanelResizeHandle className="w-1.5 bg-gray-100 hover:bg-blue-400 transition-colors cursor-col-resize" />

      {/* Main content panel */}
      <Panel className="overflow-hidden">
        {content}
      </Panel>
    </PanelGroup>
  );
}

๐ŸŒ Looking for a Dev Team That Actually Delivers?

Most agencies sell you a project manager and assign juniors. Viprasol is different โ€” senior engineers only, direct Slack access, and a 5.0โ˜… Upwork record across 100+ projects.

  • React, Next.js, Node.js, TypeScript โ€” production-grade stack
  • Fixed-price contracts โ€” no surprise invoices
  • Full source code ownership from day one
  • 90-day post-launch support included

Collapsible Sidebar

// components/Layout/CollapsibleSidebar.tsx
"use client";

import { useRef, useState } from "react";
import {
  PanelGroup,
  Panel,
  PanelResizeHandle,
  type ImperativePanelHandle,
} from "react-resizable-panels";
import { ChevronLeft, ChevronRight } from "lucide-react";

export function CollapsibleSidebar({
  sidebar,
  content,
}: {
  sidebar: React.ReactNode;
  content: React.ReactNode;
}) {
  const sidebarRef = useRef<ImperativePanelHandle>(null);
  const [collapsed, setCollapsed] = useState(false);

  const toggleSidebar = () => {
    const panel = sidebarRef.current;
    if (!panel) return;

    if (collapsed) {
      panel.expand();    // Restore to last size (or defaultSize)
    } else {
      panel.collapse();  // Collapse to 0 (or minSize if not collapsible)
    }
  };

  return (
    <PanelGroup
      direction="horizontal"
      className="h-full"
      autoSaveId="collapsible-sidebar-layout"
    >
      <Panel
        ref={sidebarRef}
        defaultSize={20}
        minSize={0}
        collapsible         // Required for collapse() to work
        collapsedSize={0}   // Size when collapsed
        onCollapse={() => setCollapsed(true)}
        onExpand={() => setCollapsed(false)}
        className="overflow-hidden"
      >
        <div
          className={`h-full transition-opacity duration-150 ${
            collapsed ? "opacity-0" : "opacity-100"
          }`}
        >
          {sidebar}
        </div>
      </Panel>

      <PanelResizeHandle className="relative w-1.5 bg-gray-100 hover:bg-blue-400 transition-colors cursor-col-resize group">
        {/* Toggle button on the handle */}
        <button
          onClick={toggleSidebar}
          className="absolute top-1/2 -translate-y-1/2 -right-3 z-10 flex h-6 w-6 items-center justify-center rounded-full bg-white border border-gray-200 shadow-sm hover:bg-gray-50 opacity-0 group-hover:opacity-100 transition-opacity"
          aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
        >
          {collapsed ? (
            <ChevronRight className="h-3 w-3 text-gray-500" />
          ) : (
            <ChevronLeft className="h-3 w-3 text-gray-500" />
          )}
        </button>
      </PanelResizeHandle>

      <Panel className="overflow-hidden">
        {content}
      </Panel>
    </PanelGroup>
  );
}

IDE-Style Three-Panel Layout

// components/Layout/IdeLayout.tsx
"use client";

import {
  PanelGroup,
  Panel,
  PanelResizeHandle,
  type ImperativePanelHandle,
} from "react-resizable-panels";
import { useRef } from "react";

interface IdeLayoutProps {
  fileTree: React.ReactNode;
  editor: React.ReactNode;
  terminal: React.ReactNode;
}

export function IdeLayout({ fileTree, editor, terminal }: IdeLayoutProps) {
  const terminalRef = useRef<ImperativePanelHandle>(null);

  return (
    // Outer: horizontal split (file tree | editor + terminal)
    <PanelGroup direction="horizontal" autoSaveId="ide-horizontal" className="h-screen">
      {/* File tree */}
      <Panel
        defaultSize={18}
        minSize={12}
        maxSize={35}
        collapsible
        className="overflow-hidden border-r border-gray-800"
      >
        {fileTree}
      </Panel>

      <PanelResizeHandle className="w-px bg-gray-800 hover:bg-blue-500 transition-colors cursor-col-resize" />

      {/* Right side: editor + terminal (vertical split) */}
      <Panel>
        <PanelGroup direction="vertical" autoSaveId="ide-vertical">
          {/* Editor */}
          <Panel defaultSize={70} minSize={30} className="overflow-hidden">
            {editor}
          </Panel>

          <PanelResizeHandle className="h-px bg-gray-800 hover:bg-blue-500 transition-colors cursor-row-resize" />

          {/* Terminal */}
          <Panel
            ref={terminalRef}
            defaultSize={30}
            minSize={10}
            collapsible
            className="overflow-hidden bg-gray-950"
          >
            {terminal}
          </Panel>
        </PanelGroup>
      </Panel>
    </PanelGroup>
  );
}

๐Ÿš€ Senior Engineers. No Junior Handoffs. Ever.

You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.

  • MVPs in 4โ€“8 weeks, full platforms in 3โ€“5 months
  • Lighthouse 90+ performance scores standard
  • Works across US, UK, AU timezones
  • Free 30-min architecture review, no commitment

Custom Drag Handle with Visual Indicator

// components/Layout/ResizeHandle.tsx
"use client";

import { PanelResizeHandle } from "react-resizable-panels";
import { GripVertical, GripHorizontal } from "lucide-react";
import { useState } from "react";

interface ResizeHandleProps {
  direction: "horizontal" | "vertical";
  className?: string;
}

export function ResizeHandle({ direction, className }: ResizeHandleProps) {
  const [isDragging, setIsDragging] = useState(false);

  const isHorizontal = direction === "horizontal";

  return (
    <PanelResizeHandle
      onDragging={setIsDragging}
      className={`
        relative flex items-center justify-center
        ${isHorizontal ? "w-2 cursor-col-resize" : "h-2 cursor-row-resize"}
        ${isDragging
          ? "bg-blue-500"
          : "bg-gray-100 hover:bg-gray-200"
        }
        transition-colors group
        ${className ?? ""}
      `}
    >
      {/* Drag grip icon */}
      <div
        className={`
          absolute flex items-center justify-center
          h-6 w-6 rounded-sm bg-white border border-gray-200 shadow-sm
          opacity-0 group-hover:opacity-100 transition-opacity
          ${isDragging ? "opacity-100" : ""}
        `}
      >
        {isHorizontal ? (
          <GripVertical className="h-4 w-4 text-gray-400" />
        ) : (
          <GripHorizontal className="h-4 w-4 text-gray-400" />
        )}
      </div>
    </PanelResizeHandle>
  );
}

Programmatic Control and Keyboard Shortcuts

// components/Layout/LayoutWithKeyboard.tsx
"use client";

import { useEffect, useRef } from "react";
import {
  PanelGroup,
  Panel,
  PanelResizeHandle,
  type ImperativePanelHandle,
} from "react-resizable-panels";

export function LayoutWithKeyboard({
  sidebar,
  content,
}: {
  sidebar: React.ReactNode;
  content: React.ReactNode;
}) {
  const sidebarRef = useRef<ImperativePanelHandle>(null);

  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      // Ctrl+B: toggle sidebar (VS Code-style)
      if (e.ctrlKey && e.key === "b") {
        e.preventDefault();
        const panel = sidebarRef.current;
        if (!panel) return;

        if (panel.isCollapsed()) {
          panel.expand();
        } else {
          panel.collapse();
        }
      }

      // Ctrl+Shift+`: toggle terminal (if you have one)
      if (e.ctrlKey && e.shiftKey && e.key === "`") {
        e.preventDefault();
        // Toggle terminal panel similarly
      }
    };

    window.addEventListener("keydown", handleKeyDown);
    return () => window.removeEventListener("keydown", handleKeyDown);
  }, []);

  return (
    <PanelGroup direction="horizontal" autoSaveId="keyboard-layout" className="h-full">
      <Panel
        ref={sidebarRef}
        defaultSize={22}
        minSize={15}
        collapsible
      >
        {sidebar}
      </Panel>
      <PanelResizeHandle className="w-1 bg-gray-200 hover:bg-blue-400 cursor-col-resize" />
      <Panel>{content}</Panel>
    </PanelGroup>
  );
}

Custom Layout Persistence (Beyond autoSaveId)

autoSaveId saves to localStorage automatically, but you might want to persist per-user on the server:

// hooks/usePersistentLayout.ts
"use client";

import { useCallback } from "react";
import { useMutation, useQuery } from "@tanstack/react-query";

export function usePersistentLayout(layoutKey: string) {
  const { data: savedLayout } = useQuery({
    queryKey: ["layout", layoutKey],
    queryFn: async () => {
      const res = await fetch(`/api/user/layout?key=${layoutKey}`);
      if (!res.ok) return null;
      return res.json() as Promise<number[]>; // Panel sizes as percentages
    },
  });

  const { mutate: saveLayout } = useMutation({
    mutationFn: (sizes: number[]) =>
      fetch("/api/user/layout", {
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ key: layoutKey, sizes }),
      }),
  });

  const onLayout = useCallback(
    (sizes: number[]) => {
      // Debounce saves โ€” don't fire on every drag pixel
      saveLayout(sizes);
    },
    [saveLayout]
  );

  return { savedLayout, onLayout };
}
// Usage:
export function PersistentLayout({ sidebar, content }: Props) {
  const { savedLayout, onLayout } = usePersistentLayout("main-sidebar");

  return (
    <PanelGroup
      direction="horizontal"
      onLayout={onLayout}
      // No autoSaveId โ€” we handle persistence ourselves
    >
      <Panel defaultSize={savedLayout?.[0] ?? 20} minSize={15}>
        {sidebar}
      </Panel>
      <PanelResizeHandle className="w-1 bg-gray-200 hover:bg-blue-400 cursor-col-resize" />
      <Panel defaultSize={savedLayout?.[1] ?? 80}>
        {content}
      </Panel>
    </PanelGroup>
  );
}

Accessibility

react-resizable-panels includes built-in keyboard navigation for the resize handles:

  • Arrow keys: Resize panels when handle is focused
  • Home/End: Snap to min/max size
  • Enter: Reset to default size
// Add aria-label to the handle for screen readers
<PanelResizeHandle
  aria-label="Resize sidebar"
  className="w-1.5 bg-gray-200 hover:bg-blue-400 cursor-col-resize focus:outline-none focus:ring-2 focus:ring-blue-500"
/>

Cost and Timeline

ComponentTimelineCost (USD)
Basic split layout0.5 day$300โ€“$500
Collapsible sidebar with toggle0.5โ€“1 day$400โ€“$800
IDE-style nested panels1โ€“2 days$800โ€“$1,600
Custom drag handle + keyboard shortcuts0.5 day$300โ€“$500
Server-persisted layout0.5โ€“1 day$400โ€“$800
Full IDE-style layout system1โ€“2 weeks$5,000โ€“$8,000

See Also


Working With Viprasol

We build IDE-style and dashboard layouts with resizable panels for developer tools, data platforms, and internal applications. Our team has shipped multi-panel layouts with nested splits, programmatic collapse, keyboard shortcuts, and server-persisted layout preferences.

What we deliver:

  • PanelGroup setup with autoSaveId or server-side persistence
  • Collapsible sidebar with keyboard toggle (Ctrl+B)
  • IDE-style horizontal + vertical nested splits
  • Custom drag handles with grip icons and drag state
  • Responsive behavior (stack vertically on mobile)

Explore our web development services or contact us to build your resizable panel interface.

Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need a Modern Web Application?

From landing pages to complex SaaS platforms โ€” we build it all with Next.js and React.

Free consultation โ€ข No commitment โ€ข Response within 24 hours

Viprasol ยท Web Development

Need a custom web application built?

We build React and Next.js web applications with Lighthouse โ‰ฅ90 scores, mobile-first design, and full source code ownership. Senior engineers only โ€” from architecture through deployment.